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};
13
14#[derive(Debug, Clone)]
15pub struct ClassOptions {
16    /// Prefix each file with a provenance comment.
17    pub provenance: bool,
18}
19
20impl Default for ClassOptions {
21    fn default() -> Self {
22        ClassOptions { provenance: true }
23    }
24}
25
26/// Decompile one class (plus nested member classes) to a Java source file.
27///
28/// Classes with large method bodies run in a monitored thread with a
29/// deadline: pathological CFGs can drive the shared structurer's walk into
30/// an exponential exploration that never returns. On timeout the class is
31/// reported failed and the thread is abandoned (reaped at process exit).
32/// One registered monitored decompile: the receiver the worker polls at
33/// the tail, the class name (for diagnostics), and the deadline counted
34/// from the spawn.
35pub type PendingMonitor = (
36    std::sync::mpsc::Receiver<Result<String, String>>,
37    String,
38    std::time::Instant,
39);
40
41pub fn decompile_class(
42    pool: &std::sync::Arc<DexPool>,
43    class: &PoolClass,
44    opts: &ClassOptions,
45    pending: &std::sync::Mutex<Vec<PendingMonitor>>,
46) -> anyhow::Result<String> {
47    if class_is_risky(pool, class) {
48        // Detached monitored thread: the CALLER registers the receiver and
49        // moves on (awaiting happens at the end of the run) — a spinning
50        // pathological method no longer stalls its worker.
51        let pool2 = pool.clone();
52        let cls = class.clone();
53        let opts2 = opts.clone();
54        let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
55        let name = class.name.clone();
56        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
57        let _ = std::thread::Builder::new()
58            .stack_size(64 * 1024 * 1024)
59            .spawn(move || {
60                let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
61                let _ = tx.send(r);
62            });
63        pending.lock().unwrap().push((rx, name, deadline));
64        return Err(anyhow::anyhow!(
65            "deferred to monitored thread (pathological-CFG guard)"
66        ));
67    }
68    decompile_class_impl(pool, class, opts)
69}
70
71/// Any method in the exponential-walk danger zone → run under the
72/// deadline. Calibrated between real workloads: routine R8 classes top out
73/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
74/// (weibo's gson TypeAdapters) sit at 24k+.
75fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
76    // Header peek only — decoding every body here would double the APK's
77    // total decode work.
78    for m in class.all_methods() {
79        if m.code_off == 0 {
80            continue;
81        }
82        if let Some(dex) = pool.dex(m.dex_idx) {
83            if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
84                if insns > 16_000 {
85                    return true;
86                }
87            }
88        }
89    }
90    false
91}
92
93fn decompile_class_impl(
94    pool: &DexPool,
95    class: &PoolClass,
96    opts: &ClassOptions,
97) -> anyhow::Result<String> {
98    let ctx = DexCtx::new(pool, class);
99    // Size the class buffer up front: corpus-average classes render to
100    // ~1KB per method — without the reserve the String doubles through
101    // 4-6 realloc+copy rounds per class (~2× the final size in memmove).
102    // Cap the reserve: weixin's monster classes (hundreds of methods)
103    // would reserve megabytes of untouched capacity per class — large
104    // blocks take mimalloc's commit/purge path (madvise churn showed up
105    // in profiles). 256KB covers the corpus-average class dozens of
106    // times over; bigger classes pay a few extra doublings.
107    let mut out = String::with_capacity(
108        (class.all_methods().count() * 1024).min(256 * 1024) + 512,
109    );
110    if opts.provenance {
111        static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
112        out.push_str(BANNER.get_or_init(|| {
113            format!(
114                "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
115                env!("CARGO_PKG_VERSION")
116            )
117        }));
118        // Provenance: which input image this class was lifted from (jadx's
119        // `loaded from: classes.dex` pattern). Byte-stable across runs —
120        // deliberately NO timestamp so outputs diff cleanly. Direct
121        // push_str: the per-class format! temporaries were 3 allocations
122        // × every class in the corpus.
123        if let Some(dex) = pool.dex(class.dex_idx) {
124            if let Some(label) = pool.dex_labels.get(class.dex_idx) {
125                out.push_str("// From: ");
126                out.push_str(label);
127                out.push_str(" (DEX ");
128                out.push_str(&dex.version);
129                out.push_str(")\n");
130            }
131        }
132    }
133    if let Some(src) = &class.source_file {
134        out.push_str("// Source file: ");
135        out.push_str(src);
136        out.push('\n');
137    }
138    if class.is_synthetic() {
139        out.push_str("// synthetic\n");
140    }
141    let (pkg, _) = split_name(&class.name);
142    if !pkg.is_empty() {
143        out.push('\n');
144        out.push_str("package ");
145        let dotted = pkg.replace('/', ".");
146        out.push_str(&sanitize_fq(&dotted));
147        out.push_str(";\n");
148    }
149    out.push('\n');
150    // Emit straight into `out`: the intermediate body buffer copied the
151    // whole rendered class (corpus-average ~10KB) a second time — pure
152    // memmove traffic; the error path discards `out` either way.
153    emit_class_body(pool, class, &ctx, opts, &mut out, 0)?;
154    Ok(out)
155}
156
157/// Render the class header, fields, methods and nested member classes into
158/// `out` (used at top level and recursively for nested members).
159// `opts` is only consumed by the nested-member recursion below — that
160// is its purpose (propagating emission options into inline children).
161/// One enum constant: the source identifier plus constructor
162/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
163use jdc_core::ir::expr::{ConstVal, Expr};
164use jdc_core::ir::stmt::Stmt;
165struct EnumConst {
166    #[allow(dead_code)]
167    field: String,
168    name: String,
169    extra_args: Vec<Expr>,
170}
171
172/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
173/// static field must be initialized in `<clinit>` by
174/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
175/// always emit. Returns None (caller falls back to the desugared
176/// `/* enum */ class` form) when anything is missing: R8 variance,
177/// constant-specific bodies (the field holds an anonymous subclass), or
178/// a <clinit> that failed to decompile.
179fn collect_enum_constants(
180    pool: &DexPool,
181    class: &PoolClass,
182    ctx: &DexCtx<'_>,
183) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
184    let _ = ctx;
185    let const_fields: Vec<&PoolField> = class
186        .static_fields
187        .iter()
188        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
189        .collect();
190    if const_fields.is_empty() {
191        return None;
192    }
193    let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
194    let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
195
196    // R8/d8 split the constant build across an intermediate local:
197    //   Self v0 = new Self("NAME", i, ...);
198    //   a = v0;                       // sput to the ACC_ENUM field
199    //   Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
200    // Pass 1 registers each intermediate (or direct) new; pass 2 binds
201    // them to the ACC_ENUM fields; references to the locals are then
202    // rewritten into constant identifiers so the $VALUES build keeps
203    // compiling once the definitions drop.
204    use std::collections::HashMap;
205    let mut const_name: Vec<String> = Vec::new();
206    let mut const_field: Vec<String> = Vec::new();
207    let mut const_extra: Vec<Vec<Expr>> = Vec::new();
208    let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
209    let mut drop_stmts: Vec<usize> = Vec::new();
210
211    // Collect (immutable borrows) first; the mutable passes come after.
212    if !matches!(&body.body, Stmt::Block(_)) {
213        return None;
214    }
215
216    // Pass 1: definitions. (immutable borrow; rewrite comes later)
217    for (i, st) in match &body.body {
218        Stmt::Block(v) => v.iter().enumerate(),
219        _ => return None,
220    } {
221        if let Stmt::LocalDef {
222            var,
223            init: Some(Expr::New { cls: ncls, args, .. }),
224            ..
225        } = st
226        {
227            if ncls.as_ref() == class.name && args.len() >= 2 {
228                if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(_))) =
229                    (&args[0], &args[1])
230                {
231                    if java_ident(n).as_ref() != &**n || n.is_empty() {
232                        return None;
233                    }
234                    var_of.insert(*var, const_name.len());
235                    const_name.push(n.to_string());
236                    const_field.push(String::new());
237                    const_extra.push(args[2..].to_vec());
238                    drop_stmts.push(i);
239                }
240            }
241        }
242    }
243
244    // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
245    for (i, st) in match &body.body {
246        Stmt::Block(v) => v.iter().enumerate(),
247        _ => return None,
248    } {
249        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
250            if let Expr::Field {
251                cls,
252                name: fname,
253                is_static: true,
254                ..
255            } = &**target
256            {
257                if cls.as_ref() != class.name {
258                    continue;
259                }
260                if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
261                    continue;
262                }
263                if const_field.iter().any(|f| !f.is_empty() && f == &**fname) {
264                    return None; // duplicate assignment
265                }
266                let idx = match &**value {
267                    Expr::Local { var, .. } => var_of.get(var).copied()?,
268                    Expr::New { cls: ncls, args, .. }
269                        if ncls.as_ref() == class.name && args.len() >= 2 =>
270                    {
271                        if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(_))) =
272                            (&args[0], &args[1])
273                        {
274                            if java_ident(n).as_ref() != &**n || n.is_empty() {
275                                return None;
276                            }
277                            let idx = const_name.len();
278                            const_name.push(n.to_string());
279                            const_field.push(String::new());
280                            const_extra.push(args[2..].to_vec());
281                            idx
282                        } else {
283                            return None;
284                        }
285                    }
286                    _ => return None,
287                };
288                const_field[idx] = fname.to_string();
289                drop_stmts.push(i);
290            }
291        }
292    }
293
294    // Every ACC_ENUM field bound, every intermediate matched.
295    if const_field.len() != const_fields.len() || const_field.iter().any(|f| f.is_empty()) {
296        return None;
297    }
298    {
299        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
300        if !const_name.iter().all(|c| seen.insert(c.as_str())) {
301            return None;
302        }
303    }
304
305    // Pass 3: rewrite references to the intermediate locals into the
306    // constant identifiers (static-field reads on Self).
307    let var2name: jdc_core::FxHashMap<u32, &str> = var_of
308        .iter()
309        .map(|(v, &i)| (*v, const_name[i].as_str()))
310        .collect();
311    let self_name: std::sync::Arc<str> = class.name.as_str().into();
312    let self_ty =
313        jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
314    crate::passes::rewrite_exprs(&mut body.body, &mut |e| {
315        crate::passes::deep_rewrite(e, &mut |x| {
316            if let Expr::Local { var, .. } = x {
317                if let Some(n) = var2name.get(var) {
318                    let name: std::sync::Arc<str> = std::sync::Arc::from(*n);
319                    *x = Expr::Field {
320                        owner: None,
321                        cls: self_name.clone(),
322                        name,
323                        ty: self_ty.clone(),
324                        is_static: true,
325                    };
326                }
327            }
328        });
329    });
330
331    // Pass 4: drop the definitions and their sputs.
332    if let Stmt::Block(vs) = &mut body.body {
333        let drop_set: std::collections::HashSet<usize> =
334            drop_stmts.iter().copied().collect();
335        vs.retain(|_| true);
336        let mut idx = 0usize;
337        vs.retain(|_| {
338            let keep = !drop_set.contains(&idx);
339            idx += 1;
340            keep
341        });
342    }
343
344    let out: Vec<EnumConst> = const_field
345        .into_iter()
346        .zip(const_name)
347        .zip(const_extra)
348        .map(|((field, name), extra_args)| EnumConst {
349            field,
350            name,
351            extra_args,
352        })
353        .collect();
354    Some((out, body))
355}
356
357/// Render enum-constant constructor arguments via the shared expression
358/// emitter (the VarTable is irrelevant for argument printing — no local
359/// names appear — but the API requires one).
360fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
361    let dummy_vt = jdc_core::var::VarTable::default();
362    let mut p = Printer::new(ctx, &dummy_vt);
363    for (i, a) in args.iter().enumerate() {
364        if i > 0 {
365            out.push_str(", ");
366        }
367        p.expr(a, 0, out);
368    }
369    let _ = pool;
370}
371
372#[allow(clippy::only_used_in_recursion)]
373fn emit_class_body(
374    pool: &DexPool,
375    class: &PoolClass,
376    ctx: &DexCtx<'_>,
377    opts: &ClassOptions,
378    out: &mut String,
379    depth: usize,
380) -> anyhow::Result<()> {
381    let ind = indent(depth);
382    // The class's own header must use the RENAMED display name — the
383    // ctor path (is_init) already renames; a header declaring `class a`
384    // while the file/ctor say `a_2` leaves the ctor looking like a
385    // method with no return type (MinisApp's Y2.a vs y2.a package
386    // case-collision: 2,518 javac parse errors).
387    let cname = crate::apply_class_rename(&class.name);
388    let (_, simple) = split_name(&cname);
389    // A class emitted as its OWN top-level file (depth 0) must declare a
390    // flat `$` name — `class Outer.Inner` is not declarable at file
391    // scope. An INLINE nested member (depth > 0) declares its own
392    // segment inside the parent's body.
393    let simple = if depth == 0 {
394        simple.to_string()
395    } else {
396        // R8 names can END in `$`: an empty last `$`-segment would
397        // render `class  {` — keep the whole simple name.
398        let seg = simple.rsplit('$').next().unwrap_or(&simple);
399        if seg.is_empty() {
400            simple.to_string()
401        } else {
402            seg.to_string()
403        }
404    };
405    // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
406    // declared — restricted contextual type names escape here and in
407    // the ctor, file name, and every type reference.
408    let simple = if is_restricted_type_name(&simple) {
409        format!("_{simple}")
410    } else {
411        simple
412    };
413    let is_iface = class.is_interface();
414    let is_enum = class.is_enum();
415
416    let mut head = String::new();
417    let a = class.access;
418    if a & ACC_PUBLIC != 0 {
419        head.push_str("public ");
420    }
421    // Inline nested members need their `static` (interfaces/annotations
422    // are implicitly static; a missing `static` on a member class makes
423    // every `new Report(...)` site an "outer instance required" error).
424    if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
425        head.push_str("static ");
426    }
427    if a & ACC_FINAL != 0 && !is_enum {
428        head.push_str("final ");
429    }
430    if a & ACC_ABSTRACT != 0 && !is_iface {
431        head.push_str("abstract ");
432    }
433    if a & ACC_ANNOTATION != 0 {
434        head.push('@');
435    }
436    let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
437        collect_enum_constants(pool, class, ctx)
438    } else {
439        None
440    };
441    if let Some(ecs) = &enum_consts {
442        // True `enum` declaration: constants render in the header, the
443        // desugared boilerplate (const fields, their <clinit> inits —
444        // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
445        // disappears. R8-renamed values()/valueOf() stay (they do not
446        // collide with the compiler-generated ones); javac-named ones
447        // are skipped at the method loop below.
448        head.push_str("enum ");
449        head.push_str(&java_ident(&simple));
450        let _ = ecs;
451    } else if is_enum {
452        // An enum with constant-specific bodies carries ACC_ABSTRACT —
453        // `abstract final` is an illegal modifier combination; abstract
454        // (already pushed above) suppresses the hardcoded final.
455        let abstract_ = a & ACC_ABSTRACT != 0;
456        head.push_str(if abstract_ {
457            "/* enum */ class "
458        } else {
459            "/* enum */ final class "
460        });
461        head.push_str(&java_ident(&simple));
462    } else if is_iface {
463        head.push_str("interface ");
464        head.push_str(&java_ident(&simple));
465    } else {
466        head.push_str("class ");
467        head.push_str(&java_ident(&simple));
468    }
469    if is_iface {
470        if !class.interfaces.is_empty() {
471            head.push_str(" extends ");
472            head.push_str(&join_dotted(pool, &class.interfaces));
473        }
474    } else {
475        if let Some(sup) = &class.super_name {
476            if sup != "java/lang/Object" && !is_enum {
477                head.push_str(" extends ");
478                head.push_str(&print_class_name(pool, sup));
479            }
480        }
481        if !class.interfaces.is_empty() {
482            head.push_str(if is_iface {
483                " extends "
484            } else {
485                " implements "
486            });
487            head.push_str(&join_dotted(pool, &class.interfaces));
488        }
489    }
490
491    out.push_str(&ind);
492    out.push_str(&head);
493    out.push_str(" {\n");
494
495    // True-enum constant list: the constants lead the body, ahead of
496    // any remaining fields.
497    if let Some((ecs, _)) = &enum_consts {
498        for (i, ec) in ecs.iter().enumerate() {
499            out.push_str(&indent(depth + 1));
500            out.push_str(&java_ident(&ec.name));
501            if !ec.extra_args.is_empty() {
502                let mut line = String::from("(");
503                render_enum_args(ctx, pool, &ec.extra_args, &mut line);
504                line.push(')');
505                out.push_str(&line);
506            }
507            if i + 1 < ecs.len() {
508                out.push_str(",\n");
509            } else {
510                out.push_str(";\n");
511            }
512        }
513        out.push('\n');
514    }
515
516    // Fields.
517    let mut field_emitted = false;
518    for (i, f) in class.static_fields.iter().enumerate() {
519        // Enum constant fields became the header list above.
520        if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
521            continue;
522        }
523        if field_emitted || !class.instance_fields.is_empty() {
524            out.push('\n');
525        }
526        field_emitted = true;
527        emit_field(
528            pool,
529            f,
530            class.static_values.get(i),
531            &class.name,
532            out,
533            depth + 1,
534            true,
535            class.is_interface(),
536        );
537    }
538    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
539        out.push('\n');
540    }
541    for (i, f) in class.instance_fields.iter().enumerate() {
542        if i > 0 {
543            out.push('\n');
544        }
545        emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
546    }
547
548    // Methods.
549    // Duplicated (name, parameter-types) pairs in one class are javac
550    // "method is already defined" errors (weibo: 11.5k hits). Source
551    // cannot declare them, so exactly one survives: covariant bridges
552    // (`Object get(int)` beside `ByteString get(int)` — the compiler
553    // GENERATES bridges), plus R8 output that lost the bridge flag
554    // (okio Buffer.clone, interface getView re-declarations). A
555    // non-bridge method outranks a bridge for the same signature
556    // (bridge bodies are delegation stubs); first occurrence otherwise.
557    // Never drop a signature outright — the last method standing for a
558    // key is always rendered.
559    fn sig_key(m: &PoolMethod) -> (&str, &str) {
560        let d: &str = &m.desc;
561        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
562        let hi = d.find(')').unwrap_or(d.len());
563        (&*m.name, &d[lo..hi])
564    }
565    let methods: Vec<&PoolMethod> = class.all_methods().collect();
566    let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
567    for (i, m) in methods.iter().enumerate() {
568        let key = sig_key(m);
569        match claim.get(&key) {
570            Some(&j)
571                if m.access & crate::access::ACC_BRIDGE == 0
572                    && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
573            {
574                claim.insert(key, i);
575            }
576            None => {
577                claim.insert(key, i);
578            }
579            _ => {}
580        }
581    }
582    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
583    for (i, m) in methods.iter().enumerate() {
584        if &*m.name == "<clinit>" {
585            continue; // rendered after the fields
586        }
587        if claim.get(&sig_key(m)) != Some(&i) {
588            continue;
589        }
590        // True-enum rendering: javac auto-generates values()/valueOf()
591        // — the original-named ones must not re-declare (R8-renamed
592        // copies stay, they do not collide).
593        if let Some((ecs, _)) = &enum_consts {
594            let d = m.parsed_desc();
595            let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
596            let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
597            let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
598            let one_str = d
599                .as_ref()
600                .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
601                .unwrap_or(false);
602            let static_ = m.is_static();
603            if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
604                continue;
605            }
606            if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
607                continue;
608            }
609            // Enum constructors must be private in source form.
610            let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
611                let mut c = (*m).clone();
612                c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
613                Some(c)
614            } else {
615                None
616            };
617            let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
618            let mark = out.len();
619            if emitted_any {
620                out.push('\n');
621            }
622            if emit_method(pool, class, ctx, m_ref, depth + 1, out)? {
623                emitted_any = true;
624            } else {
625                out.truncate(mark);
626            }
627            let _ = ecs;
628            continue;
629        }
630        let mark = out.len();
631        if emitted_any {
632            out.push('\n');
633        }
634        if emit_method(pool, class, ctx, m, depth + 1, out)? {
635            emitted_any = true;
636        } else {
637            out.truncate(mark);
638        }
639    }
640    // Static initializer. INTERFACES cannot carry a `static { }` block in
641    // Java — their clinit only assigns constants, which static_values (or
642    // the `= null` default) already render as field initializers; skip
643    // the block entirely.
644    let skip_clinit = class.is_interface();
645    if let Some((_, clinit_body)) = enum_consts.take() {
646        // True-enum <clinit>: the constant assignments are already gone;
647        // render the remainder (the $VALUES array build) directly.
648        // Skip an empty remainder (all statements were constant inits).
649        let empty = match &clinit_body.body {
650            Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
651            _ => false,
652        };
653        if !empty {
654            if emitted_any {
655                out.push('\n');
656            }
657            out.push_str(&indent(depth + 1));
658            out.push_str("static {\n");
659            let p = Printer::new(ctx, &clinit_body.vt);
660            let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
661            for line in text.lines() {
662                if line.trim().is_empty() {
663                    continue;
664                }
665                out.push_str(line);
666                out.push('\n');
667            }
668            out.push_str(&indent(depth + 1));
669            out.push_str("}\n");
670            emitted_any = true;
671        }
672    } else if let Some(clinit) = (!skip_clinit)
673        .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
674        .flatten()
675    {
676        let mark = out.len();
677        if emitted_any {
678            out.push('\n');
679        }
680        if emit_method(pool, class, ctx, clinit, depth + 1, out)? {
681            emitted_any = true;
682        } else {
683            out.truncate(mark);
684        }
685    }
686
687    // Nested member classes (clean `$` tails only; anonymous/local/lambda
688    // classes are emitted as their own top-level files by the driver).
689    for nested in nested_members(pool, class, ctx) {
690        if emitted_any {
691            out.push('\n');
692        }
693        let nested_ctx = DexCtx::new(pool, nested);
694        out.push('\n');
695        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
696        emitted_any = true;
697    }
698
699    out.push_str(&ind);
700    out.push_str("}\n");
701    Ok(())
702}
703
704/// Member classes of `class` (direct children by the `$` chain / member
705/// annotations), excluding anonymous / local / lambda shapes.
706fn nested_members<'a>(
707    pool: &'a DexPool,
708    class: &'a PoolClass,
709    ctx: &DexCtx<'_>,
710) -> Vec<&'a PoolClass> {
711    let mut out = Vec::new();
712    for name in pool.children_of(&class.name) {
713        let Some(pc) = pool.get(name) else { continue };
714        // Only a REAL `outer$tail` name is an inline member: the
715        // children index also carries annotation-derived outers
716        // (EnclosingClass) with no naming relationship to the child
717        // (obfuscated apps pair a 1-char name with a long enclosing
718        // descriptor) — the blind slice panicked there (alipay
719        // `a.a.a.a.c`, exposed once children_of actually returned data).
720        let Some(rest) = name
721            .strip_prefix(class.name.as_str())
722            .and_then(|t| t.strip_prefix('$'))
723        else {
724            continue;
725        };
726        if rest.is_empty() || rest.starts_with('-') {
727            continue;
728        }
729        if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
730            continue;
731        }
732        let tail = rest.rsplit('$').next().unwrap_or(rest);
733        if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
734            continue; // anonymous
735        }
736        if tail.starts_with(|c: char| c.is_ascii_digit()) {
737            continue; // local
738        }
739        out.push(pc);
740    }
741    out
742}
743
744// Eight parameters are all load-bearing (pool, field, static value,
745// owner class, output, depth, staticness, interface-init requirement);
746// bundling them into a struct would obscure each call site.
747#[allow(clippy::too_many_arguments)]
748fn emit_field(
749    pool: &DexPool,
750    f: &crate::PoolField,
751    init: Option<&StaticValue>,
752    f_class: &str,
753    out: &mut String,
754    depth: usize,
755    is_static: bool,
756    require_init: bool,
757) {
758    let ind = indent(depth);
759    let mut line = String::new();
760    let a = f.access;
761    if a & ACC_PUBLIC != 0 {
762        line.push_str("public ");
763    } else if a & ACC_PRIVATE != 0 {
764        line.push_str("private ");
765    } else if a & ACC_PROTECTED != 0 {
766        line.push_str("protected ");
767    }
768    if is_static {
769        line.push_str("static ");
770    }
771    if a & ACC_FINAL != 0 {
772        line.push_str("final ");
773    }
774    if a & ACC_SYNTHETIC != 0 {
775        line.push_str("/* synthetic */ ");
776    }
777    if a & ACC_TRANSIENT_HINT != 0 {
778        line.push_str("transient ");
779    }
780    if a & ACC_VOLATILE_HINT != 0 {
781        line.push_str("volatile ");
782    }
783    let ty = type_name(pool, &desc_type(&f.desc));
784    line.push_str(&ty);
785    line.push(' ');
786    let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
787    line.push_str(&java_ident(fname));
788    let mut rendered = None;
789    if let Some(v) = init {
790        rendered = render_static_value(pool, v, f_class);
791    }
792    if rendered.is_none() && require_init {
793        // Interface fields MUST have an initializer in Java; the dex may
794        // not carry a static_values entry for a compile-time-constant the
795        // compiler folded away. Keep it compilable.
796        let default = match desc_type(&f.desc) {
797            JavaType::Boolean => "false",
798            JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
799            JavaType::Long => "0L",
800            JavaType::Float => "0.0F",
801            JavaType::Double => "0.0",
802            _ => "null",
803        };
804        rendered = Some(default.to_string());
805    }
806    if let Some(text) = rendered {
807        // A long field holding an `Int(i64)` static value needs the `L`
808        // suffix: without it the literal is an int and overflows
809        // (`long d = -6343169151696340687` failed javac).
810        let text = if matches!(desc_type(&f.desc), JavaType::Long)
811            && text.chars().all(|c| c.is_ascii_digit() || c == '-')
812        {
813            format!("{text}L")
814        } else {
815            text
816        };
817        line.push_str(" = ");
818        line.push_str(&text);
819    }
820    line.push(';');
821    out.push_str(&ind);
822    out.push_str(&line);
823    out.push('\n');
824}
825
826// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
827// volatile(0x40)/transient(0x80) for fields.
828pub const ACC_VOLATILE_HINT: u32 = 0x40;
829pub const ACC_TRANSIENT_HINT: u32 = 0x80;
830
831fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
832    Some(match v {
833        StaticValue::Int(i) => i.to_string(),
834        StaticValue::Float(f) => format_float(*f as f64, true),
835        StaticValue::Double(d) => format_float(*d, false),
836        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
837        StaticValue::Type(t) => format!("{}.class", dotted(t)),
838        StaticValue::Boolean(b) => b.to_string(),
839        StaticValue::Null => "null".into(),
840        StaticValue::Field(cls, name) => {
841            let n = java_ident(name);
842            if cls == owner {
843                n.into_owned()
844            } else {
845                format!("{}.{}", print_class_name(pool, cls), n)
846            }
847        }
848        StaticValue::Other => return None,
849    })
850}
851
852/// Render one method straight into the class buffer `out`. Returns
853/// whether anything was written (skips: no descriptor, deferred to a
854/// monitored thread). The old shape returned a per-method String that
855/// the caller copied in — one extra full copy of every method body.
856fn emit_method(
857    pool: &DexPool,
858    class: &PoolClass,
859    ctx: &DexCtx<'_>,
860    m: &PoolMethod,
861    depth: usize,
862    out: &mut String,
863) -> anyhow::Result<bool> {
864    let ind = indent(depth);
865    let desc = m.parsed_desc();
866
867    // Signature.
868    let mut sig = String::with_capacity(192);
869    let a = m.access;
870    if a & ACC_PUBLIC != 0 {
871        sig.push_str("public ");
872    } else if a & ACC_PRIVATE != 0 {
873        sig.push_str("private ");
874    } else if a & ACC_PROTECTED != 0 {
875        sig.push_str("protected ");
876    }
877    let is_clinit = &*m.name == "<clinit>";
878    let is_init = &*m.name == "<init>";
879    if a & ACC_STATIC != 0 || is_clinit {
880        sig.push_str("static ");
881    }
882    if a & ACC_FINAL != 0 {
883        sig.push_str("final ");
884    }
885    // `abstract synchronized` is an illegal combination — obfuscated
886    // builds mark abstract bridges synchronized (WhatsApp
887    // SQLiteOpenHelper).
888    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
889        sig.push_str("synchronized ");
890    }
891    if a & ACC_NATIVE != 0 {
892        sig.push_str("native ");
893    }
894    if a & ACC_ABSTRACT != 0 {
895        sig.push_str("abstract ");
896    }
897    if a & ACC_SYNTHETIC != 0 {
898        sig.push_str("/* synthetic */ ");
899    }
900
901    // Body (needed for parameter names even for abstract methods).
902    let body = decompile_method(pool, class, m).ok().flatten();
903    let param_names: Vec<String> = body
904        .as_ref()
905        .map(|b| {
906            let mut ps: Vec<(u16, String)> =
907                b.vt.vars
908                    .iter()
909                    .filter(|v| v.is_param && v.name != "this")
910                    .map(|v| (v.slot, v.name.clone()))
911                    .collect();
912            ps.sort_by_key(|(s, _)| *s);
913            ps.into_iter().map(|(_, n)| n).collect()
914        })
915        .unwrap_or_else(|| {
916            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
917                .map(|i| format!("p{}", i + 1))
918                .collect()
919        });
920
921    if is_clinit {
922        // `static { ... }` — the caller strips the method name/params.
923    } else {
924        let Some(d) = &desc else { return Ok(false) };
925        if is_init {
926            // The ctor name must equal the DECLARED class name of its
927            // file: flat `$` at depth 0 (own file), own segment when
928            // inlined in the parent at depth > 0. Case-renamed classes
929            // use their display name.
930            let dname = crate::apply_class_rename(&class.name);
931            let (_, simple) = split_name(&dname);
932            // emit_method's depth is the METHOD indent = class depth + 1:
933            // own-file classes (depth 0 header → method depth 1) need the
934            // flat `$` ctor name; inline nested members use their segment.
935            let base = if depth <= 1 {
936                simple.to_string()
937            } else {
938                // R8 names can END in `$` (`ThreadMsg$$$`): the last
939                // `$`-segment is empty — keep the whole simple name.
940                let seg = simple.rsplit('$').next().unwrap_or(&simple);
941                if seg.is_empty() {
942                    simple.to_string()
943                } else {
944                    seg.to_string()
945                }
946            };
947            let name = if is_restricted_type_name(&base) {
948                format!("_{base}")
949            } else {
950                base
951            };
952            sig.push_str(&java_ident(&name));
953        } else {
954            sig.push_str(&type_name(pool, &d.ret));
955            sig.push(' ');
956            let mname =
957                jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
958            sig.push_str(&java_ident(mname));
959        }
960        sig.push('(');
961        let n = d.args.len();
962        let varargs = a & ACC_VARARGS != 0 && n > 0;
963        for (i, arg) in d.args.iter().enumerate() {
964            if i > 0 {
965                sig.push_str(", ");
966            }
967            let name = param_names
968                .get(i)
969                .cloned()
970                .unwrap_or_else(|| format!("p{}", i));
971            if varargs && i + 1 == n {
972                if let JavaType::Array(inner) = arg {
973                    sig.push_str(&type_name(pool, inner));
974                    sig.push_str("...");
975                } else {
976                    sig.push_str(&type_name(pool, arg));
977                }
978            } else {
979                sig.push_str(&type_name(pool, arg));
980            }
981            sig.push(' ');
982            // Param names come from dex debug info — obfuscated apps
983            // name them `_` (reserved since Java 9) or after keywords.
984            sig.push_str(&java_ident(&name));
985        }
986        sig.push(')');
987    }
988
989    if is_clinit {
990        let Some(b) = body else { return Ok(false) };
991        // Direct render: the printer starts at the method's ABSOLUTE
992        // indent and appends into the same buffer that carries the
993        // header — no intermediate body string, no per-line re-indent
994        // pass (two full copies of every method body saved).
995        out.push_str(&ind);
996        out.push_str("static {\n");
997        let hdr = out.len();
998        let printer = Printer::new(ctx, &b.vt)
999            .with_indent(depth + 1)
1000            .with_output(std::mem::take(out));
1001        let t_print = std::time::Instant::now();
1002        let mut rendered = printer.into_string(&b.body);
1003        crate::method::phase_hit(3, t_print);
1004        if rendered[hdr..].trim().is_empty() {
1005            rendered.truncate(hdr);
1006        }
1007        rendered.push_str(&ind);
1008        rendered.push_str("}\n");
1009        *out = rendered;
1010        return Ok(true);
1011    }
1012    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
1013        out.push_str(&ind);
1014        out.push_str(&sig);
1015        out.push_str(";\n");
1016        return Ok(true);
1017    }
1018    let Some(b) = body else { return Ok(false) };
1019
1020    // Direct render at the absolute indent level (see the clinit path).
1021    let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
1022    match &b.desc.ret {
1023        JavaType::Boolean => {
1024            printer = printer.with_ret_bool(true);
1025        }
1026        JavaType::Char => {
1027            printer = printer.with_ret_char(true);
1028        }
1029        JavaType::Byte => {
1030            printer = printer.with_ret_narrow(true, false);
1031        }
1032        JavaType::Short => {
1033            printer = printer.with_ret_narrow(false, true);
1034        }
1035        _ => {}
1036    }
1037    out.push_str(&ind);
1038    out.push_str(&sig);
1039    out.push_str(" {\n");
1040    let hdr = out.len();
1041    let printer = printer.with_output(std::mem::take(out));
1042    let t_print = std::time::Instant::now();
1043    let mut rendered = printer.into_string(&b.body);
1044    crate::method::phase_hit(3, t_print);
1045    if rendered[hdr..].trim().is_empty() {
1046        rendered.truncate(hdr);
1047    }
1048    rendered.push_str(&ind);
1049    rendered.push_str("}\n");
1050    *out = rendered;
1051    Ok(true)
1052}
1053
1054// ---------------------------------------------------------------------------
1055// Naming helpers
1056// ---------------------------------------------------------------------------
1057
1058fn indent(depth: usize) -> String {
1059    "    ".repeat(depth)
1060}
1061
1062/// `$`-separated nesting rendered with dots — but ONLY when every
1063/// segment is a clean Java identifier (a genuine member class chain).
1064/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
1065/// `...$$inlined$collect$1`) and local-class tails are NOT member
1066/// classes Java can name; the whole name stays flat with `$` (ddc emits
1067/// them as their own top-level files).
1068/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
1069/// any other non-identifier character) is not legal Java. Deterministic
1070/// mapping, applied identically at declaration and call sites.
1071pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
1072    // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
1073    // (millions per APK), where it profiled as __NSGetEnviron.
1074    static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1075    if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
1076        eprintln!("[ident] {name:?}");
1077    }
1078    let clean = name
1079        .chars()
1080        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
1081    // Kotlin names fields/methods `default` (Companion.default) — no Java
1082    // program can declare or reference a keyword; the identical mapping
1083    // lives in jdc-core's call-site sanitizer.
1084    let keyword = matches!(
1085        name,
1086        "abstract"
1087            | "assert"
1088            | "boolean"
1089            | "break"
1090            | "byte"
1091            | "case"
1092            | "catch"
1093            | "char"
1094            | "class"
1095            | "const"
1096            | "continue"
1097            | "default"
1098            | "do"
1099            | "double"
1100            | "else"
1101            | "enum"
1102            | "extends"
1103            | "final"
1104            | "finally"
1105            | "float"
1106            | "for"
1107            | "goto"
1108            | "if"
1109            | "implements"
1110            | "import"
1111            | "instanceof"
1112            | "int"
1113            | "interface"
1114            | "long"
1115            | "native"
1116            | "new"
1117            | "package"
1118            | "private"
1119            | "protected"
1120            | "public"
1121            | "return"
1122            | "short"
1123            | "static"
1124            | "strictfp"
1125            | "super"
1126            | "switch"
1127            | "synchronized"
1128            | "this"
1129            | "throw"
1130            | "throws"
1131            | "transient"
1132            | "try"
1133            | "void"
1134            | "volatile"
1135            | "while"
1136            | "true"
1137            | "false"
1138            | "null"
1139            // `_` is a reserved identifier since Java 9 (Alipay's
1140            // instant-run fields are named `_`) — the `_<name>` mapping
1141            // turns it into `__`, matching jdc-core's call sites.
1142            | "_"
1143    );
1144    // A simple name may not START with a digit either (WhatsApp nests
1145    // `X/0Xx`): the declaration site and every reference (jdc-core's
1146    // sanitize_source_name) prefix the same underscore.
1147    let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
1148    if clean && !keyword && !digit_start {
1149        std::borrow::Cow::Borrowed(name)
1150    } else if keyword || digit_start {
1151        std::borrow::Cow::Owned(format!("_{name}"))
1152    } else {
1153        // Non-ASCII single chars (Alipay names a field `支`) map to a
1154        // lone `_` — itself reserved since Java 9. Escape it.
1155        let mapped: String = name
1156            .chars()
1157            .map(|c| {
1158                if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1159                    c
1160                } else {
1161                    '_'
1162                }
1163            })
1164            .collect();
1165        if mapped == "_" {
1166            std::borrow::Cow::Owned("__".to_string())
1167        } else {
1168            std::borrow::Cow::Owned(mapped)
1169        }
1170    }
1171}
1172
1173fn split_name(internal: &str) -> (String, String) {
1174    match internal.rfind('/') {
1175        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
1176        None => (String::new(), internal.to_string()),
1177    }
1178}
1179
1180/// Dotted source form of an internal name.
1181pub fn dotted(internal: &str) -> String {
1182    let mut out = internal.replace('/', ".");
1183    // `$` → `.` only when the following segment can start a Java
1184    // identifier (anonymous/synthetic tails stay `$`).
1185    let mut i = 0;
1186    while let Some(p) = out[i..].find('$') {
1187        let at = i + p;
1188        // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
1189        // source name — dotting it produced a leading `.Gson.Types`.
1190        let head_ok = at > 0
1191            && out[..at]
1192                .chars()
1193                .next_back()
1194                .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
1195        let tail_ok = out[at + 1..]
1196            .chars()
1197            .next()
1198            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1199        if tail_ok && head_ok {
1200            out.replace_range(at..at + 1, ".");
1201            i = at + 1;
1202        } else {
1203            i = at + 1;
1204        }
1205    }
1206    // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
1207    // reached `extends` with the digit start unmapped.
1208    sanitize_fq(&out)
1209}
1210
1211fn join_dotted(pool: &DexPool, names: &[String]) -> String {
1212    names
1213        .iter()
1214        .map(|n| print_class_name(pool, n))
1215        .collect::<Vec<_>>()
1216        .join(", ")
1217}
1218
1219/// A printable type name (arrays render with `[]` suffixes). Nested class
1220/// names dot their `$` when the outer chain is known to the pool.
1221pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
1222    match t {
1223        JavaType::Void => "void".into(),
1224        JavaType::Object(n) => print_class_name(pool, n),
1225        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
1226        other => other.to_java(false),
1227    }
1228}
1229
1230/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
1231/// its left side names a known class (literal `$` top-level names survive).
1232pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
1233    let cow = crate::apply_class_rename(internal);
1234    let internal: &str = &cow;
1235    let mut out = String::new();
1236    // `known` must test the ACCUMULATED internal prefix, not the bare
1237    // inter-`$` segment: the per-segment shape checked `pool.get("a")`
1238    // for the second level of `s5/o$a$b`, missed, and rendered the
1239    // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
1240    // nested type.
1241    let mut off = 0usize;
1242    loop {
1243        let rest = &internal[off..];
1244        match rest.find('$') {
1245            Some(i) => {
1246                let seg = &rest[..i];
1247                let prefix = &internal[..off + i];
1248                let known = pool.get(prefix).is_some()
1249                    || jdc_core::rename::is_renamed_display(prefix)
1250                    || !pool.has_dollar_prefix(prefix);
1251                // The `$` may only become a nesting dot when the tail
1252                // segment STARTS a Java identifier: R8's desugared-
1253                // library names carry `$` inside PACKAGE paths
1254                // (`j$/util/...` dotted into `j..util`) and suffixes
1255                // like `Collection$-EL` or anonymous `RequestId$1`
1256                // cannot be dotted under any reading.
1257                let tail_ok = rest[i + 1..]
1258                    .chars()
1259                    .next()
1260                    .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
1261                out.push_str(&seg.replace('/', "."));
1262                out.push_str(if known && tail_ok { "." } else { "$" });
1263                off += i + 1;
1264            }
1265            None => {
1266                out.push_str(&rest.replace('/', "."));
1267                return sanitize_ref(&out);
1268            }
1269        }
1270    }
1271}
1272
1273/// Class-file names may contain characters Java source identifiers
1274/// cannot (`Collection$-EL`); the deterministic mapping matches the
1275/// declaration sites (java_ident).
1276/// Every `.`-segment of a fully-qualified name must start a Java
1277/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
1278pub(crate) fn sanitize_fq(dotted: &str) -> String {
1279    dotted
1280        .split('.')
1281        .map(|seg| {
1282            if is_java_keyword_name(seg)
1283                || is_restricted_type_name(seg)
1284                || seg.chars().next().is_some_and(|c| c.is_ascii_digit())
1285            {
1286                format!("_{seg}")
1287            } else if seg
1288                .chars()
1289                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
1290            {
1291                seg.to_string()
1292            } else {
1293                // Non-ASCII single chars (rimet nests classes named `ˆ`
1294                // / `ァ`) map to a lone `_` — reserved since Java 9.
1295                let mapped: String = seg
1296                    .chars()
1297                    .map(|c| {
1298                        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
1299                            c
1300                        } else {
1301                            '_'
1302                        }
1303                    })
1304                    .collect();
1305                if mapped == "_" {
1306                    "__".to_string()
1307                } else {
1308                    mapped
1309                }
1310            }
1311        })
1312        .collect::<Vec<_>>()
1313        .join(".")
1314}
1315
1316/// Restricted contextual TYPE names — legal as member/local names
1317/// (rt.jar compiles `var` locals), illegal in class declarations and
1318/// type references. Consulted only on CLASS-name paths.
1319pub(crate) fn is_restricted_type_name(s: &str) -> bool {
1320    matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
1321}
1322
1323fn is_java_keyword_name(s: &str) -> bool {
1324    matches!(
1325        s,
1326        "abstract"
1327            | "assert"
1328            | "boolean"
1329            | "break"
1330            | "byte"
1331            | "case"
1332            | "catch"
1333            | "char"
1334            | "class"
1335            | "const"
1336            | "continue"
1337            | "default"
1338            | "do"
1339            | "double"
1340            | "else"
1341            | "enum"
1342            | "extends"
1343            | "final"
1344            | "finally"
1345            | "float"
1346            | "for"
1347            | "goto"
1348            | "if"
1349            | "implements"
1350            | "import"
1351            | "instanceof"
1352            | "int"
1353            | "interface"
1354            | "long"
1355            | "native"
1356            | "new"
1357            | "package"
1358            | "private"
1359            | "protected"
1360            | "public"
1361            | "return"
1362            | "short"
1363            | "static"
1364            | "strictfp"
1365            | "super"
1366            | "switch"
1367            | "synchronized"
1368            | "this"
1369            | "throw"
1370            | "throws"
1371            | "transient"
1372            | "try"
1373            | "void"
1374            | "volatile"
1375            | "while"
1376            | "true"
1377            | "false"
1378            | "null"
1379            // `_` is a reserved identifier since Java 9 (Alipay's
1380            // instant-run fields are named `_`) — the `_<name>` mapping
1381            // turns it into `__`, matching jdc-core's call sites.
1382            | "_"
1383    )
1384}
1385
1386fn sanitize_ref(name: &str) -> String {
1387    sanitize_fq(name)
1388}
1389
1390/// Dotted source form of an internal name.
1391pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
1392    print_class_name(pool, internal)
1393}
1394
1395/// Unused import silencer.
1396#[allow(dead_code)]
1397fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
1398    let _ = java_type_to_generic;
1399}