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, Copy, PartialEq, Eq)]
16pub enum CureMode {
17    /// Full cure: parse → fixed-point simplify → print. The default —
18    /// semantic-preserving normalization of decompiler residue
19    /// (copy inlining, ternaries, for-each, dead stores…), ~90% of
20    /// the cure stage's CPU.
21    Full,
22    /// `--fast`: parse → print. Formatting normalization only (the
23    /// simplify engine is skipped — the stage drops from ~90% of the
24    /// cure CPU to ~10%): output keeps register noise but in canonical
25    /// layout, at roughly half the default's wall time on large APKs.
26    Fast,
27}
28
29#[derive(Debug, Clone)]
30pub struct ClassOptions {
31    /// Prefix each file with a provenance comment.
32    pub provenance: bool,
33    /// The cure source-level post-pass mode. Default Full.
34    pub cure: CureMode,
35}
36
37impl Default for ClassOptions {
38    fn default() -> Self {
39        ClassOptions {
40            provenance: true,
41            cure: CureMode::Full,
42        }
43    }
44}
45
46/// Decompile one class (plus nested member classes) to a Java source file.
47///
48/// Classes with large method bodies run in a monitored thread with a
49/// deadline: pathological CFGs can drive the shared structurer's walk into
50/// an exponential exploration that never returns. On timeout the class is
51/// reported failed and the thread is abandoned (reaped at process exit).
52/// One registered monitored decompile: the receiver the worker polls at
53/// the tail, the class name (for diagnostics), and the deadline counted
54/// from the spawn.
55pub type PendingMonitor = (
56    std::sync::mpsc::Receiver<Result<String, String>>,
57    String,
58    std::time::Instant,
59);
60
61pub fn decompile_class(
62    pool: &std::sync::Arc<DexPool>,
63    class: &PoolClass,
64    opts: &ClassOptions,
65    pending: &std::sync::Mutex<Vec<PendingMonitor>>,
66) -> anyhow::Result<String> {
67    if class_is_risky(pool, class) {
68        // Detached monitored thread: the CALLER registers the receiver and
69        // moves on (awaiting happens at the end of the run) — a spinning
70        // pathological method no longer stalls its worker.
71        let pool2 = pool.clone();
72        let cls = class.clone();
73        let opts2 = opts.clone();
74        let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
75        let name = class.name.clone();
76        // 10s: pure insurance — the structurer's walk budgets and the
77        // ladder giant short-circuit bound pathological methods
78        // deterministically now (weixin cdp/l1: 4.2s single-class), but
79        // full-run worker contention can ~2× that; the old 5s dropped
80        // l1's file entirely (every referrer cannot-finds).
81        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
82        let _ = std::thread::Builder::new()
83            .stack_size(64 * 1024 * 1024)
84            .spawn(move || {
85                let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
86                let _ = tx.send(r);
87            });
88        pending.lock().unwrap().push((rx, name, deadline));
89        return Err(anyhow::anyhow!(
90            "deferred to monitored thread (pathological-CFG guard)"
91        ));
92    }
93    decompile_class_impl(pool, class, opts)
94}
95
96/// Any method in the exponential-walk danger zone → run under the
97/// deadline. Calibrated between real workloads: routine R8 classes top out
98/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
99/// (weibo's gson TypeAdapters) sit at 24k+.
100fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
101    // Header peek only — decoding every body here would double the APK's
102    // total decode work.
103    for m in class.all_methods() {
104        if m.code_off == 0 {
105            continue;
106        }
107        if let Some(dex) = pool.dex(m.dex_idx) {
108            if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
109                if insns > 16_000 {
110                    return true;
111                }
112            }
113        }
114    }
115    false
116}
117
118fn decompile_class_impl(
119    pool: &DexPool,
120    class: &PoolClass,
121    opts: &ClassOptions,
122) -> anyhow::Result<String> {
123    let ctx = DexCtx::new(pool, class);
124    // Size the class buffer up front: corpus-average classes render to
125    // ~1KB per method — without the reserve the String doubles through
126    // 4-6 realloc+copy rounds per class (~2× the final size in memmove).
127    // Cap the reserve: weixin's monster classes (hundreds of methods)
128    // would reserve megabytes of untouched capacity per class — large
129    // blocks take mimalloc's commit/purge path (madvise churn showed up
130    // in profiles). 256KB covers the corpus-average class dozens of
131    // times over; bigger classes pay a few extra doublings.
132    let mut out = String::with_capacity(
133        (class.all_methods().count() * 1024).min(256 * 1024) + 512,
134    );
135    if opts.provenance {
136        static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
137        out.push_str(BANNER.get_or_init(|| {
138            format!(
139                "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
140                env!("CARGO_PKG_VERSION")
141            )
142        }));
143        // Provenance: which input image this class was lifted from (jadx's
144        // `loaded from: classes.dex` pattern). Byte-stable across runs —
145        // deliberately NO timestamp so outputs diff cleanly. Direct
146        // push_str: the per-class format! temporaries were 3 allocations
147        // × every class in the corpus.
148        if let Some(dex) = pool.dex(class.dex_idx) {
149            if let Some(label) = pool.dex_labels.get(class.dex_idx) {
150                out.push_str("// From: ");
151                out.push_str(label);
152                out.push_str(" (DEX ");
153                out.push_str(&dex.version);
154                out.push_str(")\n");
155            }
156        }
157    }
158    if let Some(src) = &class.source_file {
159        out.push_str("// Source file: ");
160        out.push_str(src);
161        out.push('\n');
162    }
163    if class.is_synthetic() {
164        out.push_str("// synthetic\n");
165    }
166    // The DISPLAY package: the root-package relocation rule moves the
167    // default-package cohort under a synthetic package, and the emitted
168    // `package` line + file path must agree with the renamed refs.
169    // Every pre-existing rename rule keeps the package, so this is a
170    // no-op for them.
171    let own_display_full = crate::apply_class_rename(&class.name);
172    let (pkg, _) = split_name(&own_display_full);
173    // Per-package caches below are keyed by the ORIGINAL package — the
174    // relocated cohort's same-package set is still the root one.
175    let pkg_lookup: &str = match crate::root_pkg_display() {
176        Some(rp) if rp == pkg => "",
177        _ => pkg.as_str(),
178    };
179    // Two-pass import assembly: the body renders FIRST (into a temp
180    // buffer) with the obscured render state installed — expression
181    // positions DISCOVER additional obscured refs on the fly — then the
182    // file assembles as provenance + package + imports + body. The one
183    // extra body copy is the price of correct import placement.
184    let shadow = inherited_field_shadows(pool, class);
185    let mut obscured_map = compute_obscured_renders(pool, class, &shadow);
186    // Same-package simple-name collisions: an import shadows every
187    // same-package use of that simple in this file — drop those from
188    // the map (their refs stay qualified, erroring honestly).
189    // Arc BORROW of the package's simple-name set + a small overlay for
190    // this file's own additions ($-tail extras, own field names). The
191    // old unconditional deep copy cost one full set per class —
192    // WhatsApp's ~10k-class package paid it twice (the second in the
193    // obscured-state install), the #2/#3 CPU frames on its profile.
194    static EMPTY_BLOCKED: std::sync::OnceLock<std::sync::Arc<jdc_core::FxHashSet<String>>> =
195        std::sync::OnceLock::new();
196    let blocked_base: std::sync::Arc<jdc_core::FxHashSet<String>> = pool
197        .package_simples()
198        .get(pkg_lookup)
199        .cloned()
200        .unwrap_or_else(|| {
201            std::sync::Arc::clone(EMPTY_BLOCKED.get_or_init(|| {
202                std::sync::Arc::new(jdc_core::FxHashSet::default())
203            }))
204        });
205    // Rare JLS 7.5.1 narrowing: sibling simples the file never
206    // references stop blocking — materialized as a REMOVAL overlay.
207    let mut blocked_removed: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
208    let mut blocked_extra: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
209    // JLS 7.5.1 narrowing: importing simple `n` legally shadows the
210    // same-package sibling throughout THIS file — it only breaks refs
211    // the file itself makes to that sibling. Drop sibling simples the
212    // file never references (exhaustive family scan: metadata + raw
213    // insn refs), unblocking the import route for field-shadowed
214    // package quals (lark pq/e: field `e2` captures `e2.c.b(..)` —
215    // "变量 c" ×47 — while sibling pq.c is never used). The scan runs
216    // ONLY when an obscured candidate is sibling-blocked.
217    // Scan gate: an obscured ref can only exist when some in-scope
218    // field name (or the own simple) equals a PACKAGE first segment —
219    // and the candidates include BODY refs (lark pq/e's `e2.c.b(..)`
220    // is invoke-static-only, invisible to the metadata map), so the
221    // gate must not key off obscured_map.
222    {
223        let own_simple0 = class.name.rsplit(['/', '$']).next().unwrap_or("");
224        let can_obscure = shadow.iter().any(|f| pool.package_simples().contains_key(f))
225            || (!own_simple0.is_empty()
226                && pool.package_simples().contains_key(own_simple0));
227        if can_obscure && !blocked_base.is_empty() {
228            let used = sibling_ref_simples(pool, class);
229            if !used.is_empty() {
230                blocked_removed.extend(
231                    blocked_base
232                        .iter()
233                        .filter(|b| !used.contains(*b))
234                        .map(|b| b.to_string()),
235                );
236            }
237        }
238    }
239    // Blocked names must include the RENAMED displays of same-package
240    // classes too: refs and imports render through the rename registry,
241    // so an import matching a DISPLAY (not the raw simple) shadows that
242    // class's in-file uses exactly the same (weixin n0/y0 displays as
243    // y02 while h72 declares a raw class y0 — the raw-only blocked set
244    // let the y02 import through on one side and killed it on the
245    // other, stranding 622 bare `y02` refs).
246    // Renamed-display blocking: a ZERO-COPY view into the process-wide
247    // per-package cache (built once from the rename registry — iterating
248    // `blocked` through apply_class_rename per FILE was O(files ×
249    // package): WhatsApp's ~10k-class package X cost 1,269 cumulative
250    // worker-seconds, decompile 24s → 134s; per-file clones of the
251    // extension set still cost 184s).
252    static PKG_RENAMED_DISPLAYS: std::sync::OnceLock<
253        jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
254    > = std::sync::OnceLock::new();
255    let blocked_renamed: Option<&'static jdc_core::FxHashSet<String>> = {
256        let cache = PKG_RENAMED_DISPLAYS.get_or_init(|| {
257            let mut m: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
258                jdc_core::FxHashMap::default();
259            if let Some(ren) = jdc_core::rename::with_renames(|r| r.clone()) {
260                for (raw, disp) in ren.iter() {
261                    if raw == disp {
262                        continue;
263                    }
264                    let pkg_key = raw.rfind('/').map(|i| &raw[..i]).unwrap_or("");
265                    let tail = disp
266                        .rsplit(['/', '$'])
267                        .next()
268                        .unwrap_or(disp.as_str())
269                        .to_string();
270                    m.entry(pkg_key.to_string()).or_default().insert(tail);
271                }
272            }
273            m
274        });
275        cache.get(pkg_lookup)
276    };
277    // In-file member shadowing: the own class's FIELDS and its family's
278    // nested-class tails are in scope inside the body and shadow any
279    // single-type import of the same simple (JLS 6.4.1/6.4.2) — an
280    // obscured simple colliding with them cannot be imported.
281    {
282        let own_raw = class.name.rsplit('/').next().unwrap_or("");
283        let prefix = format!("{}$", own_raw);
284        let extras: Vec<String> = blocked_base
285            .iter()
286            .filter(|b| b.starts_with(&prefix))
287            .filter_map(|b| b.rsplit('$').next().map(|t| t.to_string()))
288            .collect();
289        blocked_extra.extend(extras);
290        for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
291            blocked_extra.insert(crate::classdec::java_ident(&f.name).into_owned());
292        }
293    }
294    obscured_map.retain(|_internal, simple| {
295        !blocked_extra.contains(simple)
296            && (blocked_removed.contains(simple)
297                || !blocked_base.contains(simple))
298            && !blocked_renamed.is_some_and(|e| e.contains(simple))
299    });
300    // Display-simple collisions cannot coexist as single-type imports:
301    // importing simple `n` while THIS file declares `n` is a JLS 7.5.1
302    // error, and two imports of the same simple make every use of it
303    // ambiguous (weibo `class a2 extends a2` with `import a.a.b.c.a2`
304    // + `import a.e.a.a.a2` — both shadow-renamed from package-`a`
305    // classes named `a`). Drop every colliding entry (own-display hits
306    // drop ALL claimants; multi-internal simples keep the least
307    // internal for determinism) — dropped refs fall back to the
308    // qualified display render, which resolves against the renamed
309    // output files.
310    let own_display_internal = crate::apply_class_rename(&class.name);
311    let own_disp_simple = own_display_internal
312        .rsplit(['/', '$'])
313        .next()
314        .unwrap_or("")
315        .to_string();
316    {
317        let mut by_simple: std::collections::BTreeMap<&str, Vec<&String>> =
318            std::collections::BTreeMap::new();
319        for (internal, simple) in obscured_map.iter() {
320            by_simple.entry(simple.as_str()).or_default().push(internal);
321        }
322        let mut drop: Vec<String> = Vec::new();
323        for (simple, mut internals) in by_simple {
324            if simple == own_disp_simple.as_str() {
325                drop.extend(internals.into_iter().cloned());
326            } else if internals.len() > 1 {
327                internals.sort();
328                drop.extend(internals.into_iter().skip(1).cloned());
329            }
330        }
331        for d in &drop {
332            obscured_map.remove(d);
333        }
334    }
335    // Own field names in DISPLAY space (the registry may have renamed
336    // them — the body renders display names, the capture checks must
337    // compare against the same space): value-namespace capture guards
338    // for the static-import escape.
339    let own_field_names: jdc_core::FxHashSet<String> = class
340        .static_fields
341        .iter()
342        .chain(class.instance_fields.iter())
343        .map(|f| {
344            jdc_core::rename::field_display(&class.name, &f.name, &f.desc)
345                .map(|d| d.to_string())
346                .unwrap_or_else(|| java_ident(&f.name).into_owned())
347        })
348        .collect();
349    set_obscured_state(
350        class.name.clone(),
351        obscured_map.clone(),
352        std::sync::Arc::clone(&blocked_base),
353        blocked_removed.clone(),
354        blocked_extra.clone(),
355        blocked_renamed,
356        shadow,
357        own_field_names,
358    );
359    let mut body_buf = String::with_capacity(out.capacity() / 2);
360    let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
361    let recorded = take_recorded_and_clear();
362    let recorded_static = take_recorded_static_and_clear();
363    body_res?;
364    if !pkg.is_empty() {
365        out.push('\n');
366        out.push_str("package ");
367        let dotted = pkg.replace('/', ".");
368        out.push_str(&sanitize_fq(&dotted));
369        out.push_str(";\n");
370        // Imports: the metadata map plus the expression-level
371        // discoveries, pool-known only (an import of an unknown class
372        // is a hard error), display-renamed, sorted for determinism.
373        let mut import_set: jdc_core::FxHashSet<String> =
374            obscured_map.keys().cloned().collect();
375        for r in recorded {
376            if pool.get(&r).is_some() {
377                // The import renders under the RENAMED display — the
378                // blocked check must use the same name the refs render.
379                // Framework-shadow stubs render (and import) their RAW
380                // framework FQN — the reference means the platform class.
381                let display = if crate::is_fw_shadow(&r) {
382                    std::borrow::Cow::Borrowed(r.as_str())
383                } else {
384                    crate::apply_class_rename(&r)
385                };
386                let simple = display.rsplit(['/', '$']).next().unwrap_or("");
387                if !simple.is_empty()
388                    && !blocked_extra.contains(simple)
389                    && (blocked_removed.contains(simple)
390                        || !blocked_base.contains(simple))
391                    && !blocked_renamed.is_some_and(|e| e.contains(simple))
392                {
393                    import_set.insert(r);
394                }
395            }
396        }
397        let mut imports: Vec<String> = import_set
398            .iter()
399            .map(|internal| {
400                let renamed = if crate::is_fw_shadow(internal) {
401                    std::borrow::Cow::Borrowed(internal.as_str())
402                } else {
403                    crate::apply_class_rename(internal)
404                };
405                crate::classdec::dotted(&renamed)
406            })
407            .collect();
408        imports.sort();
409        // Body-discovered (recorded) refs joined after the pre-emission
410        // dedupe — re-check at render: one import per display simple,
411        // none against this file's own display (JLS 7.5.1). A dropped
412        // recorded ref's body render is ambiguous regardless; skipping
413        // its import at least avoids the duplicate-import error.
414        let own_display_dotted = crate::classdec::dotted(&own_display_internal);
415        let own_dot_simple = own_display_dotted.rsplit('.').next().unwrap_or("");
416        let mut seen_simples: jdc_core::FxHashSet<String> =
417            jdc_core::FxHashSet::default();
418        for display in imports {
419            let simple = display.rsplit('.').next().unwrap_or("");
420            if simple == own_dot_simple || !seen_simples.insert(simple.to_string()) {
421                continue;
422            }
423            out.push_str(&format!("import {};\n", display));
424        }
425        // 单静态导入(遮蔽 owner 的成员逃逸):裸成员名已过全部捕获/
426        // 冲突门(逃逸函数内),此处按 owner 排序去重发射,确定性。
427        if !recorded_static.is_empty() {
428            let mut statics: Vec<(String, String)> = recorded_static
429                .into_iter()
430                .map(|(member, owner)| (owner, member))
431                .collect();
432            statics.sort();
433            for (owner, member) in statics {
434                out.push_str(&format!("import static {}.{};\n", owner, member));
435            }
436        }
437    }
438    out.push('\n');
439    out.push_str(&body_buf);
440    // cure 源码级后处理:IR pass 之后的源码层净化(拷贝内联/三元化/
441    // for-each 还原…)。$DDC 标记文件与解析失败原样返回(见 cure_pass)。
442    out = crate::cure_pass::run(out, opts.cure);
443    Ok(out)
444}
445
446/// Render the class header, fields, methods and nested member classes into
447/// `out` (used at top level and recursively for nested members).
448// `opts` is only consumed by the nested-member recursion below — that
449// is its purpose (propagating emission options into inline children).
450/// One enum constant: the source identifier plus constructor
451/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
452use jdc_core::ir::expr::{ConstVal, Expr};
453use jdc_core::ir::stmt::Stmt;
454struct EnumConst {
455    #[allow(dead_code)]
456    field: String,
457    name: String,
458    extra_args: Vec<Expr>,
459}
460
461/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
462/// static field must be initialized in `<clinit>` by
463/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
464/// always emit. Returns None (caller falls back to the desugared
465/// `/* enum */ class` form) when anything is missing: R8 variance,
466/// constant-specific bodies (the field holds an anonymous subclass), or
467/// a <clinit> that failed to decompile.
468/// Cached DDC_DBG_ENUM gate (env reads must not sit in per-class paths).
469fn dbg_enum() -> bool {
470    static CACHED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
471    *CACHED.get_or_init(|| std::env::var_os("DDC_DBG_ENUM").is_some())
472}
473
474fn dbg_enum_none(ln: u32) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
475    if dbg_enum() {
476        eprintln!("[enum-none] line {}", ln);
477    }
478    None
479}
480
481fn collect_enum_constants(
482    pool: &DexPool,
483    class: &PoolClass,
484    ctx: &DexCtx<'_>,
485) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
486    let _ = ctx;
487    let const_fields: Vec<&PoolField> = class
488        .static_fields
489        .iter()
490        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
491        .collect();
492    // No ACC_ENUM constant fields: R8 drops them ALL when nothing
493    // outside reads them (weixin sp2/ji — only the synthetic $VALUES
494    // array survives). An ACC_ENUM CLASS still promotes: the synthetic-
495    // constant scan below lifts the constants out of the array literal.
496    // Non-enum classes have no business here.
497    if const_fields.is_empty() && class.access & crate::access::ACC_ENUM == 0 {
498        return dbg_enum_none(line!());
499    }
500    // JLS 8.9.2: a restored true-enum ctor may not read the enum's own
501    // static fields (javac rejects even the QUALIFIED `E.field` form).
502    // Meituan Robust injects a hotpatch guard reading
503    // `changeQuickRedirect` into every instrumented ctor — legal in
504    // bytecode (injected after javac), and strip_robust_enum_ctor_guard
505    // removes it from the restored source. A self-static sget of any
506    // OTHER type makes restoration inexpressible → stay on the fallback.
507    for m in class.all_methods() {
508        if &*m.name != "<init>" {
509            continue;
510        }
511        let dex = pool.dex(m.dex_idx)?;
512        let ci = dex.code_at(m.code_off)?;
513        for ins in &ci.insns {
514            if let ddc_dex::insn::InsnKind::SGet { field_idx, .. } = ins.kind {
515                let fr = dex.field(field_idx);
516                if dex.class_name(fr.class_idx) == class.name.as_str()
517                    && dex.type_name(fr.type_idx) != "Lcom/meituan/robust/ChangeQuickRedirect;"
518                {
519                    return dbg_enum_none(line!());
520                }
521            }
522        }
523    }
524    let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
525    let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
526    // The structurer can leave the constant-build sequence inside a
527    // nested Stmt::Block (jd1.u: stmt4 wraps all 26 ctors beside a
528    // trailing loop) — the passes below index a FLAT statement list.
529    if let Stmt::Block(vs) = &mut body.body {
530        crate::passes::flatten_top_blocks(vs);
531    }
532    if dbg_enum() {
533        if let Stmt::Block(vs) = &body.body {
534            for (i, st) in vs.iter().enumerate() {
535                let d = format!("{:?}", st);
536                eprintln!("[enum-stmt {}] {}", i, d.chars().take(220).collect::<String>());
537            }
538        }
539    }
540    crate::passes::strip_clinit_hotfix_guard(&mut body.body);
541
542    // Meituan Robust hotpatch guard (weibo's lifecycle enums — a big
543    // slice of the 1,137 fallback population): the ENTIRE constant-build
544    // sequence lives in one branch of a top-level
545    // `if (PatchProxy.isSupportClinit(..)) { .. } else { .. }`, so the
546    // flat scans below see nothing and the enum falls back (its dex
547    // `Enum.valueOf(X.class,..)` then fails — X is no Enum subtype,
548    // weibo ×853). Hoist the constant-carrying branch to the top level,
549    // but only when the top level itself has no constant defs (an
550    // ordinary clinit must not be touched, and a mixed shape is beyond
551    // the flat-scan contract). Restoration semantics already normalize
552    // the clinit (constants move to the header, inits drop); the
553    // dispatch shim is inexpressible in a true enum regardless.
554    if let Stmt::Block(vs) = &mut body.body {
555        let self_cls: &str = class.name.as_str();
556        let top_def = |st: &Stmt| -> bool {
557            let v = match st {
558                Stmt::LocalDef { init: Some(e), .. } => e,
559                Stmt::ExprStmt(Expr::Assign { value, .. }) => value,
560                _ => return false,
561            };
562            matches!(v, Expr::New { cls, args, .. }
563                if cls.as_ref() == self_cls && args.len() >= 2)
564        };
565        if !vs.iter().any(top_def) {
566            let branch_news = |st: &Stmt| -> usize {
567                let mut n = 0usize;
568                crate::passes::visit_all_exprs(st, &mut |x| {
569                    if let Expr::New { cls, args, .. } = x {
570                        if cls.as_ref() == self_cls && args.len() >= 2 {
571                            n += 1;
572                        }
573                    }
574                });
575                n
576            };
577            let mut pick: Option<(usize, bool)> = None;
578            for (i, st) in vs.iter().enumerate() {
579                if let Stmt::If { then_stmt, else_stmt, .. } = st {
580                    let t = branch_news(then_stmt) > 0;
581                    let e = else_stmt
582                        .as_ref()
583                        .map(|s2| branch_news(s2) > 0)
584                        .unwrap_or(false);
585                    if t != e {
586                        pick = Some((i, t));
587                        break;
588                    }
589                }
590            }
591            if let Some((i, then_is_const)) = pick {
592                let st = std::mem::replace(&mut vs[i], Stmt::Block(Vec::new()));
593                if let Stmt::If { then_stmt, else_stmt, .. } = st {
594                    let keep = if then_is_const {
595                        Some(then_stmt)
596                    } else {
597                        else_stmt
598                    };
599                    if let Some(bx) = keep {
600                        let inner = match *bx {
601                            Stmt::Block(v) => v,
602                            other => vec![other],
603                        };
604                        vs.splice(i..=i, inner);
605                        // The branch can be double-wrapped (Block[Block[..]]);
606                        // the passes below index a FLAT list.
607                        crate::passes::flatten_top_blocks(vs);
608                    }
609                }
610            }
611        }
612    }
613
614    // R8/d8 split the constant build across an intermediate local:
615    //   Self v0 = new Self("NAME", i, ...);
616    //   a = v0;                       // sput to the ACC_ENUM field
617    //   Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
618    // Pass 1 registers each intermediate (or direct) new; pass 2 binds
619    // them to the ACC_ENUM fields; references to the locals are then
620    // rewritten into constant identifiers so the $VALUES build keeps
621    // compiling once the definitions drop.
622    use std::collections::HashMap;
623    let mut const_name: Vec<String> = Vec::new();
624    let mut const_field: Vec<String> = Vec::new();
625    let mut const_extra: Vec<Vec<Expr>> = Vec::new();
626    // Declared ordinal per constant (the ctor's int arg) — the merge at
627    // the end orders field-bound and synthetic constants by it and
628    // demands a dense 0..n run (Java derives ordinals from declaration
629    // position).
630    let mut const_ord: Vec<i64> = Vec::new();
631    let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
632    let mut drop_stmts: Vec<usize> = Vec::new();
633
634    // Collect (immutable borrows) first; the mutable passes come after.
635    if !matches!(&body.body, Stmt::Block(_)) { return dbg_enum_none(line!()); }
636
637    // Pass 1: definitions. (immutable borrow; rewrite comes later)
638    // Rolling reaching-defs of clinit locals for resolving enum-ctor
639    // extra args (resolve_enum_arg); `tainted` flips at the first
640    // non-straight-line statement — past it, linear-scan defs are no
641    // longer provable and Local args reject the enum mode.
642    let mut defs: HashMap<u32, &Expr> = HashMap::default();
643    let mut tainted = false;
644    let self_name: std::sync::Arc<str> = class.name.as_str().into();
645    let self_ty =
646        jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
647    // A REUSED intermediate local: `v = new Self(..); A = v; v = new
648    // Self(..); B = v;` — the first def is a LocalDef, the rest are
649    // Assigns to the same var. Register every def site; pass 2 resolves
650    // a local sput to the def site CURRENT at that statement (rolling),
651    // not to the var (Kotlin EnumEntries enums, RegexOption: 8→7 count
652    // mismatch used to abort the whole promotion).
653    let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
654    for (i, st) in match &body.body {
655        Stmt::Block(v) => v.iter().enumerate(),
656        _ => return dbg_enum_none(line!()),
657    } {
658        // (var, New) for both def shapes: LocalDef and Assign-to-local.
659        let def_shape: Option<(u32, &Expr)> = match st {
660            Stmt::LocalDef {
661                var,
662                init: Some(e),
663                ..
664            } => Some((*var, e)),
665            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
666                match (&**target, &**value) {
667                    (Expr::Local { var, .. }, e) => Some((*var, e)),
668                    _ => None,
669                }
670            }
671            _ => None,
672        };
673        if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
674            if ncls.as_ref() == class.name && args.len() >= 2 {
675                if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
676                    (&args[0], &args[1])
677                {
678                    if java_ident(n).as_ref() != &**n || n.is_empty() { return dbg_enum_none(line!()); }
679                    var_of.insert(var, const_name.len());
680                    def_site.insert(i, (var, const_name.len()));
681                    const_ord.push(*ord0 as i64);
682                    const_name.push(n.to_string());
683                    const_field.push(String::new());
684                    const_extra.push(resolve_enum_extras(
685                        &args[2..],
686                        &defs,
687                        tainted,
688                        &var_of,
689                        &const_name,
690                        &self_name,
691                        &self_ty,
692                    )?);
693                    drop_stmts.push(i);
694                }
695            }
696        }
697        track_def(st, &mut defs, &mut tainted);
698    }
699
700    // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
701    // Local sputs resolve through the ROLLING def map (updated at each
702    // def-site statement) — a reused `v` must bind to the constant
703    // defined most recently, not to the var's first registration.
704    let mut defs2: HashMap<u32, &Expr> = HashMap::default();
705    let mut tainted2 = false;
706    let mut cur_def: HashMap<u32, usize> = HashMap::default();
707    for (i, st) in match &body.body {
708        Stmt::Block(v) => v.iter().enumerate(),
709        _ => return dbg_enum_none(line!()),
710    } {
711        if let Some((var, idx)) = def_site.get(&i) {
712            cur_def.insert(*var, *idx);
713        }
714        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
715            if let Expr::Field {
716                cls,
717                name: fname,
718                is_static: true,
719                ..
720            } = &**target
721            {
722                if cls.as_ref() != class.name {
723                    continue;
724                }
725                // The lifter emits field refs THROUGH field_display — an
726                // obscuring-renamed const field (te/b's field `b` shares
727                // the class name → displays `b7`) arrives here already
728                // renamed; the raw-name compare silently skipped the sput
729                // and the enum fell back to `/* enum */ class` with the
730                // synthetic ctor stripped — every `new b("STARS", 0)`
731                // then failed 「无法将构造器应用到给定类型」 (Telegram 603
732                // errors / 430 files). Match on the display name; the
733                // promoted constant declares under it, which is exactly
734                // what every pool reference resolves through.
735                let bound = const_fields.iter().any(|f| {
736                    f.name.as_ref() == &**fname
737                        || jdc_core::rename::field_display(
738                            class.name.as_str(),
739                            &f.name,
740                            &f.desc,
741                        )
742                        .is_some_and(|d| d == &**fname)
743                });
744                if !bound {
745                    continue;
746                }
747                if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { return dbg_enum_none(line!()); // duplicate assignment
748                }
749                let idx = match &**value {
750                    Expr::Local { var, .. } => cur_def.get(var).copied()?,
751                    Expr::New { cls: ncls, args, .. }
752                        if ncls.as_ref() == class.name && args.len() >= 2 =>
753                    {
754                        if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
755                            (&args[0], &args[1])
756                        {
757                            if java_ident(n).as_ref() != &**n || n.is_empty() { return dbg_enum_none(line!()); }
758                            let idx = const_name.len();
759                            const_ord.push(*ord0 as i64);
760                            const_name.push(n.to_string());
761                            const_field.push(String::new());
762                            const_extra.push(resolve_enum_extras(
763                                &args[2..],
764                                &defs2,
765                                tainted2,
766                                &var_of,
767                                &const_name,
768                                &self_name,
769                                &self_ty,
770                            )?);
771                            idx
772                        } else {
773                            return dbg_enum_none(line!());
774                        }
775                    }
776                    _ => return dbg_enum_none(line!()),
777                };
778                const_field[idx] = fname.to_string();
779                drop_stmts.push(i);
780            }
781        }
782        track_def(st, &mut defs2, &mut tainted2);
783    }
784    // Own the reaching-def values: defs2 borrows body.body, and the
785    // synthetic-constant scan below mutates the $VALUES array in place.
786    let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
787        .iter()
788        .map(|(k, v)| (*k, (*v).clone()))
789        .collect();
790    drop(defs2);
791    let defs2: std::collections::HashMap<u32, &Expr> =
792        defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
793
794    // Every ACC_ENUM field bound, every intermediate matched.
795    // Every ACC_ENUM FIELD must be bound to a constant. The converse
796    // need not hold: R8 drops the FIELD of a constant nothing reads
797    // externally while the $VALUES array keeps it (weixin lite/api/n's
798    // ON_DESTROY — a pass-1 local with no pass-2 sput). Field-less
799    // constants keep their ctor-string source name.
800    if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
801    {
802            if dbg_enum() {
803                eprintln!("[enum-gate677] const_fields={:?} const_field={:?} const_name={:?} const_ord={:?}",
804                    const_fields.iter().map(|f| f.name.to_string()).collect::<Vec<_>>(),
805                    const_field, const_name, const_ord);
806            }
807            return dbg_enum_none(line!());
808        }
809    }
810    // Obfuscated enums rename the ACC_ENUM FIELD (d/e/f) while the ctor's
811    // name STRING keeps the source identifier — the promoted constant
812    // must be declared under the FIELD name: every reference in the
813    // pool resolves through it. Using the name string broke all
814    // cross-file references (weixin +2.9k when newly-promoted enums
815    // swapped d/e/f for TEXT_ENTER_EDITING/…).
816    for i in 0..const_name.len() {
817        let field = &const_field[i];
818        if !field.is_empty() && field != &const_name[i] {
819            let id = java_ident(field);
820            if id.is_empty() || id.as_ref() != field.as_str() {
821                return dbg_enum_none(line!());
822            }
823            const_name[i] = field.clone();
824        }
825    }
826    {
827        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
828        if !const_name.iter().all(|c| seen.insert(c.as_str())) { return dbg_enum_none(line!()); }
829    }
830
831    // Synthetic constants: entries built INLINE inside the $VALUES
832    // array (`new Self("NAME", ord, ..)` with no ACC_ENUM field — R8
833    // drops the field when nothing outside reads it; weixin u2/i's
834    // "CONSTANT"). Promote them so the header declares the constant:
835    // the raw `new` in the array is "无法实例化枚举类", and every
836    // constant after the gap carries a SHIFTED ordinal (e was 2 in the
837    // dex, renders as position 1).
838    let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
839    {
840        let self_cls: &str = class.name.as_str();
841        if let Stmt::Block(vs) = &mut body.body {
842            for st in vs.iter_mut() {
843                crate::passes::walk_stmt_exprs(st, &mut |e| {
844                    crate::passes::deep_rewrite(e, &mut |x| {
845                        let Expr::NewArray { elem, init: Some(list), .. } = x else {
846                            return;
847                        };
848                        if elem.erased() != JavaType::Object(self_cls.into()) {
849                            return;
850                        }
851                        for slot in list.iter_mut() {
852                            let (n, ord, rest) = match slot {
853                                Expr::New { cls, args, raw: false, .. }
854                                    if cls.as_ref() == self_cls && args.len() >= 2 =>
855                                {
856                                    match (&args[0], &args[1]) {
857                                        (
858                                            Expr::Const(ConstVal::Str(sv)),
859                                            Expr::Const(ConstVal::Int(o)),
860                                        ) => (sv.to_string(), *o as i64, args[2..].to_vec()),
861                                        _ => continue,
862                                    }
863                                }
864                                _ => continue,
865                            };
866                            if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
867                                continue;
868                            }
869                            if const_name.contains(&n)
870                                || synth.iter().any(|(_, s2, _)| *s2 == n)
871                            {
872                                continue;
873                            }
874                            let Some(extras) = resolve_enum_extras(
875                                &rest,
876                                &defs2,
877                                tainted2,
878                                &var_of,
879                                &const_name,
880                                &self_name,
881                                &self_ty,
882                            ) else {
883                                continue;
884                            };
885                            *slot = Expr::Field {
886                                owner: None,
887                                cls: self_name.clone(),
888                                name: std::sync::Arc::from(n.as_str()),
889                                ty: self_ty.clone(),
890                                is_static: true,
891                            };
892                            synth.push((ord, n, extras));
893                        }
894                    });
895                });
896            }
897        }
898    }
899
900    // Pass 3: rewrite references to the intermediate locals into the
901    // constant identifiers (static-field reads on Self). POSITION-AWARE:
902    // a REUSED local must read as the constant defined most recently at
903    // that statement — a var-level map made every `arr[k] = v` in the
904    // $VALUES build reference the LAST constant (weixin +2.9k when this
905    // regressed values() contents).
906    {
907        let mut cur: HashMap<u32, usize> = HashMap::default();
908        if let Stmt::Block(vs) = &mut body.body {
909            for (i, st) in vs.iter_mut().enumerate() {
910                if let Some((var, idx)) = def_site.get(&i) {
911                    cur.insert(*var, *idx);
912                }
913                crate::passes::walk_stmt_exprs(st, &mut |e| {
914                    crate::passes::deep_rewrite(e, &mut |x| {
915                        if let Expr::Local { var, .. } = x {
916                            if let Some(&idx) = cur.get(var) {
917                                let name: std::sync::Arc<str> =
918                                    std::sync::Arc::from(const_name[idx].as_str());
919                                *x = Expr::Field {
920                                    owner: None,
921                                    cls: self_name.clone(),
922                                    name,
923                                    ty: self_ty.clone(),
924                                    is_static: true,
925                                };
926                            }
927                        }
928                    });
929                });
930            }
931        }
932    }
933
934    // Pass 4: drop the definitions and their sputs.
935    if let Stmt::Block(vs) = &mut body.body {
936        let drop_set: std::collections::HashSet<usize> =
937            drop_stmts.iter().copied().collect();
938        vs.retain(|_| true);
939        let mut idx = 0usize;
940        vs.retain(|_| {
941            let keep = !drop_set.contains(&idx);
942            idx += 1;
943            keep
944        });
945    }
946    // The resolved args consumed the clinit locals' readers; prune the
947    // now-dead defs from the remnant (impure inits survive as bare
948    // expression statements — drop_dead_locals' standard contract).
949    crate::passes::drop_dead_locals(&mut body.body);
950
951    let mut merged: Vec<(i64, EnumConst)> = const_ord
952        .into_iter()
953        .zip(
954            const_field
955                .into_iter()
956                .zip(const_name)
957                .zip(const_extra)
958                .map(|((field, name), extra_args)| EnumConst {
959                    field,
960                    name,
961                    extra_args,
962                }),
963        )
964        .collect();
965    for (ord, name, extra_args) in synth {
966        merged.push((
967            ord,
968            EnumConst {
969                field: name.clone(),
970                name,
971                extra_args,
972            },
973        ));
974    }
975    if merged.is_empty() {
976        return dbg_enum_none(line!()); // nothing extracted — keep the desugared form
977    }
978    merged.sort_by_key(|(o, _)| *o);
979    // Ordinals must be UNIQUE and ASCENDING; gaps are legal — R8 drops
980    // unused constants wholesale (weixin lite/api/n starts at ordinal
981    // 1). Java derives ordinal() from declaration position, so a gap
982    // shifts every later constant. Pad each gap with a synthetic
983    // constant (`_r<k>`) to keep positions faithful — only when no
984    // constant carries extra ctor args (a pad cannot invent them).
985    // The synthetic values()/valueOf() the true-enum render suppresses
986    // then see the pad (values() drift vs the dex $VALUES array — the
987    // compilable-and-ordinal-faithful side of the trade).
988    let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
989    let mut member_taken: jdc_core::FxHashSet<String> = merged
990        .iter()
991        .map(|(_, c)| c.name.clone())
992        .chain(
993            class
994                .static_fields
995                .iter()
996                .chain(class.instance_fields.iter())
997                .map(|f| f.name.to_string()),
998        )
999        .chain(class.all_methods().map(|m| m.name.to_string()))
1000        .collect();
1001    let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
1002    let mut expect: i64 = 0;
1003    let mut pad_k = 0u32;
1004    for (ord, c) in merged {
1005        if ord < expect {
1006            return dbg_enum_none(line!()); // duplicate/colliding ordinals — unfaithful
1007        }
1008        if ord > expect {
1009            // Extras-bearing ctors: a pad may borrow an adjacent
1010            // constant's literal extras ONLY when every ctor of the
1011            // class is benign (no branch, no throw, no non-<init>
1012            // invoke — pure param stores), so the invented value
1013            // cannot change runtime behavior beyond a dead constant's
1014            // stored field (jd1.u: gap at ordinal 7 + int extra arg
1015            // aborted the whole promotion — 92 files of broken
1016            // Enum.valueOf). Non-benign ctors keep the abort.
1017            let tmpl: Vec<Expr> = if has_extras {
1018                if !enum_ctors_benign(pool, class) {
1019                    return dbg_enum_none(line!()); // cannot synthesize the missing ctor args
1020                }
1021                match padded.last() {
1022                    Some(prev) => prev.extra_args.clone(),
1023                    None => c.extra_args.clone(),
1024                }
1025            } else {
1026                Vec::new()
1027            };
1028            while expect < ord {
1029                let pname = loop {
1030                    let cand = format!("_r{pad_k}");
1031                    pad_k += 1;
1032                    if !member_taken.contains(&cand) {
1033                        member_taken.insert(cand.clone());
1034                        break cand;
1035                    }
1036                };
1037                padded.push(EnumConst {
1038                    field: pname.clone(),
1039                    name: pname,
1040                    extra_args: tmpl.clone(),
1041                });
1042                expect += 1;
1043            }
1044        }
1045        expect = ord + 1;
1046        padded.push(c);
1047    }
1048    let out = padded;
1049    Some((out, body))
1050}
1051
1052/// True when every `<init>` of the class is a pure param-store: no
1053/// branch, no throw, no switch, no goto, and no invoke other than a
1054/// constructor call (the super delegation). A benign ctor cannot
1055/// validate or leak its arguments, so gap-pad constants may borrow a
1056/// neighbor's literal extras without observable effect.
1057fn enum_ctors_benign(pool: &DexPool, class: &PoolClass) -> bool {
1058    let mut checked = 0usize;
1059    for m in class.all_methods() {
1060        if &*m.name != "<init>" {
1061            continue;
1062        }
1063        checked += 1;
1064        let Some(dex) = pool.dex(m.dex_idx) else {
1065            return false;
1066        };
1067        let Some(ci) = dex.code_at(m.code_off) else {
1068            return false;
1069        };
1070        for ins in &ci.insns {
1071            use ddc_dex::insn::InsnKind;
1072            match ins.kind {
1073                InsnKind::If { .. }
1074                | InsnKind::Throw { .. }
1075                | InsnKind::PackedSwitch { .. }
1076                | InsnKind::SparseSwitch { .. }
1077                | InsnKind::Goto { .. } => return false,
1078                InsnKind::Invoke { method_idx, .. } => {
1079                    let mr = dex.method(method_idx);
1080                    if dex.string(mr.name_idx) != "<init>" {
1081                        return false;
1082                    }
1083                }
1084                _ => {}
1085            }
1086        }
1087    }
1088    checked > 0
1089}
1090
1091/// Update the rolling reaching-def map for enum-arg resolution. Only
1092/// flat single-target definitions keep the tracking sound; any control
1093/// flow taints it (a linear scan can no longer prove WHICH definition
1094/// reaches later capture sites).
1095fn track_def<'e>(
1096    st: &'e Stmt,
1097    defs: &mut std::collections::HashMap<u32, &'e Expr>,
1098    tainted: &mut bool,
1099) {
1100    match st {
1101        Stmt::LocalDef { var, init, .. } => match init {
1102            Some(e) => {
1103                defs.insert(*var, e);
1104            }
1105            None => {
1106                defs.remove(var);
1107            }
1108        },
1109        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
1110            if let Expr::Local { var, .. } = &**target {
1111                if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
1112                    defs.insert(*var, value);
1113                } else {
1114                    defs.remove(var);
1115                }
1116            }
1117        }
1118        // Plain expression statements (calls) don't define locals.
1119        Stmt::ExprStmt(_) => {}
1120        _ => *tainted = true,
1121    }
1122}
1123
1124/// Resolve every enum-ctor extra arg to a self-contained expression, or
1125/// reject the enum mode (None). R8 reuses ONE register across all
1126/// constant constructions — revenuecat's LogIntent builds 11 of 12
1127/// emoji lists through the same `list` local, reassigned between the
1128/// `new Self(.., list)` sites — and enum constants render OUTSIDE the
1129/// clinit where no local is in scope: an unresolved `Local` used to
1130/// print as the vt-dummy name (`DEBUG(var0)` — per-constant
1131/// cannot-find). Each Local is replaced by its reaching pure definition
1132/// (recursively) or, when it names an earlier constant's intermediate,
1133/// by a static-field reference to that constant. Any expression shape
1134/// WITHOUT locals passes through untouched (enum args may be arbitrary
1135/// expressions — BinOp/Cast/Method/New — each renders exactly once per
1136/// constant, so no duplication concern applies). Only an unresolvable
1137/// Local (missing/tainted def, depth > 4) rejects the whole enum
1138/// detection; the class then falls back to plain-field rendering, which
1139/// always compiles.
1140fn resolve_enum_extras(
1141    extras: &[Expr],
1142    defs: &std::collections::HashMap<u32, &Expr>,
1143    tainted: bool,
1144    var_of: &std::collections::HashMap<u32, usize>,
1145    const_name: &[String],
1146    self_name: &std::sync::Arc<str>,
1147    self_ty: &jdc_core::ir::expr::TypeRef,
1148) -> Option<Vec<Expr>> {
1149    extras
1150        .iter()
1151        .map(|e| {
1152            let mut out = e.clone();
1153            let mut fail = false;
1154            resolve_locals_in(
1155                &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
1156                &mut fail,
1157            );
1158            if fail {
1159                None
1160            } else {
1161                Some(out)
1162            }
1163        })
1164        .collect()
1165}
1166
1167#[allow(clippy::too_many_arguments)]
1168fn resolve_locals_in(
1169    e: &mut Expr,
1170    defs: &std::collections::HashMap<u32, &Expr>,
1171    tainted: bool,
1172    var_of: &std::collections::HashMap<u32, usize>,
1173    const_name: &[String],
1174    self_name: &std::sync::Arc<str>,
1175    self_ty: &jdc_core::ir::expr::TypeRef,
1176    depth: u32,
1177    fail: &mut bool,
1178) {
1179    crate::passes::deep_rewrite(e, &mut |x| {
1180        if let Expr::Local { var, .. } = x {
1181            // An earlier constant's intermediate: a static-field read of
1182            // that constant (declared above — backward reference, legal
1183            // in enum ctor args).
1184            if let Some(&ci) = var_of.get(var) {
1185                if let Some(nm) = const_name.get(ci) {
1186                    *x = Expr::Field {
1187                        owner: None,
1188                        cls: self_name.clone(),
1189                        name: std::sync::Arc::from(nm.as_str()),
1190                        ty: self_ty.clone(),
1191                        is_static: true,
1192                    };
1193                    return;
1194                }
1195            }
1196            if tainted || depth > 4 {
1197                *fail = true;
1198                return;
1199            }
1200            let Some(d) = defs.get(var).copied() else {
1201                *fail = true;
1202                return;
1203            };
1204            let mut sub = d.clone();
1205            resolve_locals_in(
1206                &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
1207                depth + 1, fail,
1208            );
1209            if *fail {
1210                return;
1211            }
1212            *x = sub;
1213        }
1214    });
1215}
1216
1217/// Render enum-constant constructor arguments via the shared expression
1218/// emitter (the VarTable is irrelevant for argument printing — no local
1219/// names appear — but the API requires one).
1220fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
1221    let dummy_vt = jdc_core::var::VarTable::default();
1222    let mut p = Printer::new(ctx, &dummy_vt);
1223    for (i, a) in args.iter().enumerate() {
1224        if i > 0 {
1225            out.push_str(", ");
1226        }
1227        p.expr(a, 0, out);
1228    }
1229    let _ = pool;
1230}
1231
1232/// Synthesize `throw new AbstractMethodError()` stubs for abstract
1233/// interface methods a CONCRETE class fails to implement. R8 tree-shakes
1234/// an interface method whose call sites it proved unreachable, leaving a
1235/// concrete class ART accepts but javac rejects ("X不是抽象的, 并且未覆盖
1236/// 抽象方法…"). jadx reproduces the same incomplete class; the stub
1237/// restores compilability AND matches ART semantics exactly (invoking a
1238/// stripped abstract method throws AbstractMethodError). `provided` is
1239/// computed from the DEX method table (not the render), so a method ddc
1240/// merely failed to render is never double-declared, and a same-erasure
1241/// return-type-specialized sibling (Family B: void `invoke` beside the
1242/// Object bridge) counts as provided → no colliding stub.
1243struct ParsedStub {
1244    ret: JavaType,
1245    args: Vec<JavaType>,
1246}
1247
1248/// Ctor argument signatures to mirror on an empty-shell class whose
1249/// ancestor chain ends at a Throwable-family FRAMEWORK class — read
1250/// from the embedded API database (the hand-written 26-name table it
1251/// replaced could not cover app-specific chains and hard-coded the
1252/// sig set instead of the real one). Private ctors are skipped: the
1253/// bridge `super(..)` would not compile against them (Throwable's
1254/// 4-arg suppression ctor is private).
1255fn throwable_ctor_sigs(fw: &str) -> Option<Vec<String>> {
1256    if !crate::fwdb::is_subtype(fw, "java/lang/Throwable") {
1257        return None;
1258    }
1259    let mut out: Vec<String> = Vec::new();
1260    crate::fwdb::for_each_method(fw, |d, f| {
1261        if d.starts_with("<init>(")
1262            && f & crate::fwdb::MF_STATIC == 0
1263            && f & crate::fwdb::MF_PRIVATE == 0
1264            && f & crate::fwdb::MF_UNKNOWN == 0
1265        {
1266            let hi = d.find(')').unwrap_or(d.len());
1267            let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1268            out.push(d[lo..hi].to_string());
1269        }
1270    });
1271    out.sort();
1272    out.dedup();
1273    Some(out)
1274}
1275
1276fn synth_missing_interface_stubs(
1277    pool: &DexPool,
1278    class: &PoolClass,
1279    out: &mut String,
1280    depth: usize,
1281    emitted_any: &mut bool,
1282) {
1283    use crate::access::*;
1284    // Concrete classes only: interfaces / abstract classes / annotations
1285    // legitimately leave methods unimplemented.
1286    if class.access & (ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION) != 0 {
1287        return;
1288    }
1289    fn argsig(d: &str) -> &str {
1290        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1291        let hi = d.find(')').unwrap_or(d.len());
1292        &d[lo..hi]
1293    }
1294    // Framework interface method lists come from the embedded API
1295    // database (fwdb) — see the lookup below.
1296
1297    // Transitive interface closure — seeded from the WHOLE superclass
1298    // chain, not just the class's own `implements`: weixin rd extends
1299    // abstract k0 implements ey2.a, R8 stripped rd's onAttach, and the
1300    // old own-interfaces-only seed saw an empty requirement (×51
1301    // weixin a.onAttach family). Pool interfaces contribute their
1302    // method lists; framework interfaces consult the table above.
1303    // (name, argsig) -> the unique declaring desc, or None when two
1304    // ancestors declare the same erasure with DIFFERENT descriptors —
1305    // a Java-inexpressible diamond where any synthesized stub would
1306    // itself fail "无法覆盖/无法实现" (weibo +1016 regression when the
1307    // first-wins desc was stubbed against a conflicting sibling).
1308    let mut required: jdc_core::FxHashMap<(String, String), Option<(String, String)>> =
1309        jdc_core::FxHashMap::default();
1310    let mut defaults: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1311    let mut stack: Vec<String> = Vec::new();
1312    {
1313        let mut cur: Option<&PoolClass> = Some(class);
1314        let mut hops = 0;
1315        while let Some(c) = cur {
1316            hops += 1;
1317            if hops > 64 {
1318                break;
1319            }
1320            stack.extend(c.interfaces.iter().cloned());
1321            let mut hit_fw = false;
1322            cur = c.super_name.as_ref().and_then(|s| {
1323                if s == "java/lang/Object" {
1324                    None
1325                } else {
1326                    let pc = pool.get(s);
1327                    if pc.is_none() {
1328                        hit_fw = true;
1329                    }
1330                    pc
1331                }
1332            });
1333            if hit_fw {
1334                // A FRAMEWORK superclass contributes methods the pool
1335                // walk cannot see: the class may already provide (or be
1336                // barred from overriding — Context.getString is final)
1337                // any requirement, and a stub here collided ×669 weibo.
1338                // No stubs past the framework boundary.
1339                return;
1340            }
1341        }
1342    }
1343    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1344    while let Some(iname) = stack.pop() {
1345        if !seen.insert(iname.clone()) {
1346            continue;
1347        }
1348        let Some(ic) = pool.get(&iname) else {
1349            // Framework interface: the embedded API database provides
1350            // its abstract instance methods with real access flags
1351            // (abstract, not static, not default) — the hand-written
1352            // 13-interface table this replaced covered only classics.
1353            if crate::fwdb::class_flags(&iname) & crate::fwdb::CF_INTERFACE != 0 {
1354                crate::fwdb::for_each_method(&iname, |d, f| {
1355                    if f & crate::fwdb::MF_ABSTRACT == 0
1356                        || f & crate::fwdb::MF_STATIC != 0
1357                        || f & crate::fwdb::MF_UNKNOWN != 0
1358                        || d.starts_with("<init>")
1359                    {
1360                        return;
1361                    }
1362                    let hi = d.find('(').unwrap_or(d.len());
1363                    let mn = d[..hi].to_string();
1364                    let md = d[hi..].to_string();
1365                    let key = (mn.clone(), argsig(&md).to_string());
1366                    match required.entry(key) {
1367                        std::collections::hash_map::Entry::Vacant(v) => {
1368                            v.insert(Some((mn, md)));
1369                        }
1370                        std::collections::hash_map::Entry::Occupied(mut o) => {
1371                            if o.get().as_ref().is_some_and(|(_, d0)| *d0 != md) {
1372                                o.insert(None);
1373                            }
1374                        }
1375                    }
1376                });
1377            }
1378            continue;
1379        };
1380        for m in ic.all_methods() {
1381            if m.is_static() || &*m.name == "<clinit>" || &*m.name == "<init>" {
1382                continue;
1383            }
1384            let key = (m.name.to_string(), argsig(&m.desc).to_string());
1385            if m.code_off == 0 {
1386                let mdesc = m.desc.to_string();
1387                match required.entry(key) {
1388                    std::collections::hash_map::Entry::Vacant(v) => {
1389                        v.insert(Some((m.name.to_string(), mdesc)));
1390                    }
1391                    std::collections::hash_map::Entry::Occupied(mut o) => {
1392                        if o.get().as_ref().is_some_and(|(_, d)| *d != mdesc) {
1393                            o.insert(None);
1394                        }
1395                    }
1396                }
1397            } else {
1398                defaults.insert(key);
1399            }
1400        }
1401        stack.extend(ic.interfaces.iter().cloned());
1402    }
1403    if required.is_empty() {
1404        return;
1405    }
1406    // Methods provided by the class + its concrete superclass chain, plus
1407    // the java/lang/Object publics that satisfy common requirements.
1408    let mut provided: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1409    provided.insert(("equals".into(), "Ljava/lang/Object;".into()));
1410    provided.insert(("hashCode".into(), String::new()));
1411    provided.insert(("toString".into(), String::new()));
1412    // (name, desc) pairs whose erasure twin exists elsewhere in the
1413    // chain with a DIFFERENT desc — stub-hostile shapes (see below).
1414    let mut chain_sigs: jdc_core::FxHashMap<(String, String), jdc_core::FxHashSet<String>> =
1415        jdc_core::FxHashMap::default();
1416    let mut cur: Option<&PoolClass> = Some(class);
1417    while let Some(c) = cur {
1418        for m in c.all_methods() {
1419            if !m.is_static() && &*m.name != "<init>" && &*m.name != "<clinit>" {
1420                chain_sigs
1421                    .entry((m.name.to_string(), argsig(&m.desc).to_string()))
1422                    .or_default()
1423                    .insert(m.desc.to_string());
1424            }
1425        }
1426        for m in c.all_methods() {
1427            // A method PROVIDES the implementation if it is not abstract:
1428            // bytecode (code_off != 0) OR native (JNI body, code_off == 0
1429            // but ACC_NATIVE — `…ToNative` methods satisfy their interface
1430            // and must not be stubbed twice).
1431            if m.access & ACC_ABSTRACT == 0 && !m.is_static() {
1432                // A RENAMED method no longer provides its raw (name,
1433                // argsig): impostor overrides (return type satisfying no
1434                // ancestor declaration) and erasure-clash losers render
1435                // under their registry display, and the ancestor
1436                // requirement falls to the stub below — matching ART,
1437                // where the interface proto never resolved either.
1438                let renamed = jdc_core::rename::field_display(&c.name, &m.name, &m.desc)
1439                    .is_some();
1440                if !renamed {
1441                    provided.insert((m.name.to_string(), argsig(&m.desc).to_string()));
1442                }
1443            }
1444        }
1445        cur = c.super_name.as_ref().and_then(|s| {
1446            if s == "java/lang/Object" {
1447                None
1448            } else {
1449                pool.get(s)
1450            }
1451        });
1452    }
1453    let mut chain_conflict: jdc_core::FxHashSet<(String, String)> =
1454        jdc_core::FxHashSet::default();
1455    for ((name, _argsig), descs) in chain_sigs.iter() {
1456        if descs.len() > 1 {
1457            for d in descs {
1458                chain_conflict.insert((name.clone(), d.clone()));
1459            }
1460        }
1461    }
1462    let mut missing: Vec<(String, String)> = Vec::new();
1463    for (key, m) in &required {
1464        let Some(m) = m else { continue }; // conflicting ancestor descs
1465        if defaults.contains(key) || provided.contains(key) {
1466            continue;
1467        }
1468        // Kotlin bridge variant (return kotlin/Unit beside the real
1469        // boolean/object twin a framework interface declares): stubbing
1470        // it renders `java.lang.Void addAll(Collection)` that neither
1471        // implements the twin nor serves its callers — the poisoned
1472        // return type cascades through every call site (weibo +703
1473        // regression). The honest 未覆盖 error for the twin stays.
1474        if m.1.ends_with(")Lkotlin/Unit;") {
1475            continue;
1476        }
1477        // A chain method sharing the erasure but NOT the descriptor
1478        // (abstract super with a different return, an impostor the
1479        // rename pass left because it satisfies another ancestor):
1480        // the stub would clash with it — skip; the class keeps the
1481        // single honest 未覆盖 error instead of gaining a 无法覆盖 one.
1482        if chain_conflict.contains(&(m.0.clone(), m.1.clone())) {
1483            continue;
1484        }
1485        missing.push(m.clone());
1486    }
1487    missing.sort();
1488    let ind = "    ".repeat(depth + 1);
1489    for (mname, mdesc) in missing {
1490        let lo = mdesc.find('(').map(|i| i + 1).unwrap_or(0);
1491        let hi = mdesc.find(')').unwrap_or(mdesc.len());
1492        let ret_s = &mdesc[hi + 1..];
1493        let args_v = split_arg_descs(&mdesc[lo..hi]);
1494        let d = ParsedStub { ret: crate::desc_type(ret_s), args: args_v };
1495        if *emitted_any {
1496            out.push('\n');
1497        }
1498        out.push_str(&ind);
1499        out.push_str("public ");
1500        out.push_str(&type_name(pool, &d.ret));
1501        out.push(' ');
1502        out.push_str(&java_ident(&mname));
1503        out.push('(');
1504        for (i, a) in d.args.iter().enumerate() {
1505            if i > 0 {
1506                out.push_str(", ");
1507            }
1508            out.push_str(&type_name(pool, a));
1509            out.push_str(&format!(" p{}", i + 1));
1510        }
1511        out.push_str(") {\n");
1512        out.push_str(&ind);
1513        out.push_str("    throw new AbstractMethodError();\n");
1514        out.push_str(&ind);
1515        out.push_str("}\n");
1516        *emitted_any = true;
1517    }
1518}
1519
1520/// Synthetic storage fields backing name()/ordinal() on a FALLBACK enum
1521/// (a plain class cannot inherit java.lang.Enum's). The enum ctor's
1522/// (String, int) trace params feed them (injected in emit_method).
1523const ENUM_NAME_FIELD: &str = "$ddcName";
1524const ENUM_ORD_FIELD: &str = "$ddcOrdinal";
1525
1526/// True when the class renders as a fallback enum BASE: ACC_ENUM,
1527/// restoration failed (enum_consts None — caller-gated), super is the
1528/// Enum root. Constant subclasses inherit the synthesized members.
1529fn fallback_enum_base(class: &PoolClass) -> bool {
1530    class.is_enum()
1531        && class
1532            .super_name
1533            .as_ref()
1534            .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
1535            .unwrap_or(true)
1536}
1537
1538/// Full gate for the synthetic Enum surface: fallback base WITH the
1539/// trace ctor and no `$ddc*` field-name clash. The ctor injection in
1540/// emit_method, the member synthesis and the dex-valueOf suppression
1541/// must all agree, or fields/ctor/renders desynchronize.
1542fn enum_synth_active(class: &PoolClass) -> bool {
1543    fallback_enum_base(class)
1544        && enum_trace_ctor(class)
1545        && !class
1546            .static_fields
1547            .iter()
1548            .chain(class.instance_fields.iter())
1549            .any(|f| &*f.name == ENUM_NAME_FIELD || &*f.name == ENUM_ORD_FIELD)
1550}
1551
1552/// Does the class have the compiler-generated `(String, int, ..)` enum
1553/// ctor whose leading params feed the synthetic name/ordinal fields?
1554fn enum_trace_ctor(class: &PoolClass) -> bool {
1555    class.all_methods().any(|m| {
1556        &*m.name == "<init>"
1557            && m.parsed_desc()
1558                .map(|d| {
1559                    matches!(d.args.first(), Some(JavaType::Object(s))
1560                        if s.as_ref() == "java/lang/String")
1561                        && matches!(d.args.get(1), Some(JavaType::Int))
1562                })
1563                .unwrap_or(false)
1564    })
1565}
1566
1567/// Synthesize the java.lang.Enum member surface a FALLBACK enum base
1568/// lacks (it renders as a plain class — `extends Enum` is illegal in
1569/// source): storage fields `$ddcName`/`$ddcOrdinal` (fed by the ctor
1570/// trace-param injection in emit_method), `name()`, `ordinal()`,
1571/// `valueOf(String)` and `compareTo`, plus `values()` when R8 stripped
1572/// the dex copy. Kills the illegal dex `Enum.valueOf(X.class, ..)`
1573/// ("无法将类 Enum<E>中的方法 valueOf应用到给定类型" — weixin ×1,307,
1574/// weibo ×477) and makes ordinal() EXACT (the interim values()-scan
1575/// misread enums whose $VALUES carries register-reuse duplicates,
1576/// protobuf ub). Purely additive: the dex never declares these members
1577/// on the enum class (they are Enum inheritances), so no collision —
1578/// except the dex valueOf, suppressed at the method loop.
1579fn synth_enum_members(
1580    pool: &DexPool,
1581    class: &PoolClass,
1582    out: &mut String,
1583    depth: usize,
1584    emitted_any: &mut bool,
1585) {
1586    use crate::access::*;
1587    if !fallback_enum_base(class) {
1588        return;
1589    }
1590    let self_ty = JavaType::Object(class.name.as_str().into());
1591    let self_name = type_name(pool, &self_ty);
1592    let self_arr = JavaType::Array(Box::new(self_ty));
1593    let arr_name = type_name(pool, &self_arr);
1594    let traced = enum_synth_active(class);
1595    let has_values = class.all_methods().any(|m| {
1596        &*m.name == "values"
1597            && m.access & ACC_STATIC != 0
1598            && m.parsed_desc()
1599                .map(|d| d.ret == self_arr && d.args.is_empty())
1600                .unwrap_or(false)
1601    });
1602    let ind = "    ".repeat(depth + 1);
1603    let push_line = |out: &mut String, l: &str| {
1604        out.push_str(&ind);
1605        out.push_str(l);
1606        out.push('\n');
1607    };
1608    // values(): from the $VALUES array field when the dex copy is gone.
1609    if !has_values {
1610        let want = format!("[L{};", class.name);
1611        if let Some(vf) = class.static_fields.iter().find(|f| &*f.desc == want.as_str()) {
1612            if *emitted_any {
1613                out.push('\n');
1614            }
1615            *emitted_any = true;
1616            push_line(out, &format!("public static {} values() {{", arr_name));
1617            push_line(
1618                out,
1619                &format!("    return ({}) {}.clone();", arr_name, java_ident(&vf.name)),
1620            );
1621            push_line(out, "}");
1622        }
1623    }
1624    if !traced {
1625        return; // no (String,int) ctor: fields would stay default — keep
1626                // the scan-based ordinal only (below) and the dex valueOf.
1627    }
1628    if *emitted_any {
1629        out.push('\n');
1630    }
1631    *emitted_any = true;
1632    push_line(out, &format!("private java.lang.String {};", ENUM_NAME_FIELD));
1633    push_line(out, &format!("private int {};", ENUM_ORD_FIELD));
1634    out.push('\n');
1635    push_line(out, &format!(
1636        "public java.lang.String name() {{\n{}    return this.{};\n{}}}",
1637        ind, ENUM_NAME_FIELD, ind
1638    ));
1639    out.push('\n');
1640    push_line(out, &format!(
1641        "public int ordinal() {{\n{}    return this.{};\n{}}}",
1642        ind, ENUM_ORD_FIELD, ind
1643    ));
1644    out.push('\n');
1645    push_line(out, &format!(
1646        "public int compareTo({0} o) {{\n{1}    return this.{2} - o.{2};\n{1}}}",
1647        self_name, ind, ENUM_ORD_FIELD
1648    ));
1649    out.push('\n');
1650    push_line(out, &format!(
1651        "public static {0} valueOf(java.lang.String s) {{\n{1}    for ({0} v : values()) {{\n{1}        if (v.name().equals(s)) {{\n{1}            return v;\n{1}        }}\n{1}    }}\n{1}    throw new java.lang.IllegalArgumentException(s);\n{1}}}",
1652        self_name, ind
1653    ));
1654}
1655
1656/// Scan-based ordinal() fallback for fallback enums WITHOUT the trace
1657/// ctor (rare): position in values(). Kept separate so synth_enum_members
1658/// can bail after values() synthesis.
1659fn synth_enum_ordinal_scan(
1660    pool: &DexPool,
1661    class: &PoolClass,
1662    out: &mut String,
1663    depth: usize,
1664    emitted_any: &mut bool,
1665) {
1666    if !fallback_enum_base(class) || enum_trace_ctor(class) {
1667        return;
1668    }
1669    if class
1670        .all_methods()
1671        .any(|m| &*m.name == "ordinal" && &*m.desc == "()I")
1672    {
1673        return;
1674    }
1675    let self_arr = JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into())));
1676    let arr_name = type_name(pool, &self_arr);
1677    let ind = "    ".repeat(depth + 1);
1678    if *emitted_any {
1679        out.push('\n');
1680    }
1681    *emitted_any = true;
1682    out.push_str(&ind);
1683    out.push_str("public int ordinal() {\n");
1684    out.push_str(&ind);
1685    out.push_str(&format!("    {} vs = values();\n", arr_name));
1686    out.push_str(&ind);
1687    out.push_str("    for (int i = 0; i < vs.length; i++) {\n");
1688    out.push_str(&ind);
1689    out.push_str("        if (vs[i] == this) {\n");
1690    out.push_str(&ind);
1691    out.push_str("            return i;\n");
1692    out.push_str(&ind);
1693    out.push_str("        }\n");
1694    out.push_str(&ind);
1695    out.push_str("    }\n");
1696    out.push_str(&ind);
1697    out.push_str("    return 0;\n");
1698    out.push_str(&ind);
1699    out.push_str("}\n");
1700}
1701
1702/// Strip the Meituan Robust hotpatch guard from a RESTORED enum ctor:
1703/// `Object[] v3 = {name, ordinal}; Class[] v4 = ..; boolean s =
1704/// PatchProxy.isSupport(v3, this, changeQuickRedirect, ..); if (s) {
1705/// PatchProxy.accessDispatch(..); return; }`. The guard reads the enum's
1706/// own static field — bytecode-legal (Robust instruments after javac),
1707/// source-illegal (JLS 8.9.2, both simple AND qualified forms). It is
1708/// dead code for decompiled output (no patch is ever loaded). Removal is
1709/// surgical: statements containing a PatchProxy call, the locals feeding
1710/// its arguments, their defs and element writes — a fixpoint over the
1711/// top-level list. collect_enum_constants' dex gate already refused
1712/// restoration when self-static reads go beyond the redirect field.
1713fn strip_robust_enum_ctor_guard(body: &mut Stmt) {
1714    let is_patchy = |e: &Expr| {
1715        matches!(e, Expr::Method { cls, name, .. }
1716            if cls.contains("PatchProxy")
1717                && (&**name == "isSupport"
1718                    || &**name == "accessDispatch"
1719                    || &**name == "proxy"
1720                    || &**name == "isSupportClinit"
1721                    || &**name == "accessDispatchClinit"))
1722    };
1723    let Stmt::Block(vs) = body else { return };
1724    if !vs.iter().any(|st| {
1725        let mut h = false;
1726        crate::passes::visit_all_exprs(st, &mut |x| {
1727            if is_patchy(x) {
1728                h = true;
1729            }
1730        });
1731        h
1732    }) {
1733        return;
1734    }
1735    let mut dead: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
1736    // Locals read inside the PatchProxy call args of a statement (the
1737    // pack arrays) — dead once the statement goes.
1738    let patchy_arg_locals = |st: &Stmt, out: &mut jdc_core::FxHashSet<u32>| {
1739        crate::passes::visit_all_exprs(st, &mut |x| {
1740            if let Expr::Method { args, .. } = x {
1741                if is_patchy(x) {
1742                    for a in args {
1743                        let mut aa = a.clone();
1744                        crate::passes::deep_rewrite(&mut aa, &mut |y| {
1745                            if let Expr::Local { var, .. } = y {
1746                                out.insert(*var);
1747                            }
1748                        });
1749                    }
1750                }
1751            }
1752        });
1753    };
1754    for _ in 0..4 {
1755        let before = vs.len();
1756        let mut i = 0usize;
1757        while i < vs.len() {
1758            let action = {
1759                let st = &vs[i];
1760                match st {
1761                    Stmt::LocalDef { var, .. } if dead.contains(var) => Some((*var, false)),
1762                    Stmt::LocalDef { var, init: Some(e), .. } => {
1763                        let mut has_patchy = false;
1764                        let mut ec = e.clone();
1765                        crate::passes::deep_rewrite(&mut ec, &mut |x| {
1766                            if is_patchy(x) {
1767                                has_patchy = true;
1768                            }
1769                        });
1770                        if has_patchy {
1771                            Some((*var, true))
1772                        } else {
1773                            None
1774                        }
1775                    }
1776                    Stmt::If { .. } => {
1777                        let mut h = false;
1778                        crate::passes::visit_all_exprs(st, &mut |x| {
1779                            if is_patchy(x) {
1780                                h = true;
1781                            }
1782                        });
1783                        if h {
1784                            Some((u32::MAX, true))
1785                        } else {
1786                            None
1787                        }
1788                    }
1789                    // `v3[0] = str;` / `v3 = ..;` writes to a dead pack.
1790                    Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1791                        let mut tgt_dead = false;
1792                        let mut tc = target.clone();
1793                        crate::passes::deep_rewrite(&mut tc, &mut |x| {
1794                            if let Expr::Local { var, .. } = x {
1795                                if dead.contains(var) {
1796                                    tgt_dead = true;
1797                                }
1798                            }
1799                        });
1800                        if tgt_dead {
1801                            Some((u32::MAX, false))
1802                        } else {
1803                            None
1804                        }
1805                    }
1806                    _ => None,
1807                }
1808            };
1809            if let Some((var, collect_args)) = action {
1810                if collect_args {
1811                    patchy_arg_locals(&vs[i], &mut dead);
1812                }
1813                if var != u32::MAX {
1814                    dead.insert(var);
1815                }
1816                vs.remove(i);
1817            } else {
1818                i += 1;
1819            }
1820        }
1821        if vs.len() == before {
1822            break;
1823        }
1824    }
1825}
1826
1827#[allow(clippy::only_used_in_recursion)]
1828fn emit_class_body(
1829    pool: &DexPool,
1830    class: &PoolClass,
1831    ctx: &DexCtx<'_>,
1832    opts: &ClassOptions,
1833    out: &mut String,
1834    depth: usize,
1835) -> anyhow::Result<()> {
1836    let ind = indent(depth);
1837    // The class's own header must use the RENAMED display name — the
1838    // ctor path (is_init) already renames; a header declaring `class a`
1839    // while the file/ctor say `a_2` leaves the ctor looking like a
1840    // method with no return type (MinisApp's Y2.a vs y2.a package
1841    // case-collision: 2,518 javac parse errors).
1842    let cname = crate::apply_class_rename(&class.name);
1843    let (_, simple) = split_name(&cname);
1844    // A class emitted as its OWN top-level file (depth 0) must declare a
1845    // flat `$` name — `class Outer.Inner` is not declarable at file
1846    // scope. An INLINE nested member (depth > 0) declares its own
1847    // segment inside the parent's body.
1848    let simple = if depth == 0 {
1849        simple.to_string()
1850    } else {
1851        // R8 names can END in `$`: an empty last `$`-segment would
1852        // render `class  {` — keep the whole simple name.
1853        let seg = simple.rsplit('$').next().unwrap_or(&simple);
1854        if seg.is_empty() {
1855            simple.to_string()
1856        } else {
1857            seg.to_string()
1858        }
1859    };
1860    // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
1861    // declared — restricted contextual type names escape here and in
1862    // the ctor, file name, and every type reference.
1863    let simple = if is_restricted_type_name(&simple) {
1864        format!("_{simple}")
1865    } else {
1866        simple
1867    };
1868    let is_iface = class.is_interface();
1869    let is_enum = class.is_enum();
1870
1871    let mut head = String::new();
1872    let a = class.access;
1873    if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
1874        head.push_str("public ");
1875    }
1876    // Inline nested members need their `static` (interfaces/annotations
1877    // are implicitly static; a missing `static` on a member class makes
1878    // every `new Report(...)` site an "outer instance required" error).
1879    if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
1880        head.push_str("static ");
1881    }
1882    if a & ACC_FINAL != 0 && !is_enum {
1883        head.push_str("final ");
1884    }
1885    if a & ACC_ABSTRACT != 0 && !is_iface {
1886        head.push_str("abstract ");
1887    }
1888    if a & ACC_ANNOTATION != 0 {
1889        head.push('@');
1890    }
1891    let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
1892        collect_enum_constants(pool, class, ctx)
1893    } else {
1894        None
1895    };
1896    if let Some(ecs) = &enum_consts {
1897        // True `enum` declaration: constants render in the header, the
1898        // desugared boilerplate (const fields, their <clinit> inits —
1899        // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
1900        // disappears. R8-renamed values()/valueOf() stay (they do not
1901        // collide with the compiler-generated ones); javac-named ones
1902        // are skipped at the method loop below.
1903        head.push_str("enum ");
1904        head.push_str(&sanitize_ref(&simple));
1905        let _ = ecs;
1906    } else if is_enum {
1907        // An enum with constant-specific bodies carries ACC_ABSTRACT —
1908        // `abstract final` is an illegal modifier combination; abstract
1909        // (already pushed above) suppresses the hardcoded final.
1910        let abstract_ = a & ACC_ABSTRACT != 0;
1911        head.push_str(if abstract_ {
1912            "/* enum */ class "
1913        } else {
1914            "/* enum */ final class "
1915        });
1916        head.push_str(&sanitize_ref(&simple));
1917    } else if is_iface {
1918        head.push_str("interface ");
1919        head.push_str(&sanitize_ref(&simple));
1920    } else {
1921        head.push_str("class ");
1922        head.push_str(&sanitize_ref(&simple));
1923    }
1924    // The obscured-super import was emitted at the package line; the
1925    // clause must render the SIMPLE name (the qualified form binds to
1926    // the class itself even with the import present).
1927    let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
1928    if is_iface {
1929        // A @interface cannot declare extends at all (JLS 9.6): the dex
1930        // interface table lists java/lang/annotation/Annotation for
1931        // every annotation type — rendering it produced "对于
1932        // @interfaces, 不允许 'extends'" (weibo 1100).
1933        let is_annot = a & ACC_ANNOTATION != 0;
1934        if !class.interfaces.is_empty() && !is_annot {
1935            head.push_str(" extends ");
1936            head.push_str(&join_dotted(pool, &class.interfaces));
1937        }
1938    } else {
1939        if let Some(sup) = &class.super_name {
1940            // Suppress `extends` for TRUE `enum` declarations (they
1941            // implicitly extend java.lang.Enum) and for the enum BASE in
1942            // fallback form (super IS java/lang/Enum — `class X extends
1943            // Enum` is illegal). But an ENUM CONSTANT SUBCLASS rendered
1944            // in fallback (`/* enum */ class j$1`, ACC_ENUM, super = the
1945            // enum base) MUST keep `extends j`: without it the constant
1946            // loses its subtype relation and every `field = new j$1(..)`
1947            // fails ("j$1无法转换为j", weibo jsoup TokeniserState ×172).
1948            let suppress_extends =
1949                is_enum && (enum_consts.is_some() || sup == "java/lang/Enum");
1950            if sup != "java/lang/Object" && !suppress_extends {
1951                head.push_str(" extends ");
1952                head.push_str(&render_super(sup));
1953            }
1954        }
1955        if !class.interfaces.is_empty() {
1956            head.push_str(if is_iface {
1957                " extends "
1958            } else {
1959                " implements "
1960            });
1961            head.push_str(&join_dotted(pool, &class.interfaces));
1962        }
1963    }
1964
1965    out.push_str(&ind);
1966    out.push_str(&head);
1967    out.push_str(" {\n");
1968
1969    // True-enum constant list: the constants lead the body, ahead of
1970    // any remaining fields.
1971    if let Some((ecs, _)) = &enum_consts {
1972        for (i, ec) in ecs.iter().enumerate() {
1973            out.push_str(&indent(depth + 1));
1974            out.push_str(&java_ident(&ec.name));
1975            if !ec.extra_args.is_empty() {
1976                let mut line = String::from("(");
1977                render_enum_args(ctx, pool, &ec.extra_args, &mut line);
1978                line.push(')');
1979                out.push_str(&line);
1980            }
1981            if i + 1 < ecs.len() {
1982                out.push_str(",\n");
1983            } else {
1984                out.push_str(";\n");
1985            }
1986        }
1987        out.push('\n');
1988    }
1989
1990    // Fields.
1991    // A static final whose dex-encoded static value is OVERWRITTEN by
1992    // the clinit must render as a blank final: the dex encoded array
1993    // value is just the class-load default (ART lets <clinit> reassign
1994    // static finals), but Java forbids touching an initialized final
1995    // ("无法为 static final 变量 $stable 分配值" — Compose `$stable = 0`
1996    // + clinit `= 8`; WalletBaseUI HARDCODE_* = 0 + clinit computed
1997    // value; the original null-valued shape: Kotlin `object` INSTANCE,
1998    // okio SegmentPool). Scan the clinit's raw SPuts once.
1999    let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
2000        // Gate: only classes with a VALUED final static can hit the
2001        // conflict — skip the clinit decode otherwise (decoding it for
2002        // every class cost seconds on 98k-class corpora).
2003        (|| {
2004            let has_valued_final = class.static_fields.iter().enumerate().any(|(i, f)| {
2005                f.access & crate::access::ACC_FINAL != 0
2006                    && class.static_values.get(i).is_some()
2007            });
2008            if !has_valued_final {
2009                return None;
2010            }
2011            let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
2012            let dex = pool.dex(m.dex_idx)?;
2013            let ci = dex.code_at(m.code_off)?;
2014            let mut set: jdc_core::FxHashSet<(
2015                std::sync::Arc<str>,
2016                std::sync::Arc<str>,
2017            )> = jdc_core::FxHashSet::default();
2018            for ins in &ci.insns {
2019                if let InsnKind::SPut { field_idx, .. } = ins.kind {
2020                    let fid = dex.field(field_idx);
2021                    // clinit only writes own-class fields here; match
2022                    // (name, type) against the field list below.
2023                    set.insert((
2024                        std::sync::Arc::from(dex.string(fid.name_idx)),
2025                        std::sync::Arc::from(dex.type_name(fid.type_idx)),
2026                    ));
2027                }
2028            }
2029            Some(set)
2030        })();
2031    let mut field_emitted = false;
2032    for (i, f) in class.static_fields.iter().enumerate() {
2033        // Enum constant fields became the header list above.
2034        if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
2035            continue;
2036        }
2037        if field_emitted || !class.instance_fields.is_empty() {
2038            out.push('\n');
2039        }
2040        field_emitted = true;
2041        // A blank-final: static final, clinit-assigned. Interfaces are
2042        // excluded — their fields are implicitly `public static final`
2043        // and MUST carry an initializer.
2044        let blank_final = f.access & crate::access::ACC_FINAL != 0
2045            && !class.is_interface()
2046            && clinit_sputs
2047                .as_ref()
2048                .is_some_and(|s| {
2049                    s.contains(&(
2050                        f.name.clone(),
2051                        f.desc.clone(),
2052                    ))
2053                });
2054        emit_field(
2055            pool,
2056            f,
2057            if blank_final {
2058                None
2059            } else {
2060                class.static_values.get(i)
2061            },
2062            &class.name,
2063            out,
2064            depth + 1,
2065            true,
2066            class.is_interface(),
2067        );
2068    }
2069    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
2070        out.push('\n');
2071    }
2072    for (i, f) in class.instance_fields.iter().enumerate() {
2073        if i > 0 {
2074            out.push('\n');
2075        }
2076        emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
2077    }
2078
2079    // Methods.
2080    // Duplicated (name, parameter-types) pairs in one class are javac
2081    // "method is already defined" errors (weibo: 11.5k hits). Source
2082    // cannot declare them, so exactly one survives: covariant bridges
2083    // (`Object get(int)` beside `ByteString get(int)` — the compiler
2084    // GENERATES bridges), plus R8 output that lost the bridge flag
2085    // (okio Buffer.clone, interface getView re-declarations). A
2086    // non-bridge method outranks a bridge for the same signature
2087    // (bridge bodies are delegation stubs); first occurrence otherwise.
2088    // Never drop a signature outright — the last method standing for a
2089    // key is always rendered.
2090    // Keyed on the DISPLAY name: a member-renamed method (return-type-
2091    // only collisions on isolated classes) no longer collides with its
2092    // sibling — both render, each caller resolving through the registry.
2093    fn sig_key<'m>(cls: &str, renames_on: bool, m: &'m PoolMethod) -> (&'m str, &'m str) {
2094        let d: &'m str = &m.desc;
2095        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2096        let hi = d.find(')').unwrap_or(d.len());
2097        let n = if renames_on {
2098            jdc_core::rename::field_display(cls, &m.name, d).unwrap_or(&*m.name)
2099        } else {
2100            &*m.name
2101        };
2102        (n, &d[lo..hi])
2103    }
2104    let renames_on = jdc_core::rename::member_rename_active();
2105    let suppress_dex_valueof = class.is_enum()
2106        && enum_consts.is_none()
2107        && enum_synth_active(class);
2108    let methods: Vec<&PoolMethod> = class.all_methods().collect();
2109    let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
2110    for (i, m) in methods.iter().enumerate() {
2111        let key = sig_key(&class.name, renames_on, m);
2112        match claim.get(&key) {
2113            Some(&j)
2114                if m.access & crate::access::ACC_BRIDGE == 0
2115                    && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
2116            {
2117                claim.insert(key, i);
2118            }
2119            None => {
2120                claim.insert(key, i);
2121            }
2122            _ => {}
2123        }
2124    }
2125    // NOTE: a same-signature bridge is already retired by the claim map
2126    // above. An ERASURE-shaped bridge (SAM/variance: params are the
2127    // Object-erased types of a sibling's) must NOT be skipped: with raw
2128    // (non-generic) interface rendering the bridge is exactly what
2129    // satisfies the interface — skipping it turned every Kotlin lambda
2130    // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
2131    // +17), while the "对invoke的引用不明确" it appeared to fix was a
2132    // missing-kotlin-classpath cascade, not a ddc bug.
2133    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
2134    for (i, m) in methods.iter().enumerate() {
2135        if &*m.name == "<clinit>" {
2136            continue; // rendered after the fields
2137        }
2138        if claim.get(&sig_key(&class.name, renames_on, m)) != Some(&i) {
2139            continue;
2140        }
2141        if bridge_shadowed_by_ancestor(pool, class, m) {
2142            continue;
2143        }
2144        // Fallback enum base: the dex valueOf body is
2145        // `Enum.valueOf(X.class, str)` — illegal once X is a plain class
2146        // (T is not bounded by Enum). Suppress it; synth_enum_members
2147        // renders the values()-scan replacement.
2148        if suppress_dex_valueof
2149            && m.is_static()
2150            && &*m.name == "valueOf"
2151            && m.parsed_desc().map(|d| {
2152                d.args.len() == 1
2153                    && matches!(d.args[0], JavaType::Object(ref sn)
2154                        if sn.as_ref() == "java/lang/String")
2155                    && d.ret == JavaType::Object(class.name.as_str().into())
2156            }).unwrap_or(false)
2157        {
2158            continue;
2159        }
2160        // True-enum rendering: javac auto-generates values()/valueOf()
2161        // — the original-named ones must not re-declare (R8-renamed
2162        // copies stay, they do not collide).
2163        if let Some((ecs, _)) = &enum_consts {
2164            let d = m.parsed_desc();
2165            let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
2166            let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
2167            let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
2168            let one_str = d
2169                .as_ref()
2170                .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
2171                .unwrap_or(false);
2172            let static_ = m.is_static();
2173            if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
2174                continue;
2175            }
2176            if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
2177                continue;
2178            }
2179            // Enum constructors must be private in source form.
2180            let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
2181                let mut c = (*m).clone();
2182                c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
2183                Some(c)
2184            } else {
2185                None
2186            };
2187            let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
2188            let mark = out.len();
2189            if emitted_any {
2190                out.push('\n');
2191            }
2192            if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
2193                emitted_any = true;
2194            } else {
2195                out.truncate(mark);
2196            }
2197            let _ = ecs;
2198            continue;
2199        }
2200        let mark = out.len();
2201        if emitted_any {
2202            out.push('\n');
2203        }
2204        if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
2205            emitted_any = true;
2206        } else {
2207            out.truncate(mark);
2208        }
2209    }
2210    // Inherited-ctor bridges: dex method refs resolve through the
2211    // hierarchy, so `new C(args)` against a class C with NO declared
2212    // <init> legally targets the SUPERCLASS ctor (weixin tenpay
2213    // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
2214    // has no inherited constructors: without a bridge, C's implicit
2215    // default ctor is the only one and every arg-carrying construction
2216    // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
2217    // Mirror the nearest ctor-declaring ancestor's public/protected
2218    // ctors as thin `super(..)` delegations.
2219    if !class.is_interface()
2220        && enum_consts.is_none()
2221        && !class.all_methods().any(|m| &*m.name == "<init>")
2222    {
2223        if let Some(mut sup) = class.super_name.clone() {
2224            loop {
2225                if sup == "java/lang/Object" {
2226                    break;
2227                }
2228                let Some(sc) = pool.get(&sup) else { break };
2229                let ctors: Vec<&PoolMethod> = sc
2230                    .all_methods()
2231                    .filter(|m| &*m.name == "<init>")
2232                    .collect();
2233                if !ctors.is_empty() {
2234                    for sm in ctors {
2235                        if sm.access & crate::access::ACC_PRIVATE != 0
2236                            || (sm.access
2237                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2238                                == 0)
2239                        {
2240                            continue; // private/package-private: no legal bridge
2241                        }
2242                        let Some(d) = sm.parsed_desc() else { continue };
2243                        let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2244                            "public "
2245                        } else {
2246                            "protected "
2247                        };
2248                        if emitted_any {
2249                            out.push('\n');
2250                        }
2251                        out.push_str(&format!("    {}", "    ".repeat(depth)));
2252                        out.push_str(mods);
2253                        out.push_str(&sanitize_ref(&simple));
2254                        out.push('(');
2255                        let mut names = Vec::with_capacity(d.args.len());
2256                        for (i, a) in d.args.iter().enumerate() {
2257                            if i > 0 {
2258                                out.push_str(", ");
2259                            }
2260                            out.push_str(&type_name(pool, a));
2261                            let nm = format!("p{}", i + 1);
2262                            out.push(' ');
2263                            out.push_str(&nm);
2264                            names.push(nm);
2265                        }
2266                        out.push_str(") {\n");
2267                        out.push_str(&format!("    {}    super({});\n", "    ".repeat(depth), names.join(", ")));
2268                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2269                        emitted_any = true;
2270                    }
2271                    break;
2272                }
2273                match &sc.super_name {
2274                    Some(n) => sup = n.clone(),
2275                    None => break,
2276                }
2277            }
2278        }
2279    }
2280
2281    // Referenced-but-undeclared inherited-ctor bridges. The block above
2282    // covers a class with NO declared ctor; this covers a class that
2283    // DECLARES ctors but is missing one that dex callers reach THROUGH
2284    // it. Dex resolves a `<init>` method-ref up the superclass chain, so
2285    // a subclass `super(2, recv, owner, name, sig, flags)` may target
2286    // `m.<init>(int,Object,Class,String,String,int)` that `m`
2287    // (FunctionReferenceImpl) does NOT declare — its super `l` does.
2288    // Java has no inherited ctors, so without a bridge on `m` every such
2289    // subclass fails ("无法将类 m中的构造器 m应用到给定类型"). Mirror each
2290    // referenced sig C lacks from the nearest ancestor declaring it, as a
2291    // thin `super(..)` delegation. Ref-gated (only sigs actually called
2292    // through C) so ordinary classes gain nothing.
2293    if !class.is_interface()
2294        && enum_consts.is_none()
2295        && class.all_methods().any(|m| &*m.name == "<init>")
2296    {
2297        if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2298            fn arg_sig(d: &str) -> &str {
2299                let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2300                let hi = d.find(')').unwrap_or(d.len());
2301                &d[lo..hi]
2302            }
2303            let declared: jdc_core::FxHashSet<&str> = class
2304                .all_methods()
2305                .filter(|m| &*m.name == "<init>")
2306                .map(|m| arg_sig(&m.desc))
2307                .collect();
2308            let mut missing: Vec<&String> = refs
2309                .iter()
2310                .filter(|s| !declared.contains(s.as_str()))
2311                .collect();
2312            missing.sort();
2313            for sig in missing {
2314                // Nearest ancestor declaring a public/protected <init>
2315                // with this exact arg signature.
2316                let mut sup = class.super_name.clone();
2317                let mut found: Option<&PoolMethod> = None;
2318                while let Some(s) = sup {
2319                    if s == "java/lang/Object" {
2320                        break;
2321                    }
2322                    let Some(sc) = pool.get(&s) else { break };
2323                    if let Some(sm) = sc.all_methods().find(|m| {
2324                        &*m.name == "<init>"
2325                            && arg_sig(&m.desc) == sig.as_str()
2326                            && m.access & crate::access::ACC_PRIVATE == 0
2327                            && m.access
2328                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2329                                != 0
2330                    }) {
2331                        found = Some(sm);
2332                        break;
2333                    }
2334                    sup = sc.super_name.clone();
2335                }
2336                let Some(sm) = found else {
2337                    // The walk terminated at java/lang/Object — which
2338                    // always declares <init>()V, so an empty-sig ref
2339                    // through this class resolved there dex-wise. The
2340                    // Java implicit default ctor is gone (the class
2341                    // declares other ctors); mirror it as an explicit
2342                    // public bridge (AgentMenuTask$Request →
2343                    // AppBrandProxyUIProcessTask$ProcessRequest:
2344                    // "需要: Parcel 找到: 没有参数", weixin 205).
2345                    if sig.is_empty() {
2346                        if emitted_any {
2347                            out.push('\n');
2348                        }
2349                        out.push_str(&format!("    {}", "    ".repeat(depth)));
2350                        out.push_str("public ");
2351                        out.push_str(&sanitize_ref(&simple));
2352                        out.push_str("() {\n");
2353                        out.push_str(&format!(
2354                            "    {}    super();\n",
2355                            "    ".repeat(depth)
2356                        ));
2357                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2358                        emitted_any = true;
2359                    }
2360                    continue;
2361                };
2362                let Some(d) = sm.parsed_desc() else { continue };
2363                let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2364                    "public "
2365                } else {
2366                    "protected "
2367                };
2368                if emitted_any {
2369                    out.push('\n');
2370                }
2371                out.push_str(&format!("    {}", "    ".repeat(depth)));
2372                out.push_str(mods);
2373                out.push_str(&sanitize_ref(&simple));
2374                out.push('(');
2375                let mut names = Vec::with_capacity(d.args.len());
2376                for (i, a) in d.args.iter().enumerate() {
2377                    if i > 0 {
2378                        out.push_str(", ");
2379                    }
2380                    out.push_str(&type_name(pool, a));
2381                    let nm = format!("p{}", i + 1);
2382                    out.push(' ');
2383                    out.push_str(&nm);
2384                    names.push(nm);
2385                }
2386                out.push_str(") {\n");
2387                out.push_str(&format!(
2388                    "    {}    super({});\n",
2389                    "    ".repeat(depth),
2390                    names.join(", ")
2391                ));
2392                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2393                emitted_any = true;
2394            }
2395        }
2396    }
2397
2398    // Framework-ancestor ctor bridge. The two blocks above mirror POOL
2399    // ancestors; both stop at a framework (off-pool) superclass. A class
2400    // whose DIRECT superclass is a framework type (in android.jar) that
2401    // declares a ctor the class lacks also needs a bridge — `super(args)`
2402    // resolves against the jar. Only the DIRECT-super-is-framework case is
2403    // safe: an intermediate pool ancestor with no matching ctor would
2404    // break the `super()` chain (that ancestor needs its own bridge, which
2405    // its own refs may not trigger). Covers weixin g3 extends
2406    // org.xml.sax.SAXException (`new g3(String)` → SAXException(String)),
2407    // the custom-exception / Parcelable family the pool walk can't reach.
2408    if !class.is_interface() && enum_consts.is_none() {
2409        let direct_fw = class
2410            .super_name
2411            .as_ref()
2412            .map(|s| s != "java/lang/Object" && pool.get(s).is_none())
2413            .unwrap_or(false);
2414        if direct_fw {
2415            if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2416                fn arg_sig(d: &str) -> &str {
2417                    let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2418                    let hi = d.find(')').unwrap_or(d.len());
2419                    &d[lo..hi]
2420                }
2421                let declared: jdc_core::FxHashSet<&str> = class
2422                    .all_methods()
2423                    .filter(|m| &*m.name == "<init>")
2424                    .map(|m| arg_sig(&m.desc))
2425                    .collect();
2426                let mut missing: Vec<&String> = refs
2427                    .iter()
2428                    .filter(|s| !declared.contains(s.as_str()))
2429                    .collect();
2430                missing.sort();
2431                for sig in missing {
2432                    // Never bridge the empty sig here: a no-ctor class
2433                    // gets Java's implicit default, and a class WITH
2434                    // ctors is covered by the referenced-but-undeclared
2435                    // block's Object-walk tail (emitting in both was
2436                    // `已在类 b0中定义了构造器 b0()` ×6 weixin).
2437                    if sig.is_empty() {
2438                        continue;
2439                    }
2440                    let args = split_arg_descs(sig);
2441                    if emitted_any {
2442                        out.push('\n');
2443                    }
2444                    out.push_str(&format!("    {}", "    ".repeat(depth)));
2445                    out.push_str("public ");
2446                    out.push_str(&sanitize_ref(&simple));
2447                    out.push('(');
2448                    let mut names = Vec::with_capacity(args.len());
2449                    for (i, a) in args.iter().enumerate() {
2450                        if i > 0 {
2451                            out.push_str(", ");
2452                        }
2453                        out.push_str(&type_name(pool, a));
2454                        let nm = format!("p{}", i + 1);
2455                        out.push(' ');
2456                        out.push_str(&nm);
2457                        names.push(nm);
2458                    }
2459                    out.push_str(") {\n");
2460                    out.push_str(&format!(
2461                        "    {}    super({});\n",
2462                        "    ".repeat(depth),
2463                        names.join(", ")
2464                    ));
2465                    out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2466                    emitted_any = true;
2467                }
2468            }
2469        }
2470    }
2471
2472    // Throwable-family ctor bridge for empty-shell exception classes
2473    // (see throwable_ctor_sigs). Walks the pool chain to the nearest
2474    // FRAMEWORK ancestor, stopping at any pool ancestor that DECLARES
2475    // ctors (the mirror block above covers that shape) — so chains of
2476    // empty shells (C extends P extends Error) each get the bridges and
2477    // every super(..) resolves against its parent's bridge.
2478    if !class.is_interface()
2479        && enum_consts.is_none()
2480        && !class.all_methods().any(|m| &*m.name == "<init>")
2481    {
2482        let mut fw_anc: Option<&str> = None;
2483        if let Some(sup) = class.super_name.as_deref() {
2484            let mut cur = sup;
2485            let mut hops = 0;
2486            loop {
2487                hops += 1;
2488                if hops > 32 || cur == "java/lang/Object" {
2489                    break;
2490                }
2491                match pool.get(cur) {
2492                    Some(sc) => {
2493                        if sc.all_methods().any(|m| &*m.name == "<init>") {
2494                            break;
2495                        }
2496                        match sc.super_name.as_deref() {
2497                            Some(n) => cur = n,
2498                            None => break,
2499                        }
2500                    }
2501                    None => {
2502                        fw_anc = Some(cur);
2503                        break;
2504                    }
2505                }
2506            }
2507        }
2508        if let Some(sigs) = fw_anc.and_then(throwable_ctor_sigs) {
2509            // The framework-ancestor bridge above already emitted every
2510            // sig the dex method-ref table attributes to this class —
2511            // emitting the table copy too was `已在类 z中定义了构造器
2512            // z(String)` (+60 weixin dup-def). The empty sig is exempt
2513            // there, so the table keeps it.
2514            let ref_sigs = pool.ctor_ref_sigs(&class.name);
2515            for sig in &sigs {
2516                if !sig.is_empty()
2517                    && ref_sigs.is_some_and(|rs| rs.contains(sig))
2518                {
2519                    continue;
2520                }
2521                let args = split_arg_descs(sig);
2522                if emitted_any {
2523                    out.push('\n');
2524                }
2525                out.push_str(&format!("    {}", "    ".repeat(depth)));
2526                out.push_str("public ");
2527                out.push_str(&sanitize_ref(&simple));
2528                out.push('(');
2529                let mut names = Vec::with_capacity(args.len());
2530                for (i, a) in args.iter().enumerate() {
2531                    if i > 0 {
2532                        out.push_str(", ");
2533                    }
2534                    out.push_str(&type_name(pool, a));
2535                    let nm = format!("p{}", i + 1);
2536                    out.push(' ');
2537                    out.push_str(&nm);
2538                    names.push(nm);
2539                }
2540                out.push_str(") {\n");
2541                out.push_str(&format!(
2542                    "    {}    super({});\n",
2543                    "    ".repeat(depth),
2544                    names.join(", ")
2545                ));
2546                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2547                emitted_any = true;
2548            }
2549        }
2550    }
2551
2552    // Static resolver helpers extracted from branched-delegation ctors
2553    // (passes::extract_branched_delegation_helper) — the computation the
2554    // Java first-statement rule cannot host inline.
2555    for h in crate::passes::take_ctor_helpers(&class.name) {
2556        if emitted_any {
2557            out.push('\n');
2558        }
2559        out.push_str(&indent(depth + 1));
2560        out.push_str("private static ");
2561        out.push_str(&type_name(pool, &h.ret));
2562        out.push(' ');
2563        out.push_str(&h.name);
2564        out.push('(');
2565        for (i, (ty, nm)) in h.params.iter().enumerate() {
2566            if i > 0 {
2567                out.push_str(", ");
2568            }
2569            out.push_str(&type_name(pool, ty));
2570            out.push(' ');
2571            out.push_str(&java_ident(nm));
2572        }
2573        out.push_str(") {\n");
2574        let p = Printer::new(ctx, &h.vt);
2575        let text = p.with_indent(depth + 2).into_string(&h.body);
2576        for line in text.lines() {
2577            if line.trim().is_empty() {
2578                continue;
2579            }
2580            out.push_str(line);
2581            out.push('\n');
2582        }
2583        out.push_str(&indent(depth + 1));
2584        out.push_str("}\n");
2585        emitted_any = true;
2586    }
2587
2588    // Synthetic relay ctors for FULL-mode branched delegations
2589    // (passes::extract_branched_delegation_helper): the public ctor
2590    // renders `this(resolve$X(params))`; this unpacks the carrier
2591    // array into the one legal first-statement delegation. Primitives
2592    // unbox through their wrappers (the array boxes them on pack).
2593    // ONE relay per class: the (Object[]) erasure is class-unique. Two
2594    // Full-mode ctors in one class (rare; each worker rewrites its own
2595    // body independently) resolve deterministically — sorted by origin
2596    // desc, the least emits and the others keep the honest
2597    // ctor-missing error. No duplicate-declaration risk.
2598    for (ri, (_desc, r)) in crate::passes::take_ctor_relays(&class.name)
2599        .into_iter()
2600        .enumerate()
2601    {
2602        if ri > 0 {
2603            continue;
2604        }
2605        if emitted_any {
2606            out.push('\n');
2607        }
2608        // Same depth rule as emit_class_body's header: a depth-0 class
2609        // declares its FLAT `$` name (the ctor must match it — weixin
2610        // ModularizingPkgRetrieverContract$SharedIPCCallArgs rendered a
2611        // tail-named relay ctor: "方法声明无效" parse-abort); an INLINE
2612        // nested member declares its last segment.
2613        let disp = crate::apply_class_rename(&class.name);
2614        let (_, simple) = split_name(&disp);
2615        let ctor_name = if depth == 0 {
2616            simple.clone()
2617        } else {
2618            let seg = simple.rsplit('$').next().unwrap_or("");
2619            if seg.is_empty() {
2620                simple.clone()
2621            } else {
2622                seg.to_string()
2623            }
2624        };
2625        out.push_str(&indent(depth + 1));
2626        out.push_str("private ");
2627        out.push_str(&java_ident(&ctor_name));
2628        out.push_str("(Object[] h$relay) {\n");
2629        out.push_str(&indent(depth + 2));
2630        out.push_str(if r.is_super { "super(" } else { "this(" });
2631        for (i, t) in r.formals.iter().enumerate() {
2632            if i > 0 {
2633                out.push_str(", ");
2634            }
2635            let slot = format!("h$relay[{}]", i);
2636            match t {
2637                JavaType::Boolean => out.push_str(&format!("((Boolean) {}).booleanValue()", slot)),
2638                JavaType::Byte => out.push_str(&format!("((Byte) {}).byteValue()", slot)),
2639                JavaType::Char => out.push_str(&format!("((Character) {}).charValue()", slot)),
2640                JavaType::Short => out.push_str(&format!("((Short) {}).shortValue()", slot)),
2641                JavaType::Int => out.push_str(&format!("((Integer) {}).intValue()", slot)),
2642                JavaType::Float => out.push_str(&format!("((Float) {}).floatValue()", slot)),
2643                JavaType::Long => out.push_str(&format!("((Long) {}).longValue()", slot)),
2644                JavaType::Double => out.push_str(&format!("((Double) {}).doubleValue()", slot)),
2645                other => {
2646                    out.push('(');
2647                    out.push_str(&type_name(pool, other));
2648                    out.push_str(") ");
2649                    out.push_str(&slot);
2650                }
2651            }
2652        }
2653        out.push_str(");\n");
2654        out.push_str(&indent(depth + 1));
2655        out.push_str("}\n");
2656        emitted_any = true;
2657    }
2658
2659    // Missing-abstract-method stubs (R8 tree-shook an interface method a
2660    // concrete class no longer implements; javac rejects the incomplete
2661    // class). Synthesized last so they sit after the real members.
2662    synth_missing_interface_stubs(pool, class, out, depth, &mut emitted_any);
2663
2664    // Fallback enum base: recover the Enum member surface the
2665    // plain-class rendering lacks (name/ordinal/valueOf/compareTo +
2666    // synthetic storage fields fed by the ctor injection).
2667    if class.is_enum() && enum_consts.is_none() {
2668        synth_enum_members(pool, class, out, depth, &mut emitted_any);
2669        synth_enum_ordinal_scan(pool, class, out, depth, &mut emitted_any);
2670    }
2671
2672    // Static initializer. INTERFACES cannot carry a `static { }` block in
2673    // Java — their clinit only assigns constants, which static_values (or
2674    // the `= null` default) already render as field initializers; skip
2675    // the block entirely.
2676    let skip_clinit = class.is_interface();
2677    if let Some((_, clinit_body)) = enum_consts.take() {
2678        // True-enum <clinit>: the constant assignments are already gone;
2679        // render the remainder (the $VALUES array build) directly.
2680        // Skip an empty remainder (all statements were constant inits).
2681        let empty = match &clinit_body.body {
2682            Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
2683            _ => false,
2684        };
2685        if !empty {
2686            if emitted_any {
2687                out.push('\n');
2688            }
2689            out.push_str(&indent(depth + 1));
2690            out.push_str("static {\n");
2691            let p = Printer::new(ctx, &clinit_body.vt);
2692            let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
2693            for line in text.lines() {
2694                if line.trim().is_empty() {
2695                    continue;
2696                }
2697                out.push_str(line);
2698                out.push('\n');
2699            }
2700            out.push_str(&indent(depth + 1));
2701            out.push_str("}\n");
2702            emitted_any = true;
2703        }
2704    } else if let Some(clinit) = (!skip_clinit)
2705        .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
2706        .flatten()
2707    {
2708        let mark = out.len();
2709        if emitted_any {
2710            out.push('\n');
2711        }
2712        if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
2713            emitted_any = true;
2714        } else {
2715            out.truncate(mark);
2716        }
2717    }
2718
2719    // Nested member classes (clean `$` tails only; anonymous/local/lambda
2720    // classes are emitted as their own top-level files by the driver).
2721    for nested in nested_members(pool, class, ctx) {
2722        if emitted_any {
2723            out.push('\n');
2724        }
2725        let nested_ctx = DexCtx::new(pool, nested);
2726        out.push('\n');
2727        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
2728        emitted_any = true;
2729    }
2730
2731    out.push_str(&ind);
2732    out.push_str("}\n");
2733    Ok(())
2734}
2735
2736/// Member classes of `class` (direct children by the `$` chain / member
2737/// annotations), excluding anonymous / local / lambda shapes.
2738fn nested_members<'a>(
2739    pool: &'a DexPool,
2740    class: &'a PoolClass,
2741    ctx: &DexCtx<'_>,
2742) -> Vec<&'a PoolClass> {
2743    let mut out = Vec::new();
2744    for name in pool.children_of(&class.name) {
2745        let Some(pc) = pool.get(name) else { continue };
2746        // Only a REAL `outer$tail` name is an inline member: the
2747        // children index also carries annotation-derived outers
2748        // (EnclosingClass) with no naming relationship to the child
2749        // (obfuscated apps pair a 1-char name with a long enclosing
2750        // descriptor) — the blind slice panicked there (alipay
2751        // `a.a.a.a.c`, exposed once children_of actually returned data).
2752        let Some(rest) = name
2753            .strip_prefix(class.name.as_str())
2754            .and_then(|t| t.strip_prefix('$'))
2755        else {
2756            continue;
2757        };
2758        // The SAME predicate top_level_classes uses for the standalone
2759        // decision: a non-clean member tail ($ExternalSynthetic flat
2760        // units, hyphen markers, digit-led anonymous/local shapes) is
2761        // emitted as its own file — inlining it here too DOUBLE-EMITS
2762        // the class, and the nested copy renders corrupt (weibo:
2763        // 12,152 nested `ExternalSyntheticLambdaN` chimeras across
2764        // 4,316 files; Recorder's nested copy mixed a sibling's
2765        // captures — `int f$0` + Builder.setSource — 无法取消引用int).
2766        if !crate::clean_member_tail(rest) {
2767            continue;
2768        }
2769        if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
2770            continue;
2771        }
2772        out.push(pc);
2773    }
2774    out
2775}
2776
2777// Eight parameters are all load-bearing (pool, field, static value,
2778// owner class, output, depth, staticness, interface-init requirement);
2779// bundling them into a struct would obscure each call site.
2780#[allow(clippy::too_many_arguments)]
2781fn emit_field(
2782    pool: &DexPool,
2783    f: &crate::PoolField,
2784    init: Option<&StaticValue>,
2785    f_class: &str,
2786    out: &mut String,
2787    depth: usize,
2788    is_static: bool,
2789    require_init: bool,
2790) {
2791    let ind = indent(depth);
2792    let mut line = String::new();
2793    let a = f.access;
2794    if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
2795        line.push_str("public ");
2796    } else if a & ACC_PRIVATE != 0 {
2797        line.push_str("private ");
2798    } else if a & ACC_PROTECTED != 0 {
2799        line.push_str("protected ");
2800    }
2801    if is_static {
2802        line.push_str("static ");
2803    }
2804    if a & ACC_FINAL != 0 && !unfinal_field(f_class, &f.name) {
2805        line.push_str("final ");
2806    }
2807    if a & ACC_SYNTHETIC != 0 {
2808        line.push_str("/* synthetic */ ");
2809    }
2810    if a & ACC_TRANSIENT_HINT != 0 {
2811        line.push_str("transient ");
2812    }
2813    if a & ACC_VOLATILE_HINT != 0 {
2814        line.push_str("volatile ");
2815    }
2816    let ty = type_name(pool, &desc_type(&f.desc));
2817    line.push_str(&ty);
2818    line.push(' ');
2819    let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
2820    // javac reserves `$assertionsDisabled`; the emit side prints every
2821    // REFERENCE under the private alias ASSERT_FIELD, so the
2822    // DECLARATION must carry the same alias or the field is 找不到符号
2823    // at each `if (!$jcdcAssertionsDisabled)` guard (QQ jbox2d Vec2
2824    // ×166).
2825    let fname: std::borrow::Cow<str> = if fname == "$assertionsDisabled" {
2826        std::borrow::Cow::Borrowed(jdc_core::analysis::ASSERT_FIELD)
2827    } else {
2828        std::borrow::Cow::Borrowed(fname)
2829    };
2830    line.push_str(&java_ident(&fname));
2831    let mut rendered = None;
2832    if let Some(v) = init {
2833        rendered = render_static_value(pool, v, f_class);
2834    }
2835    if rendered.is_none() && require_init {
2836        // Interface fields MUST have an initializer in Java; the dex may
2837        // not carry a static_values entry for a compile-time-constant the
2838        // compiler folded away. Keep it compilable.
2839        let default = match desc_type(&f.desc) {
2840            JavaType::Boolean => "false",
2841            JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
2842            JavaType::Long => "0L",
2843            JavaType::Float => "0.0F",
2844            JavaType::Double => "0.0",
2845            _ => "null",
2846        };
2847        rendered = Some(default.to_string());
2848    }
2849    if let Some(text) = rendered {
2850        // A long field holding an `Int(i64)` static value needs the `L`
2851        // suffix: without it the literal is an int and overflows
2852        // (`long d = -6343169151696340687` failed javac).
2853        let text = if matches!(desc_type(&f.desc), JavaType::Long)
2854            && text.chars().all(|c| c.is_ascii_digit() || c == '-')
2855        {
2856            format!("{text}L")
2857        } else {
2858            text
2859        };
2860        line.push_str(" = ");
2861        line.push_str(&text);
2862    }
2863    line.push(';');
2864    out.push_str(&ind);
2865    out.push_str(&line);
2866    out.push('\n');
2867}
2868
2869// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
2870// volatile(0x40)/transient(0x80) for fields.
2871pub const ACC_VOLATILE_HINT: u32 = 0x40;
2872pub const ACC_TRANSIENT_HINT: u32 = 0x80;
2873
2874fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
2875    Some(match v {
2876        StaticValue::Int(i) => i.to_string(),
2877        StaticValue::Float(f) => format_float(*f as f64, true),
2878        StaticValue::Double(d) => format_float(*d, false),
2879        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
2880        StaticValue::Type(t) => format!("{}.class", dotted(t)),
2881        StaticValue::Boolean(b) => b.to_string(),
2882        StaticValue::Null => "null".into(),
2883        StaticValue::Field(cls, name, desc) => {
2884            // The rename registry owns the display name (obscuring-field
2885            // renames key on (owner, name, desc)) — a const field ref must
2886            // render the SAME name as the declaration.
2887            let n: std::borrow::Cow<str> = match jdc_core::rename::field_display(cls, name, desc) {
2888                Some(d) => std::borrow::Cow::Borrowed(d),
2889                None => java_ident(name),
2890            };
2891            if cls == owner {
2892                n.into_owned()
2893            } else {
2894                format!("{}.{}", print_class_name(pool, cls), n)
2895            }
2896        }
2897        StaticValue::Other => return None,
2898    })
2899}
2900
2901/// Render one method straight into the class buffer `out`. Returns
2902/// whether anything was written (skips: no descriptor, deferred to a
2903/// monitored thread). The old shape returned a per-method String that
2904/// the caller copied in — one extra full copy of every method body.
2905/// `enum_promoted`: the class rendered as a true `enum` declaration
2906/// (constants in the header), which changes what a ctor signature may
2907/// declare.
2908/// d8's covariant-return bridges: `Context getContext() { return
2909/// super.getContext(); }` shadowing MMActivity's covariant
2910/// `AppCompatActivity getContext()`. Source cannot declare the
2911/// wider-return override, and the rendered bridge HIJACKS every
2912/// `this.getContext()` in the file to the wide type (weixin
2913/// FTSBaseVoiceSearchUI's `AppCompatActivity context =
2914/// this.getContext();` — Context无法转换为AppCompatActivity ×74). The
2915/// inherited covariant method satisfies source callers natively and
2916/// javac regenerates binary bridges on demand — skip the render.
2917///
2918/// TWO gates, both learned the hard way (the ungated ACC_BRIDGE +
2919/// ancestor-name version skipped erasure/interface-satisfier bridges
2920/// and cost lark +29k — the claim-map's raw-interface lesson at whole-
2921/// hierarchy scale):
2922/// 1. INSTRUCTION SHAPE: the body must be exactly invoke-super to a
2923///    same-named method + optional move-result + return (≤3 insns).
2924///    Erasure bridges call THIS with casts; interface satisfiers are
2925///    real bodies — both keep rendering.
2926/// 2. POOL ancestor (super chain, exact name+args, non-private)
2927///    declares the covariant source. Framework ancestors are not
2928///    consulted (fwdb carries no arg descs — a name-only match could
2929///    skip a real overload); such bridges keep rendering.
2930fn bridge_shadowed_by_ancestor(pool: &DexPool, class: &PoolClass, m: &PoolMethod) -> bool {
2931    if m.access & crate::access::ACC_BRIDGE == 0 || &*m.name == "<init>" || m.code_off == 0 {
2932        return false;
2933    }
2934    let Some(d) = m.parsed_desc() else {
2935        return false;
2936    };
2937    // Gate 1: invoke-super-same-name + move-result*/return* tail only.
2938    let Some(dex) = pool.dex(m.dex_idx) else {
2939        return false;
2940    };
2941    let dex = &*dex;
2942    let Some(code) = dex.code_insns_bytes_at(m.code_off) else {
2943        return false;
2944    };
2945    let mut head_ok = false;
2946    let mut bad_tail = false;
2947    let mut cnt = 0usize;
2948    let mut first = true;
2949    ddc_dex::insn::scan_instructions(code, &mut |op, pc, bytes| {
2950        cnt += 1;
2951        if first {
2952            first = false;
2953            if op == 0x6f || op == 0x75 {
2954                let unit = bytes
2955                    .get(2 * (pc + 1)..2 * (pc + 1) + 2)
2956                    .map(|b| u16::from_le_bytes([b[0], b[1]]) as u32)
2957                    .unwrap_or(u32::MAX);
2958                if unit != u32::MAX {
2959                    let mr = dex.method(unit);
2960                    head_ok = dex.string(mr.name_idx) == &*m.name;
2961                }
2962            }
2963        } else if !matches!(op, 0x0a | 0x0b | 0x0c | 0x0e | 0x0f | 0x10 | 0x11) {
2964            bad_tail = true;
2965        }
2966    });
2967    if !head_ok || bad_tail || !(2..=3).contains(&cnt) {
2968        return false;
2969    }
2970    // Gate 2: pool super chain declares name+args.
2971    let mut cur: Option<String> = class.super_name.clone();
2972    let mut hops = 0u32;
2973    while let Some(s) = cur {
2974        hops += 1;
2975        if hops > 64 {
2976            return false;
2977        }
2978        let Some(pc) = pool.get_if_materialized(&s) else {
2979            return false; // unresolvable ancestor — keep the bridge
2980        };
2981        if pc.all_methods().any(|am| {
2982            am.name == m.name
2983                && am.access & crate::access::ACC_PRIVATE == 0
2984                && am.parsed_desc().is_some_and(|ad| ad.args == d.args)
2985        }) {
2986            return true;
2987        }
2988        cur = pc.super_name.clone();
2989    }
2990    false
2991}
2992
2993fn emit_method(
2994    pool: &DexPool,
2995    class: &PoolClass,
2996    ctx: &DexCtx<'_>,
2997    m: &PoolMethod,
2998    depth: usize,
2999    enum_promoted: bool,
3000    out: &mut String,
3001) -> anyhow::Result<bool> {
3002    let ind = indent(depth);
3003    let desc = m.parsed_desc();
3004
3005    // Signature.
3006    let mut sig = String::with_capacity(192);
3007    let a = m.access;
3008    if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
3009        sig.push_str("public ");
3010    } else if a & ACC_PRIVATE != 0 {
3011        // A fallback enum BASE (`/* enum */ class`, not a promoted
3012        // `enum`) renders its constant-specific-body subclasses as
3013        // SEPARATE same-package classes (`j$6 extends j`); their ctors
3014        // call `super(name, ordinal, ..)`, which a PRIVATE base ctor
3015        // forbids ("j(String,int) has private access"). Widen the base
3016        // ctor to package-private so the subclass links. The base is
3017        // abstract → no external instantiation risk. Only the BASE
3018        // (super is Enum/Object), not the constant subclass itself.
3019        let fallback_enum_base_ctor = class.is_enum()
3020            && !enum_promoted
3021            && &*m.name == "<init>"
3022            && class
3023                .super_name
3024                .as_ref()
3025                .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
3026                .unwrap_or(true);
3027        if !fallback_enum_base_ctor {
3028            sig.push_str("private ");
3029        }
3030    } else if a & ACC_PROTECTED != 0 {
3031        sig.push_str("protected ");
3032    }
3033    let is_clinit = &*m.name == "<clinit>";
3034    let is_init = &*m.name == "<init>";
3035    if a & ACC_STATIC != 0 || is_clinit {
3036        sig.push_str("static ");
3037    }
3038    if a & ACC_FINAL != 0 {
3039        sig.push_str("final ");
3040    }
3041    // `abstract synchronized` is an illegal combination — obfuscated
3042    // builds mark abstract bridges synchronized (WhatsApp
3043    // SQLiteOpenHelper).
3044    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
3045        sig.push_str("synchronized ");
3046    }
3047    if a & ACC_NATIVE != 0 {
3048        sig.push_str("native ");
3049    }
3050    if a & ACC_ABSTRACT != 0 {
3051        sig.push_str("abstract ");
3052    }
3053    if a & ACC_SYNTHETIC != 0 {
3054        sig.push_str("/* synthetic */ ");
3055    }
3056
3057    // Body (needed for parameter names even for abstract methods).
3058    let mut body = decompile_method(pool, class, m).ok().flatten();
3059    if &*m.name == "<clinit>" {
3060        if let Some(b) = &mut body {
3061            crate::passes::strip_clinit_hotfix_guard(&mut b.body);
3062        }
3063    }
3064    // An interface method WITH a body is a `default` method (JLS 9.4.3)
3065    // unless static/private — dex carries no `default` flag, so the
3066    // plain form rendered an abstract signature with a body and javac
3067    // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
3068    // default REQUIRES a body: only push it once the body is confirmed
3069    // (a failed decompile renders a body-less declaration).
3070    if class.is_interface()
3071        && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
3072        && body.is_some()
3073    {
3074        sig.insert_str(0, "default ");
3075    }
3076    let param_names: Vec<String> = body
3077        .as_ref()
3078        .map(|b| {
3079            let mut ps: Vec<(u16, String)> =
3080                b.vt.vars
3081                    .iter()
3082                    .filter(|v| v.is_param && v.name != "this")
3083                    .map(|v| (v.slot, v.name.clone()))
3084                    .collect();
3085            ps.sort_by_key(|(s, _)| *s);
3086            ps.into_iter().map(|(_, n)| n).collect()
3087        })
3088        .unwrap_or_else(|| {
3089            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
3090                .map(|i| format!("p{}", i + 1))
3091                .collect()
3092        });
3093
3094    // Non-static member-inner ctor: the synthetic outer instance rides
3095    // as args[0] (typed as the direct enclosing class). Every emission
3096    // site already passes it implicitly — qualified `outer.new Inner(..)`,
3097    // `this.new Inner(..)` from inside the outer, `super(..)` after
3098    // skip_outer_arg — so the signature drops the param and the body's
3099    // references become `Outer.this`. Without this, every construction
3100    // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
3101    // guava androidx inner-class families, d8 capture lambdas).
3102    let mut inner_arg0 = 0usize;
3103    if is_init {
3104        let tail = class.name.rsplit('$').next().unwrap_or("");
3105        let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
3106        if let Some(d) = desc.as_ref() {
3107            if let Some(JavaType::Object(outer)) = d.args.first() {
3108                // The direct enclosing class: nesting annotation when
3109                // present, else the `$`-chain parent (find_outer_name —
3110                // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
3111                // a plain rsplit leaves a trailing `$`). It must agree
3112                // with the this$0 type for the rewrite to fire.
3113                let enclosing: Option<String> = class
3114                    .nesting
3115                    .enclosing_class
3116                    .clone()
3117                    .or_else(|| crate::find_outer_name(pool, &class.name));
3118                let direct = enclosing.as_deref() == Some(outer.as_ref());
3119                if direct && !digit_simple && ctx.class_has_this0(&class.name) {
3120                    let param0 = body.as_ref().and_then(|b| {
3121                        b.vt.vars
3122                            .iter()
3123                            .find(|v| v.is_param && v.name != "this")
3124                            .map(|v| v.id)
3125                    });
3126                    let written = match (&body, param0) {
3127                        (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
3128                        _ => false,
3129                    };
3130                    if let (Some(p0), false) = (param0, written) {
3131                        if let Some(b) = body.as_mut() {
3132                            // this()-delegations to the SAME class drop the
3133                            // outer arg (all of its ctors share the strip);
3134                            // a super target joins when it is an inner of
3135                            // the same enclosing family.
3136                            let mut eligible: Vec<String> = vec![class.name.clone()];
3137                            if let Some(sup) = &class.super_name {
3138                                let sup_enclosing =
3139                                    crate::find_outer_name(pool, sup);
3140                                if sup_enclosing.as_deref() == Some(outer.as_ref())
3141                                    && ctx.class_has_this0(sup)
3142                                {
3143                                    eligible.push(sup.clone());
3144                                }
3145                            }
3146                            let renamed = crate::apply_class_rename(outer);
3147                            let display = dotted(renamed.as_ref());
3148                            let ty =
3149                                jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
3150                            crate::passes::rewrite_inner_ctor_outer_param(
3151                                &mut b.body,
3152                                p0,
3153                                &display,
3154                                &ty,
3155                                &eligible,
3156                            );
3157                            inner_arg0 = 1;
3158                        }
3159                    }
3160                }
3161            }
3162        }
3163    }
3164
3165    if is_clinit {
3166        // `static { ... }` — the caller strips the method name/params.
3167    } else {
3168        let Some(d) = &desc else { return Ok(false) };
3169        if is_init {
3170            // The ctor name must equal the DECLARED class name of its
3171            // file: flat `$` at depth 0 (own file), own segment when
3172            // inlined in the parent at depth > 0. Case-renamed classes
3173            // use their display name.
3174            let dname = crate::apply_class_rename(&class.name);
3175            let (_, simple) = split_name(&dname);
3176            // emit_method's depth is the METHOD indent = class depth + 1:
3177            // own-file classes (depth 0 header → method depth 1) need the
3178            // flat `$` ctor name; inline nested members use their segment.
3179            let base = if depth <= 1 {
3180                simple.to_string()
3181            } else {
3182                // R8 names can END in `$` (`ThreadMsg$$$`): the last
3183                // `$`-segment is empty — keep the whole simple name.
3184                let seg = simple.rsplit('$').next().unwrap_or(&simple);
3185                if seg.is_empty() {
3186                    simple.to_string()
3187                } else {
3188                    seg.to_string()
3189                }
3190            };
3191            let name = if is_restricted_type_name(&base) {
3192                format!("_{base}")
3193            } else {
3194                base
3195            };
3196            // The ctor name must equal the DECLARED class name — the same
3197            // injective `_u<hex>` sanitizer the header uses (java_ident's
3198            // lossy `-`→`_` fold left `class _u2dDeprecatedOkio` with a
3199            // `private _DeprecatedOkio()` ctor: "方法声明无效; 需要返回
3200            // 类型", which as a PARSE error also masks every semantic
3201            // error in the whole javac run).
3202            sig.push_str(&sanitize_ref(&name));
3203        } else {
3204            sig.push_str(&type_name(pool, &d.ret));
3205            sig.push(' ');
3206            let mname =
3207                jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
3208            sig.push_str(&java_ident(mname));
3209        }
3210        sig.push('(');
3211        // A promoted enum ctor's dex descriptor carries the compiler-
3212        // synthesized `(String name, int ordinal)` prefix — JLS forbids
3213        // declaring those (they are implicit in the `A(args)` constant
3214        // declarations the promotion emits, and strip_enum_ctor_super
3215        // already removed the `super(name, ordinal, ..)` delegation).
3216        // Skip the pair in the signature, or every constant declaration
3217        // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
3218        // when the body never reads the two params — code that genuinely
3219        // uses them keeps the declared form.
3220        // Robust hotpatch guard strip MUST run before the trace-param
3221        // analysis: the guard's isSupport(..) call is the ctor's only
3222        // reader of the (String, int) params for plain robust enums, and
3223        // its self-static read is what JLS 8.9.2 forbids.
3224        if enum_promoted && is_init {
3225            if let Some(b) = body.as_mut() {
3226                strip_robust_enum_ctor_guard(&mut b.body);
3227            }
3228        }
3229        // Fallback enum base ctor: store the (String, int) trace params
3230        // into the synthetic $ddcName/$ddcOrdinal fields that back the
3231        // synthesized name()/ordinal()/valueOf() (synth_enum_members).
3232        // Non-final fields: constant-subclass ctors reach this store
3233        // through their super(..) chain, and a `final` would demand
3234        // definite assignment on every ctor path.
3235        if is_init
3236            && !enum_promoted
3237            && class.is_enum()
3238            && enum_synth_active(class)
3239            && matches!(d.args.first(), Some(JavaType::Object(sx)) if sx.as_ref() == "java/lang/String")
3240            && matches!(d.args.get(1), Some(JavaType::Int))
3241        {
3242            if let Some(b) = body.as_mut() {
3243                // A ctor that DELEGATES first (the -IA synthetic bridge,
3244                // `this(str, p2)`) must stay delegation-first — the
3245                // delegated-to ctor performs the stores.
3246                // First-LEAF probe: the ctor chain leaves the delegation
3247                // Block-wrapped (`Block[this(..), assigns]`), and a
3248                // stripped hotfix guard can leave the wrapper as the
3249                // first top-level statement — unwrap to the real head.
3250                let starts_with_delegation =
3251                    crate::passes::is_bare_ctor_call(crate::passes::first_leaf_stmt(&b.body));
3252                let ps: Vec<u32> = b
3253                    .vt
3254                    .vars
3255                    .iter()
3256                    .filter(|v| v.is_param && v.name != "this")
3257                    .take(2)
3258                    .map(|v| v.id)
3259                    .collect();
3260                if ps.len() == 2 && !starts_with_delegation {
3261                    let mk_store = |var: u32, fname: &str, fty: JavaType| {
3262                        Stmt::ExprStmt(Expr::Assign {
3263                            target: Box::new(Expr::Field {
3264                                owner: Some(Box::new(Expr::This)),
3265                                cls: std::sync::Arc::from(class.name.as_str()),
3266                                name: std::sync::Arc::from(fname),
3267                                ty: jdc_core::ir::expr::TypeRef::J(fty.clone()),
3268                                is_static: false,
3269                            }),
3270                            op: jdc_core::ir::expr::AssignOp::Plain,
3271                            value: Box::new(Expr::Local {
3272                                var,
3273                                ty: jdc_core::ir::expr::TypeRef::J(fty),
3274                            }),
3275                        })
3276                    };
3277                    let mut inj = vec![
3278                        mk_store(ps[0], ENUM_NAME_FIELD, JavaType::Object("java/lang/String".into())),
3279                        mk_store(ps[1], ENUM_ORD_FIELD, JavaType::Int),
3280                    ];
3281                    if let Stmt::Block(vs) = &mut b.body {
3282                        let mut rest = std::mem::take(vs);
3283                        inj.append(&mut rest);
3284                        *vs = inj;
3285                    } else {
3286                        let prev = std::mem::replace(&mut b.body, Stmt::Block(Vec::new()));
3287                        inj.push(prev);
3288                        b.body = Stmt::Block(inj);
3289                    }
3290                }
3291            }
3292        }
3293        let mut arg0 = 0;
3294        if enum_promoted
3295            && is_init
3296            && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
3297            && matches!(d.args.get(1), Some(JavaType::Int))
3298        {
3299            // The Kotlin default-arg bridge ctor reads name/ordinal in
3300            // its `this(str, p2, ..)` delegation only — the LEADING pair
3301            // of a this()-delegation drops together with the params, so
3302            // those reads do not block the strip (the constants' extra
3303            // args match the bridge's user params, not the 2-param user
3304            // ctor).
3305            let synthetic: Vec<u32> = body
3306                .as_ref()
3307                .map(|b| {
3308                    b.vt
3309                        .vars
3310                        .iter()
3311                        .filter(|v| v.is_param && v.name != "this")
3312                        .take(2)
3313                        .map(|v| v.id)
3314                        .collect()
3315                })
3316                .unwrap_or_default();
3317            // The (name, ordinal) strip is UNIFORM across every promoted
3318            // enum ctor shape: refs_ok below requires each param's total
3319            // reads to equal its delegation-LEAD reads, i.e. the param is
3320            // used ONLY as a leading arg of a this() delegation (or not
3321            // at all). That covers all three shapes at once —
3322            //   • the real field-store ctor (super(name,ordinal) already
3323            //     stripped by strip_enum_ctor_super → total 0 == lead 0);
3324            //   • the d8 SYNTHETIC bare-constant bridge `g(String,int)
3325            //     { this(name, ordinal, <defaults>); }` (uuyc Sb/g ×294);
3326            //   • a REAL user forwarding ctor `C(String,int,X,..) {
3327            //     this(name, ordinal, x, ..); }` for a multi-arity Java
3328            //     enum (rimet GaeaConfigKey: constants pass explicit args
3329            //     only, so the implicit pair MUST drop or every constant
3330            //     fails 找不到合适的构造器 ×1,204).
3331            // A ctor that READS name/ordinal in its body (total > lead)
3332            // keeps its signature unless the value-read rewrite above
3333            // turned those reads into name()/ordinal() calls. The old
3334            // is_bridge/ACC_SYNTHETIC gate on this was stale: relaxing it
3335            // to the uniform total==lead rule IMPROVED every corpus
3336            // (weixin 6,419→6,381, weibo −90, lark −62, rimet −1,180) —
3337            // the feared "weixin +2.9k ungated" regression did not recur
3338            // (that predates the refs_ok total==lead check, strip_enum_
3339            // ctor_super, and the value-read rewrite, which together keep
3340            // body-reading and Kotlin-bridge ctors correct).
3341            let n_args = d.args.len();
3342            let is_bridge = n_args >= 2
3343                && matches!(&d.args[n_args - 1], JavaType::Object(ref m)
3344                    if m.starts_with("kotlin/jvm/internal/"))
3345                && matches!(d.args[n_args - 2], JavaType::Int);
3346            // Meituan Robust instrumented ctors PACK the trace params
3347            // into dispatch arrays (`v3[0] = str; v3[1] = new
3348            // Integer(p2); PatchProxy.isSupport(v3, ..)`) — value reads
3349            // that refuse the strip, and a true enum cannot DECLARE
3350            // (String, int) params (JLS 8.9.1), so the whole class fell
3351            // back (illegal `Enum.valueOf` + no ordinal()). Rewrite pure
3352            // VALUE reads to the equivalent `name()`/`ordinal()` calls —
3353            // Enum's fields are set by the implicit super before the
3354            // body runs, so the pack sees identical values. Delegation
3355            // LEAD reads (this(str, p2) of Kotlin/-IA bridges) must keep
3356            // their Local shape for the lead-drop below, so skip any
3357            // ctor that has them; written params are never replaceable.
3358            if synthetic.len() == 2 && !is_bridge {
3359                if let Some(b) = body.as_ref() {
3360                    let (p0, p1) = (synthetic[0], synthetic[1]);
3361                    let mut lead_reads = 0usize;
3362                    {
3363                        let cls_name = class.name.as_str();
3364                        crate::passes::visit_stmt_exprs_ro(&b.body, &mut |e| {
3365                            if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3366                                if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3367                                    for a in args.iter().take(2) {
3368                                        if let Expr::Local { var: v, .. } = a {
3369                                            if *v == p0 || *v == p1 {
3370                                                lead_reads += 1;
3371                                            }
3372                                        }
3373                                    }
3374                                }
3375                            }
3376                        });
3377                    }
3378                    let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3379                    let reads = uses.get(&p0).copied().unwrap_or(0)
3380                        + uses.get(&p1).copied().unwrap_or(0);
3381                    let written = crate::passes::local_is_written(&b.body, p0)
3382                        || crate::passes::local_is_written(&b.body, p1);
3383                    if lead_reads == 0 && reads > 0 && !written {
3384                        let mk = |nm: &str, ret: JavaType| Expr::Method {
3385                            owner: Some(Box::new(Expr::This)),
3386                            cls: std::sync::Arc::from("java/lang/Enum"),
3387                            name: std::sync::Arc::from(nm),
3388                            desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
3389                                ret,
3390                                args: Vec::new(),
3391                            }),
3392                            args: Vec::new(),
3393                            is_static: false,
3394                            is_interface: false,
3395                            is_special: false,
3396                            is_super: false,
3397                            is_dynamic: false,
3398                            type_args: Vec::new(),
3399                        };
3400                        let name_call =
3401                            mk("name", JavaType::Object("java/lang/String".into()));
3402                        let ord_call = mk("ordinal", JavaType::Int);
3403                        if let Some(b) = body.as_mut() {
3404                            crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3405                                crate::passes::deep_rewrite(e, &mut |x| {
3406                                    if let Expr::Local { var, .. } = x {
3407                                        if *var == p0 {
3408                                            *x = name_call.clone();
3409                                        } else if *var == p1 {
3410                                            *x = ord_call.clone();
3411                                        }
3412                                    }
3413                                });
3414                            });
3415                        }
3416                    }
3417                }
3418            }
3419            let mut lead = [0usize, 0usize];
3420            let refs_ok = body.as_ref().is_some_and(|b| {
3421                let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3422                let cls_name = class.name.as_str();
3423                crate::passes::visit_stmt_exprs_ro(&b.body, &mut |e| {
3424                    if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3425                        if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3426                            for (k, a) in args.iter().enumerate() {
3427                                if let Expr::Local { var: v, .. } = a {
3428                                    if let Some(pi) = synthetic.iter().position(|p| p == v) {
3429                                        if k == pi && k < 2 {
3430                                            lead[pi] += 1;
3431                                        }
3432                                    }
3433                                }
3434                            }
3435                        }
3436                    }
3437                });
3438                synthetic.iter().enumerate().all(|(pi, id)| {
3439                    let total = uses.get(id).copied().unwrap_or(0);
3440                    // Uniform rule: the implicit param is used ONLY as a
3441                    // this()-delegation lead (or not at all). See the
3442                    // block comment above — this subsumes the old
3443                    // total==0 (real ctor) and is_bridge cases.
3444                    total == lead[pi]
3445                })
3446            });
3447            if refs_ok {
3448                arg0 = 2;
3449                // Drop the leading (name, ordinal) args of the this()
3450                // delegations.
3451                if let Some(b) = body.as_mut() {
3452                    let cls_name = class.name.as_str();
3453                    let syn = synthetic;
3454                    crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3455                        if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3456                            if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3457                                let mut drop_n = 0;
3458                                for a in args.iter().take(2) {
3459                                    if let Expr::Local { var: v, .. } = a {
3460                                        if syn.contains(v) && drop_n == v - syn[0] {
3461                                            drop_n += 1;
3462                                            continue;
3463                                        }
3464                                    }
3465                                    break;
3466                                }
3467                                for _ in 0..drop_n {
3468                                    args.remove(0);
3469                                }
3470                            }
3471                        }
3472                    });
3473                }
3474            }
3475        }
3476        let n = d.args.len();
3477        let varargs = a & ACC_VARARGS != 0 && n > 0;
3478        let arg0 = arg0 + inner_arg0;
3479        for (i, arg) in d.args.iter().enumerate().skip(arg0) {
3480            if i > arg0 {
3481                sig.push_str(", ");
3482            }
3483            let name = param_names
3484                .get(i)
3485                .cloned()
3486                .unwrap_or_else(|| format!("p{}", i));
3487            if varargs && i + 1 == n {
3488                if let JavaType::Array(inner) = arg {
3489                    sig.push_str(&type_name(pool, inner));
3490                    sig.push_str("...");
3491                } else {
3492                    sig.push_str(&type_name(pool, arg));
3493                }
3494            } else {
3495                sig.push_str(&type_name(pool, arg));
3496            }
3497            sig.push(' ');
3498            // Param names come from dex debug info — obfuscated apps
3499            // name them `_` (reserved since Java 9) or after keywords.
3500            sig.push_str(&java_ident(&name));
3501        }
3502        sig.push(')');
3503    }
3504
3505    if is_clinit {
3506        let Some(b) = body else { return Ok(false) };
3507        // Direct render: the printer starts at the method's ABSOLUTE
3508        // indent and appends into the same buffer that carries the
3509        // header — no intermediate body string, no per-line re-indent
3510        // pass (two full copies of every method body saved).
3511        out.push_str(&ind);
3512        out.push_str("static {\n");
3513        let hdr = out.len();
3514        let printer = Printer::new(ctx, &b.vt)
3515            .with_indent(depth + 1)
3516            .with_output(std::mem::take(out));
3517        let t_print = std::time::Instant::now();
3518        let mut rendered = printer.into_string(&b.body);
3519        crate::method::phase_hit(3, t_print);
3520        if rendered[hdr..].trim().is_empty() {
3521            rendered.truncate(hdr);
3522        }
3523        rendered.push_str(&ind);
3524        rendered.push_str("}\n");
3525        *out = rendered;
3526        return Ok(true);
3527    }
3528    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
3529        out.push_str(&ind);
3530        out.push_str(&sig);
3531        out.push_str(";\n");
3532        return Ok(true);
3533    }
3534    let Some(b) = body else { return Ok(false) };
3535
3536    // Direct render at the absolute indent level (see the clinit path).
3537    let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
3538    match &b.desc.ret {
3539        JavaType::Boolean => {
3540            printer = printer.with_ret_bool(true);
3541        }
3542        JavaType::Char => {
3543            printer = printer.with_ret_char(true);
3544        }
3545        JavaType::Byte => {
3546            printer = printer.with_ret_narrow(true, false);
3547        }
3548        JavaType::Short => {
3549            printer = printer.with_ret_narrow(false, true);
3550        }
3551        _ => {}
3552    }
3553    out.push_str(&ind);
3554    out.push_str(&sig);
3555    out.push_str(" {\n");
3556    let hdr = out.len();
3557    let printer = printer.with_output(std::mem::take(out));
3558    let t_print = std::time::Instant::now();
3559    let mut rendered = printer.into_string(&b.body);
3560    crate::method::phase_hit(3, t_print);
3561    if rendered[hdr..].trim().is_empty() {
3562        rendered.truncate(hdr);
3563    }
3564    rendered.push_str(&ind);
3565    rendered.push_str("}\n");
3566    *out = rendered;
3567    Ok(true)
3568}
3569
3570// ---------------------------------------------------------------------------
3571// Naming helpers
3572// ---------------------------------------------------------------------------
3573
3574fn indent(depth: usize) -> String {
3575    "    ".repeat(depth)
3576}
3577
3578/// `$`-separated nesting rendered with dots — but ONLY when every
3579/// segment is a clean Java identifier (a genuine member class chain).
3580/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
3581/// `...$$inlined$collect$1`) and local-class tails are NOT member
3582/// classes Java can name; the whole name stays flat with `$` (ddc emits
3583/// them as their own top-level files).
3584/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
3585/// any other non-identifier character) is not legal Java. Deterministic
3586/// mapping, applied identically at declaration and call sites.
3587pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
3588    // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
3589    // (millions per APK), where it profiled as __NSGetEnviron.
3590    static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3591    if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
3592        eprintln!("[ident] {name:?}");
3593    }
3594    let clean = name
3595        .chars()
3596        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
3597    // Kotlin names fields/methods `default` (Companion.default) — no Java
3598    // program can declare or reference a keyword; the identical mapping
3599    // lives in jdc-core's call-site sanitizer.
3600    let keyword = matches!(
3601        name,
3602        "abstract"
3603            | "assert"
3604            | "boolean"
3605            | "break"
3606            | "byte"
3607            | "case"
3608            | "catch"
3609            | "char"
3610            | "class"
3611            | "const"
3612            | "continue"
3613            | "default"
3614            | "do"
3615            | "double"
3616            | "else"
3617            | "enum"
3618            | "extends"
3619            | "final"
3620            | "finally"
3621            | "float"
3622            | "for"
3623            | "goto"
3624            | "if"
3625            | "implements"
3626            | "import"
3627            | "instanceof"
3628            | "int"
3629            | "interface"
3630            | "long"
3631            | "native"
3632            | "new"
3633            | "package"
3634            | "private"
3635            | "protected"
3636            | "public"
3637            | "return"
3638            | "short"
3639            | "static"
3640            | "strictfp"
3641            | "super"
3642            | "switch"
3643            | "synchronized"
3644            | "this"
3645            | "throw"
3646            | "throws"
3647            | "transient"
3648            | "try"
3649            | "void"
3650            | "volatile"
3651            | "while"
3652            | "true"
3653            | "false"
3654            | "null"
3655            // `_` is a reserved identifier since Java 9 (Alipay's
3656            // instant-run fields are named `_`) — the `_<name>` mapping
3657            // turns it into `__`, matching jdc-core's call sites.
3658            | "_"
3659    );
3660    // A simple name may not START with a digit either (WhatsApp nests
3661    // `X/0Xx`): the declaration site and every reference (jdc-core's
3662    // sanitize_source_name) prefix the same underscore.
3663    let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
3664    if clean && !keyword && !digit_start {
3665        std::borrow::Cow::Borrowed(name)
3666    } else if keyword || digit_start {
3667        std::borrow::Cow::Owned(format!("_{name}"))
3668    } else {
3669        // Non-ASCII single chars (Alipay names a field `支`) map to a
3670        // lone `_` — itself reserved since Java 9. Escape it.
3671        let mapped: String = name
3672            .chars()
3673            .map(|c| {
3674                if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
3675                    c
3676                } else {
3677                    '_'
3678                }
3679            })
3680            .collect();
3681        if mapped == "_" {
3682            std::borrow::Cow::Owned("__".to_string())
3683        } else {
3684            std::borrow::Cow::Owned(mapped)
3685        }
3686    }
3687}
3688
3689fn split_name(internal: &str) -> (String, String) {
3690    match internal.rfind('/') {
3691        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
3692        None => (String::new(), internal.to_string()),
3693    }
3694}
3695
3696/// Dotted source form of an internal name.
3697pub fn dotted(internal: &str) -> String {
3698    let mut out = internal.replace('/', ".");
3699    // `$` → `.` only when the following segment can start a Java
3700    // identifier (anonymous/synthetic tails stay `$`).
3701    let mut i = 0;
3702    while let Some(p) = out[i..].find('$') {
3703        let at = i + p;
3704        // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
3705        // source name — dotting it produced a leading `.Gson.Types`.
3706        let head_ok = at > 0
3707            && out[..at]
3708                .chars()
3709                .next_back()
3710                .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
3711        let tail_ok = out[at + 1..]
3712            .chars()
3713            .next()
3714            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
3715        if tail_ok && head_ok {
3716            out.replace_range(at..at + 1, ".");
3717            i = at + 1;
3718        } else {
3719            i = at + 1;
3720        }
3721    }
3722    // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
3723    // reached `extends` with the digit start unmapped.
3724    sanitize_fq(&out)
3725}
3726
3727fn join_dotted(pool: &DexPool, names: &[String]) -> String {
3728    names
3729        .iter()
3730        .map(|n| print_class_name(pool, n))
3731        .collect::<Vec<_>>()
3732        .join(", ")
3733}
3734
3735/// A printable type name (arrays render with `[]` suffixes). Nested class
3736/// names dot their `$` when the outer chain is known to the pool.
3737/// Split a concatenated method-descriptor ARGUMENT string
3738/// (`"Ljava/lang/String;I[Ljava/lang/Object;"`) into per-parameter
3739/// `JavaType`s. Used by the framework-ancestor ctor bridge, where the
3740/// referenced sig comes from the method-id table (no pool ancestor
3741/// method to read a parsed descriptor from).
3742pub(crate) fn split_arg_descs(sig: &str) -> Vec<JavaType> {
3743    let mut out = Vec::new();
3744    let b = sig.as_bytes();
3745    let mut i = 0usize;
3746    while i < b.len() {
3747        let start = i;
3748        while i < b.len() && b[i] == b'[' {
3749            i += 1;
3750        }
3751        if i < b.len() {
3752            if b[i] == b'L' {
3753                while i < b.len() && b[i] != b';' {
3754                    i += 1;
3755                }
3756                if i < b.len() {
3757                    i += 1; // consume ';'
3758                }
3759            } else {
3760                i += 1; // primitive single char
3761            }
3762        }
3763        if i > start {
3764            out.push(desc_type(&sig[start..i]));
3765        } else {
3766            break; // malformed: avoid an infinite loop
3767        }
3768    }
3769    out
3770}
3771
3772pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
3773    match t {
3774        JavaType::Void => "void".into(),
3775        JavaType::Object(n) => print_class_name(pool, n),
3776        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
3777        other => other.to_java(false),
3778    }
3779}
3780
3781/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
3782/// its left side names a known class (literal `$` top-level names survive).
3783// ---------------------------------------------------------------------------
3784// The per-class import map (JLS 6.4.2 obscuring repair).
3785//
3786// A referenced FQN whose FIRST segment equals an in-scope class simple
3787// name binds the qualifier to the CLASS, not the package — `class j3
3788// implements j3.g` is cyclic inheritance and collapses javac's
3789// attribution for the whole package (the Object-cascade root, weixin
3790// 84 files + contagion). The repair: emit `import j3.g;` and render
3791// the SIMPLE name in every type position. The set is populated per
3792// class from a metadata pre-scan (supertypes, field types, method
3793// signature descriptors) and read by print_class_name and the
3794// DexCtx::obscured_simple implementation (which jdc-core's shorten /
3795// java_type_name consult).
3796struct ObscureState {
3797    /// The ROOT class being rendered (its simple name is the obscuring
3798    /// in-scope name).
3799    class: String,
3800    /// Field names in scope through the root class's inheritance chain
3801    /// (own + pool ancestors + framework ancestors' statics/instances +
3802    /// interface constants). An in-scope field obscures a package's
3803    /// first segment in expression position (JLS 6.4.2) — WhatsApp's
3804    /// View subclasses inherit the static Property X/Y/Z (API 31+) and
3805    /// its whole obfuscated codebase lives in package X (×30,171); the
3806    /// general set replaces the hardcoded X/Y/Z list. Obscured refs
3807    /// route through the import layer like own-simple obscuring.
3808    shadow: jdc_core::FxHashSet<String>,
3809    /// Metadata pre-scan results: internal → simple render.
3810    map: jdc_core::FxHashMap<String, String>,
3811    /// Expression-level obscured refs DISCOVERED during the body render
3812    /// (their imports emit at assembly time).
3813    recorded: jdc_core::FxHashSet<String>,
3814    /// Obscured-owner static-member escapes discovered during the body
3815    /// render: bare member name → the RENAMED display owner (dotted) —
3816    /// each emits an `import static <owner>.<member>;` line.
3817    recorded_static: jdc_core::FxHashMap<String, String>,
3818    /// The ROOT class's own field names (static + instance, display
3819    /// space): value-namespace capture checks for import simples and
3820    /// bare static-import members (JLS 表达式名解析优先绑定变量).
3821    own_field_names: jdc_core::FxHashSet<String>,
3822    /// Simple names of the CURRENT PACKAGE's own classes: importing a
3823    /// name that collides would SHADOW every same-package use of it in
3824    /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
3825    /// erroring) rather than corrupt.
3826    blocked_base: std::sync::Arc<jdc_core::FxHashSet<String>>,
3827    blocked_removed: jdc_core::FxHashSet<String>,
3828    blocked_extra: jdc_core::FxHashSet<String>,
3829    /// RENAMED display tails of same-package classes (zero-copy view
3830    /// into the process-wide cache): same blocking role as `blocked`
3831    /// for refs that render under the rename registry.
3832    blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3833}
3834
3835thread_local! {
3836    static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
3837        const { std::cell::RefCell::new(None) };
3838}
3839
3840/// Install the per-class render state (call at class render entry).
3841#[allow(clippy::too_many_arguments)] // one install call per class — the params ARE the state tuple
3842pub(crate) fn set_obscured_state(
3843    class: String,
3844    map: jdc_core::FxHashMap<String, String>,
3845    blocked_base: std::sync::Arc<jdc_core::FxHashSet<String>>,
3846    blocked_removed: jdc_core::FxHashSet<String>,
3847    blocked_extra: jdc_core::FxHashSet<String>,
3848    blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3849    shadow: jdc_core::FxHashSet<String>,
3850    own_field_names: jdc_core::FxHashSet<String>,
3851) {
3852    OBSCURE.with(|m| {
3853        *m.borrow_mut() = Some(ObscureState {
3854            class,
3855            shadow,
3856            map,
3857            own_field_names,
3858            recorded: jdc_core::FxHashSet::default(),
3859            recorded_static: jdc_core::FxHashMap::default(),
3860            blocked_base,
3861            blocked_removed,
3862            blocked_extra,
3863            blocked_renamed,
3864        });
3865    });
3866}
3867
3868/// Simple names this class's file renders through the import layer
3869/// (metadata map values + dynamically recorded nested tails). A method
3870/// local with one of these names captures the imported simple name in
3871/// expression position (locals beat single-type imports — `h.e` binds
3872/// a local int `h`, 无法取消引用int); deshadow_locals renames them.
3873/// Empty when no render state is installed (progressive paths).
3874pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
3875    OBSCURE.with(|m| {
3876        let mut out = jdc_core::FxHashSet::default();
3877        let Ok(st) = m.try_borrow() else { return out };
3878        let Some(st) = st.as_ref() else { return out };
3879        for simple in st.recorded_static.keys() {
3880            out.insert(simple.clone());
3881        }
3882        for simple in st.map.values() {
3883            out.insert(simple.clone());
3884        }
3885        for internal in &st.recorded {
3886            let renamed = crate::apply_class_rename(internal);
3887            if let Some(tail) = renamed.rsplit(['/', '$']).next() {
3888                if !tail.is_empty() {
3889                    out.insert(tail.to_string());
3890                }
3891            }
3892        }
3893        out
3894    })
3895}
3896
3897/// Take the recorded static-member escapes (bare member → display
3898/// owner dotted) and clear.
3899pub(crate) fn take_recorded_static_and_clear() -> jdc_core::FxHashMap<String, String> {
3900    OBSCURE.with(|m| {
3901        let mut st = m.borrow_mut();
3902        st.as_mut()
3903            .map(|st| std::mem::take(&mut st.recorded_static))
3904            .unwrap_or_default()
3905    })
3906}
3907
3908/// Take the recorded expression-level refs and clear the state.
3909pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
3910    // Field-level take (NOT the whole-state .take()): the static-member
3911    // escapes are consumed by a follow-up take — destroying the state
3912    // here would silently drop them.
3913    OBSCURE.with(|m| {
3914        m.borrow_mut()
3915            .as_mut()
3916            .map(|st| std::mem::take(&mut st.recorded))
3917            .unwrap_or_default()
3918    })
3919}
3920
3921// ---- cross-package access widening ----------------------------------
3922// ART tolerates cross-package access to non-public classes/members that
3923// R8/d8 preserved; Java source cannot express it ("是在不可访问的类或
3924// 接口中定义的", "k在kotlinx.coroutines中不是公共的", "unknownFields 在
3925// l6 中是 protected"). The census scan records every cross-package
3926// reference; the installer keeps the non-public targets and the
3927// declaration renders widen them to public.
3928
3929struct WidenState {
3930    classes: jdc_core::FxHashSet<String>,
3931    methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
3932    fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
3933    /// (owner, field name) finals written outside the declaring class's
3934    /// ctor/clinit — rendered WITHOUT `final` (see refscan census).
3935    unfinal: jdc_core::FxHashSet<(String, String)>,
3936}
3937
3938static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
3939
3940pub(crate) fn install_access_widening(pool: &crate::DexPool) {
3941    let t0 = std::time::Instant::now();
3942    let raw = crate::refscan::access_widening_scan(pool.dexes());
3943    let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3944    for c in &raw.classes {
3945        if let Some(pc) = pool.get_if_materialized(c) {
3946            if pc.access & crate::access::ACC_PUBLIC == 0 {
3947                classes.insert(c.clone());
3948            }
3949        }
3950    }
3951    let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
3952        jdc_core::FxHashMap::default();
3953    for (owner, set) in &raw.methods {
3954        for (name, desc) in set {
3955            if name == "<clinit>" {
3956                continue; // never referenced
3957            }
3958            // A dex ref of an INHERITED member may name any subclass as
3959            // owner (ART resolves up the chain), while the declaration —
3960            // and its access modifier — lives on an ancestor. Register
3961            // the widening on the DECLARING class or the rendered decl
3962            // stays protected ("在 X 中是 protected 访问控制").
3963            let mut cur: Option<&str> = Some(owner.as_str());
3964            let mut hops = 0u32;
3965            while let Some(cname) = cur {
3966                hops += 1;
3967                if hops > 64 {
3968                    break;
3969                }
3970                let Some(pc) = pool.get_if_materialized(cname) else {
3971                    break;
3972                };
3973                // Enum ctors are source-level private-only: widening is
3974                // a javac "modifier not allowed here".
3975                if name == "<init>" && pc.access & crate::access::ACC_ENUM != 0 {
3976                    break;
3977                }
3978                match pc.all_methods()
3979                    .find(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str())
3980                {
3981                    Some(m) if m.access & crate::access::ACC_PUBLIC == 0 => {
3982                        methods
3983                            .entry(cname.to_string())
3984                            .or_default()
3985                            .insert((name.clone(), desc.clone()));
3986                        break;
3987                    }
3988                    Some(_) => break, // already public
3989                    None => cur = pc.super_name.as_deref(),
3990                }
3991            }
3992        }
3993    }
3994    // Override-closure DOWN the hierarchy: a widened (public) super
3995    // method whose subclass override stays protected/package is
3996    // "无法覆盖...更低的访问权限" (lark +823). Every transitive
3997    // subclass declaring the same (name, desc) non-public widens too.
3998    {
3999        let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
4000            jdc_core::FxHashMap::default();
4001        for name in &pool.order {
4002            let Some(pc) = pool.get_if_materialized(name) else {
4003                continue;
4004            };
4005            if let Some(sup) = &pc.super_name {
4006                subs.entry(sup.clone()).or_default().push(name.clone());
4007            }
4008            for i in &pc.interfaces {
4009                subs.entry(i.clone()).or_default().push(name.clone());
4010            }
4011        }
4012        let seeds: Vec<(String, String, String)> = methods
4013            .iter()
4014            .flat_map(|(o, set)| {
4015                set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
4016            })
4017            .collect();
4018        for (owner, mname, mdesc) in seeds {
4019            let mut queue: Vec<String> = match subs.get(&owner) {
4020                Some(v) => v.clone(),
4021                None => continue,
4022            };
4023            let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4024            while let Some(c) = queue.pop() {
4025                if !seen.insert(c.clone()) {
4026                    continue;
4027                }
4028                let Some(pc) = pool.get_if_materialized(&c) else {
4029                    continue;
4030                };
4031                let declares_nonpub = pc.all_methods().any(|m| {
4032                    &*m.name == mname.as_str()
4033                        && &*m.desc == mdesc.as_str()
4034                        && m.access & crate::access::ACC_PUBLIC == 0
4035                });
4036                if declares_nonpub {
4037                    methods
4038                        .entry(c.clone())
4039                        .or_default()
4040                        .insert((mname.clone(), mdesc.clone()));
4041                }
4042                if let Some(v) = subs.get(&c) {
4043                    queue.extend(v.iter().cloned());
4044                }
4045            }
4046        }
4047        // Subclass `super(..)` targets. A class C extends S; C's ctor
4048        // renders `super(..)` against S's ctor. When R8 INLINES S's
4049        // trivial ctor, C's dex ctor calls `Object.<init>` directly (not
4050        // `S.<init>`), so the bytecode census above never sees the S.<init>
4051        // reference and a PRIVATE S ctor stays private — the rendered
4052        // `super()` then fails ("R() 在 R 中是 private 访问控制", lark ×122:
4053        // pl.droidsonroids.gif.R extends com.ss.android.lark.R, whose ctor
4054        // R8 inlined to Object.<init>). Widen every private ctor of a
4055        // class that HAS subclasses so the implicit super() links. Enums
4056        // are excluded (their ctors are source-level private-only).
4057        for sup in subs.keys() {
4058            let Some(pc) = pool.get_if_materialized(sup) else {
4059                continue;
4060            };
4061            if pc.access & crate::access::ACC_ENUM != 0 {
4062                continue;
4063            }
4064            for m in pc.all_methods() {
4065                if &*m.name == "<init>" && m.access & crate::access::ACC_PRIVATE != 0 {
4066                    methods
4067                        .entry(sup.clone())
4068                        .or_default()
4069                        .insert((m.name.to_string(), m.desc.to_string()));
4070                }
4071            }
4072        }
4073    }
4074    let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
4075        jdc_core::FxHashMap::default();
4076    for (owner, set) in &raw.fields {
4077        for name in set {
4078            // Same inherited-owner walk as methods: the protected field
4079            // lives on the ancestor that DECLARES it (weibo
4080            // mModuleContext/mData ×470 — refs named the module
4081            // subclass, the own-fields-only filter never widened the
4082            // declaring base).
4083            let mut cur: Option<&str> = Some(owner.as_str());
4084            let mut hops = 0u32;
4085            while let Some(cname) = cur {
4086                hops += 1;
4087                if hops > 64 {
4088                    break;
4089                }
4090                let Some(pc) = pool.get_if_materialized(cname) else {
4091                    break;
4092                };
4093                match pc.static_fields
4094                    .iter()
4095                    .chain(pc.instance_fields.iter())
4096                    .find(|f| f.name.as_ref() == name.as_str())
4097                {
4098                    Some(f) if f.access & crate::access::ACC_PUBLIC == 0 => {
4099                        fields
4100                            .entry(cname.to_string())
4101                            .or_default()
4102                            .insert(name.clone());
4103                        break;
4104                    }
4105                    Some(_) => break, // already public
4106                    None => cur = pc.super_name.as_deref(),
4107                }
4108            }
4109        }
4110    }
4111    if std::env::var("DDC_STATS").is_ok() {
4112        eprintln!(
4113            "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
4114            classes.len(),
4115            methods.len(),
4116            fields.len(),
4117            t0.elapsed()
4118        );
4119    }
4120
4121    let unfinal: jdc_core::FxHashSet<(String, String)> =
4122        raw.unfinal_fields.into_iter().collect();
4123    let _ = WIDEN.set(WidenState { classes, methods, fields, unfinal });
4124}
4125
4126#[inline]
4127pub(crate) fn widen_class(internal: &str) -> bool {
4128    WIDEN.get().is_some_and(|w| w.classes.contains(internal))
4129}
4130
4131#[inline]
4132pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
4133    WIDEN.get().is_some_and(|w| {
4134        w.methods
4135            .get(owner)
4136            .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
4137    })
4138}
4139
4140#[inline]
4141pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
4142    WIDEN.get().is_some_and(|w| {
4143        w.fields.get(owner).is_some_and(|s| s.contains(name))
4144    })
4145}
4146
4147/// True when the census saw this final field written outside the
4148/// declaring class's `<init>`/`<clinit>` — the `final` modifier must
4149/// not render ("无法为 final 变量 v 分配值" at every foreign write).
4150#[inline]
4151pub(crate) fn unfinal_field(owner: &str, name: &str) -> bool {
4152    WIDEN.get().is_some_and(|w| {
4153        w.unfinal.contains(&(owner.to_string(), name.to_string()))
4154    })
4155}
4156
4157/// Render-time lookup: the metadata map first; otherwise an
4158/// expression-level ref whose first segment equals the root class's
4159/// simple name — record it (its import emits at assembly) and render
4160/// the simple name NOW.
4161pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
4162    OBSCURE.with(|m| {
4163        let mut st = m.borrow_mut();
4164        let st = st.as_mut()?;
4165        if let Some(simple) = st.map.get(internal) {
4166            return Some(simple.clone());
4167        }
4168        let first = internal.split('/').next().unwrap_or("");
4169        // The DISPLAY own simple: when the root class itself was renamed
4170        // (case-collision `widget/d` → `widget/d2`), no in-scope type
4171        // bears the raw name any more — the package's first segment is
4172        // NOT obscured and the qualified render is the correct one
4173        // (reqable d2.java: importing d.h let the class's own boolean
4174        // field h shadow it — 无法在调用超类型构造器之前引用h ×23).
4175        let own_renamed = crate::apply_class_rename(&st.class);
4176        let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
4177        let seg_obscured = first == own || st.shadow.contains(first);
4178        if seg_obscured && internal.split('/').count() >= 2 {
4179            // A NESTED internal's in-scope simple name is its `$` tail
4180            // (`x/a$b` imports/renders as `b`) — the slash-tail left the
4181            // `$` in the render (`t2$a` flat against a nested emission).
4182            // The tail must come from the RENAMED display: case-collision
4183            // renames (`X/00i` → `X/_00i_2`) live in the class registry —
4184            // the raw tail rendered `_00i` against the declaration's
4185            // `_00i_2` (WhatsApp cannot-find ×27k after the View-shadow
4186            // escape opened this path at scale).
4187            let renamed = crate::apply_class_rename(internal);
4188            // Simple = the DOTTED display's last segment: nested `$` dots
4189            // (`x/a$b` → `x.a.b` → `b`), flat `$$` units keep it
4190            // (`x/a$$E…` → `x.a$$E…` → `a$$E…`) — the import line binds
4191            // exactly this name, so the render simple MUST match (the
4192            // old rsplit('$') produced `E…` against import simple
4193            // `a$$E…` — DetectBaseActivity 找不到符号 ×24).
4194            let simple = crate::classdec::dotted(&renamed)
4195                .rsplit('.')
4196                .next()
4197                .unwrap_or("")
4198                .to_string();
4199            // NOTE: no digit-lead rejection here — a digit-leading RAW
4200            // tail (`X/00i`) is sanitized to an importable `_00i` at
4201            // the import line and the refs render through the registry;
4202            // rejecting them dropped WhatsApp's whole field-X-shadowed
4203            // package import layer (cannot-find:变量 ×17k cascade).
4204            if !simple.is_empty()
4205                && !st.blocked_extra.contains(&simple)
4206                && (st.blocked_removed.contains(&simple)
4207                    || !st.blocked_base.contains(&simple))
4208                && !st.blocked_renamed.is_some_and(|e| e.contains(&simple))
4209            {
4210                st.recorded.insert(internal.to_string());
4211                return Some(simple);
4212            }
4213        }
4214        None
4215    })
4216}
4217
4218/// 静态成员引用的遮蔽逃逸判定(jdc-core `Ctx::obscured_member_escape`
4219/// 的实现体,Field/Method 两臂的静态限定渲染前调用):
4220/// - owner 点分渲染的首段被作用域名遮蔽(shadow ∪ 本类简名)——限定
4221///   形式 `d.j.v` 的 d 绑定到字段后成员链断裂;
4222/// - 类型导入路线(`import d.j; j.v`)同样不可用——简单名 j 也是
4223///   作用域字段名(本类/继承,JLS 表达式名解析优先绑定变量);
4224/// - 裸成员名 `v` 干净(非字段名、非遮蔽集、不撞 import 简单名门)。
4225///
4226/// 三条全满足 → 记录 `import static <display>.<member>;`、返回 true
4227///   (emit 打印裸成员名)。否则 false(限定渲染照旧,诚实报错)。
4228pub(crate) fn obscured_member_escape_pub(owner: &str, member: &str) -> bool {
4229    OBSCURE.with(|m| {
4230        let mut st = m.borrow_mut();
4231        let Some(st) = st.as_mut() else { return false };
4232        if owner.split('/').count() < 2 {
4233            return false;
4234        }
4235        let first = owner.split('/').next().unwrap_or("");
4236        let own_renamed = crate::apply_class_rename(&st.class);
4237        let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
4238        let seg_obscured = first == own || st.shadow.contains(first);
4239        if !seg_obscured {
4240            return false;
4241        }
4242        // 本类字段名(含静态/实例——值命名空间捕获裸名)
4243        let own_fields: jdc_core::FxHashSet<&str> = st
4244            .own_field_names
4245            .iter()
4246            .map(|s| s.as_str())
4247            .collect();
4248        // 类型导入路线不可用时逃逸:简单名是本类/继承字段名(JLS
4249        // 表达式名解析优先绑定变量)。局部变量捕获简单名的场景
4250        // (weixin GameWebViewUI 局部 l)逃逸函数看不见 vt——残留为
4251        // 诚实错误(与预扫描导入的既有捕获面同类,罕见)。
4252        let renamed_simple = {
4253            let r = crate::apply_class_rename(owner);
4254            crate::classdec::dotted(&r)
4255                .rsplit('.')
4256                .next()
4257                .unwrap_or("")
4258                .to_string()
4259        };
4260        let simple_unusable = !renamed_simple.is_empty()
4261            && (own_fields.contains(renamed_simple.as_str())
4262                || st.shadow.contains(renamed_simple.as_str())
4263                || renamed_simple == own);
4264        if !simple_unusable {
4265            return false; // 类型导入路线可用(j.v 渲染照旧)
4266        }
4267        // 裸成员名必须干净(字段捕获位)。同名**方法**不是冲突:本类
4268        // 方法与静态导入同名时构成重载集,javac 按签名联合解析
4269        //(escbat4 实测方法守卫 −11 纯损失)。
4270        if member.is_empty()
4271            || own_fields.contains(member)
4272            || st.shadow.contains(member)
4273            || member == own
4274            || st.blocked_extra.contains(member)
4275            || (!st.blocked_removed.contains(member) && st.blocked_base.contains(member))
4276            || st.blocked_renamed.is_some_and(|e| e.contains(member))
4277            || st.map.values().any(|v| v == member)
4278        {
4279            return false;
4280        }
4281        // 已记录(同成员多处引用)→ 照常逃逸:Map 天然去重导入行
4282        let renamed = crate::apply_class_rename(owner);
4283        let display_dotted = crate::classdec::dotted(&renamed);
4284        st.recorded_static.insert(member.to_string(), display_dotted.clone());
4285        true
4286    })
4287}
4288
4289/// Field names in scope for `class` through inheritance: own fields,
4290/// pool ancestors (supers + interfaces, transitively), and framework
4291/// ancestors via the embedded API database (chain sets memoized
4292/// process-wide — every View subclass shares the View/ViewGroup/Object
4293/// field union, so the per-file cost is its own pool chain only).
4294fn inherited_field_shadows(
4295    pool: &DexPool,
4296    class: &PoolClass,
4297) -> jdc_core::FxHashSet<String> {
4298    fn fw_chain_fields(boundary: &str) -> std::sync::Arc<jdc_core::FxHashSet<String>> {
4299        use std::sync::{Mutex, OnceLock};
4300        static CACHE: OnceLock<Mutex<jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>>>> =
4301            OnceLock::new();
4302        let cache = CACHE.get_or_init(|| Mutex::new(jdc_core::FxHashMap::default()));
4303        if let Some(hit) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(boundary) {
4304            return hit.clone();
4305        }
4306        let mut set: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4307        let mut stack: Vec<&str> = vec![boundary];
4308        let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
4309        let mut hops = 0usize;
4310        while let Some(c) = stack.pop() {
4311            hops += 1;
4312            if hops > 256 || !seen.insert(c) {
4313                continue;
4314            }
4315            crate::fwdb::for_each_field(c, |n, f| {
4316                // STATIC only: instance fields shadow qualified refs in
4317                // instance contexts but NOT in static ones, and the
4318                // render decision is context-free — the static subset
4319                // is the sound approximation (View.X/Y/Z are static;
4320                // including instance fields cost +33 battery).
4321                if f & crate::fwdb::MF_STATIC != 0 {
4322                    set.insert(n.to_string());
4323                }
4324            });
4325            if let Some(sup) = crate::fwdb::super_of(c) {
4326                stack.push(sup);
4327            }
4328            crate::fwdb::for_each_interface(c, |i| stack.push(i));
4329        }
4330        let arc = std::sync::Arc::new(set);
4331        cache
4332            .lock()
4333            .unwrap_or_else(|e| e.into_inner())
4334            .insert(boundary.to_string(), arc.clone());
4335        arc
4336    }
4337
4338    let mut out: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4339    let mut stack: Vec<String> = vec![class.name.to_string()];
4340    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4341    let mut hops = 0usize;
4342    while let Some(cn) = stack.pop() {
4343        hops += 1;
4344        if hops > 512 || !seen.insert(cn.clone()) {
4345            continue;
4346        }
4347        let Some(pc) = pool.get(&cn) else {
4348            // Framework boundary: memoized chain set.
4349            out.extend(fw_chain_fields(&cn).iter().cloned());
4350            continue;
4351        };
4352        // Static-only (pool side too): ANCESTOR private instance
4353        // fields are NOT inherited into lexical scope (JLS 6.4.3.1) —
4354        // including them over-triggered the obscured-import route and
4355        // the emitted simple static qualifiers were captured by real
4356        // in-scope variables (reqable SearchView `a.H` "变量 H" ×128).
4357        // Own instance fields shadowing packages are handled at the
4358        // ROOT: the collision-mint loops avoid package first segments
4359        // (lib.rs root_segs guard), and raw-name collisions go
4360        // through field_deshadow_renames.
4361        for f in pc.static_fields.iter() {
4362            out.insert(f.name.to_string());
4363        }
4364        // Accessible (public/protected) instance fields ARE inherited
4365        // into lexical scope (JLS 8.2) and obscure package names in
4366        // expression position — k.c1 `public k.m d` vs package `d`
4367        // (ActionMenuItemView `d.j.v` → 找不到符号 变量 j ×97 in
4368        // reqable). Private/package-private stay out (not in scope;
4369        // the ×128 SearchView lesson).
4370        for f in pc.instance_fields.iter() {
4371            if f.access & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED) != 0 {
4372                out.insert(f.name.to_string());
4373            }
4374        }
4375        if let Some(sup) = &pc.super_name {
4376            if sup != "java/lang/Object" {
4377                stack.push(sup.clone());
4378            } else {
4379                // Object itself lives in the DB (its fields shadow too —
4380                // none exist, but interface constants reached via
4381                // interfaces below still do).
4382            }
4383        } else {
4384            // Interface (no super): java/lang/Object contributes nothing.
4385        }
4386        stack.extend(pc.interfaces.iter().cloned());
4387    }
4388    out
4389}
4390
4391/// The pre-scan: internal names referenced by the class's metadata
4392/// (supertypes, field types, method descriptors) whose first segment
4393/// equals the class's own simple name and which exist in the pool —
4394/// these get imports and simple-name renders.
4395/// Base simple name of an internal (`pq/c$x` → `c`), if it lives in
4396/// `pkg` (the bare-name segment a same-package ref renders under).
4397fn sibling_simple(internal: &str, pkg: &str) -> Option<String> {
4398    let (p, tail) = match internal.rfind('/') {
4399        Some(i) => (&internal[..i], &internal[i + 1..]),
4400        None => ("", internal),
4401    };
4402    if p != pkg || tail.is_empty() {
4403        return None;
4404    }
4405    let base = tail.split('$').next().unwrap_or("");
4406    if base.is_empty() {
4407        None
4408    } else {
4409        Some(base.to_string())
4410    }
4411}
4412
4413/// Simple names of SAME-PACKAGE classes this file's family references
4414/// from method bodies (raw insn ref scan — decode only, no lift) or
4415/// metadata (supertypes, field descriptors, signature descriptors).
4416/// JLS 7.5.1: a single-type import legally shadows same-package
4417/// siblings throughout the file — it only breaks the refs the file
4418/// ITSELF makes to them, so the package-wide blocked set narrows to
4419/// this. Undercounting would silently rebind a sibling ref (semantic
4420/// corruption), so the universe is exhaustive over code refs:
4421/// invoke/field owners, type-index instructions, catch types.
4422fn sibling_ref_simples(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashSet<String> {
4423    let pkg = class.name.rfind('/').map(|i| &class.name[..i]).unwrap_or("");
4424    let mut out: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4425    let add_internal = |n: &str, out: &mut jdc_core::FxHashSet<String>| {
4426        if let Some(s) = sibling_simple(n, pkg) {
4427            out.insert(s);
4428        }
4429    };
4430    let add_desc = |d: &str, out: &mut jdc_core::FxHashSet<String>| {
4431        // descriptor → internal(s): arrays peel, L..; strips
4432        let mut t = d;
4433        while t.starts_with('[') {
4434            t = &t[1..];
4435        }
4436        if t.len() > 1 && t.starts_with('L') && t.ends_with(';') {
4437            add_internal(&t[1..t.len() - 1], out);
4438        }
4439    };
4440    // metadata: supertypes, fields, signatures (family-wide below)
4441    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4442    let mut queue: Vec<String> = vec![class.name.clone()];
4443    while let Some(cn) = queue.pop() {
4444        if !seen.insert(cn.clone()) {
4445            continue;
4446        }
4447        queue.extend(pool.children_of(&cn).iter().cloned());
4448        let Some(pc) = pool.get(&cn) else { continue };
4449        add_internal(&pc.name, &mut out);
4450        if let Some(sup) = &pc.super_name {
4451            add_internal(sup, &mut out);
4452        }
4453        for i in &pc.interfaces {
4454            add_internal(i, &mut out);
4455        }
4456        for f in pc.static_fields.iter().chain(pc.instance_fields.iter()) {
4457            add_desc(&f.desc, &mut out);
4458        }
4459        for m in pc.all_methods() {
4460            if let Some(d) = m.parsed_desc() {
4461                for a in &d.args {
4462                    if let JavaType::Object(n) = a {
4463                        add_internal(n, &mut out);
4464                    } else if let JavaType::Array(_) = a {
4465                        add_desc(&a.to_descriptor(), &mut out);
4466                    }
4467                }
4468                if let JavaType::Object(n) = &d.ret {
4469                    add_internal(n, &mut out);
4470                } else if let JavaType::Array(_) = &d.ret {
4471                    add_desc(&d.ret.to_descriptor(), &mut out);
4472                }
4473            }
4474            let Some(dex) = pool.dex(m.dex_idx) else {
4475                continue;
4476            };
4477            let Some(code) = dex.code_at(m.code_off) else {
4478                continue;
4479            };
4480            for ins in code.insns.iter() {
4481                use ddc_dex::insn::InsnKind;
4482                match &ins.kind {
4483                    InsnKind::Invoke { method_idx, .. } => {
4484                        let mm = dex.method(*method_idx);
4485                        add_internal(&dex.class_name(mm.class_idx), &mut out);
4486                    }
4487                    InsnKind::IGet { field_idx, .. }
4488                    | InsnKind::IPut { field_idx, .. }
4489                    | InsnKind::SGet { field_idx, .. }
4490                    | InsnKind::SPut { field_idx, .. } => {
4491                        let f = dex.field(*field_idx);
4492                        add_internal(&dex.class_name(f.class_idx), &mut out);
4493                    }
4494                    InsnKind::ConstClass { type_idx, .. }
4495                    | InsnKind::NewInstance { type_idx, .. }
4496                    | InsnKind::NewArray { type_idx, .. }
4497                    | InsnKind::FilledNewArray { type_idx, .. }
4498                    | InsnKind::CheckCast { type_idx, .. }
4499                    | InsnKind::InstanceOf { type_idx, .. } => {
4500                        add_desc(dex.type_name(*type_idx), &mut out);
4501                    }
4502                    _ => {}
4503                }
4504            }
4505            for h in code.handlers.iter() {
4506                for (tidx, _) in &h.catches {
4507                    add_desc(dex.type_name(*tidx), &mut out);
4508                }
4509            }
4510        }
4511    }
4512    // the file's OWN family names are not import-shadow risks for
4513    // themselves (own-simple drops happen later); keep them anyway —
4514    // a sibling simple equal to a family base only blocks when a
4515    // same-package OTHER class shares it, which is the true collision.
4516    out
4517}
4518
4519fn compute_obscured_renders(
4520    pool: &DexPool,
4521    class: &PoolClass,
4522    shadow: &jdc_core::FxHashSet<String>,
4523) -> jdc_core::FxHashMap<String, String> {
4524    let own_renamed = crate::apply_class_rename(&class.name);
4525    let own_simple = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
4526    if own_simple.is_empty() {
4527        return jdc_core::FxHashMap::default();
4528    }
4529    let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
4530    for sup in class.interfaces.iter() {
4531        refs.insert(sup.clone());
4532    }
4533    if let Some(sup) = &class.super_name {
4534        refs.insert(sup.clone());
4535    }
4536    for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
4537        if let JavaType::Object(n) = crate::desc_type(&f.desc) {
4538            refs.insert(n.to_string());
4539        }
4540    }
4541    for m in class.all_methods() {
4542        if let Some(d) = m.parsed_desc() {
4543            for a in d.args.iter() {
4544                if let JavaType::Object(n) = a {
4545                    refs.insert(n.to_string());
4546                }
4547            }
4548            if let JavaType::Object(n) = &d.ret {
4549                refs.insert(n.to_string());
4550            }
4551        }
4552    }
4553    let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
4554    for r in refs {
4555        // First segment == the class's own simple name: the qualified
4556        // render would be obscured. The name must exist in the pool for
4557        // the import to resolve. The KEY stays internal (renders look
4558        // refs up by internal name), but the import line and the
4559        // simple name use the RENAMED display — a collision-renamed
4560        // class renders under its display name and an import of the
4561        // internal name does not resolve.
4562        let first = r.split('/').next().unwrap_or("");
4563        let seg_obscured = first == own_simple || shadow.contains(first);
4564        if seg_obscured && r.split('/').count() >= 2 && pool.get(&r).is_some() {
4565            // Own-family refs (the class itself, its nested members)
4566            // render through member scope — an import would be redundant
4567            // (self-import) or shadow a same-package sibling named like
4568            // the tail (`import t.t2.a` hijacks every bare `a` that
4569            // meant sibling class t.a — lark t/t2).
4570            if r == class.name || r.starts_with(&format!("{}$", class.name)) {
4571                continue;
4572            }
4573            let display = crate::apply_class_rename(&r);
4574            // dotted-tail simple (flat `$$` vs nested — see the dynamic
4575            // path's note)
4576            let simple = crate::classdec::dotted(&display)
4577                .rsplit('.')
4578                .next()
4579                .unwrap_or("")
4580                .to_string();
4581            if !simple.is_empty() {
4582                out.insert(r.clone(), simple);
4583            }
4584        }
4585    }
4586    out
4587}
4588
4589pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
4590    if let Some(simple) = obscured_render_pub(internal) {
4591        return sanitize_ref(&simple);
4592    }
4593    let orig_internal: &str = internal;
4594    // Framework-shadow stub: the reference means the PLATFORM class —
4595    // render the raw FQN (only the stub's own FILE identity moved).
4596    let shadow_skip = crate::is_fw_shadow(internal);
4597    if std::env::var("DDC_DBG_FW").is_ok() && internal.contains("ImageDecoder") {
4598        eprintln!("[fw] print_class_name input={internal} skip={shadow_skip}");
4599    }
4600    let cow = if shadow_skip {
4601        std::borrow::Cow::Borrowed(internal)
4602    } else {
4603        crate::apply_class_rename(internal)
4604    };
4605    let internal: &str = &cow;
4606    // The pool indexes PRE-rename internals. The ddcroot ROOT-PACKAGE
4607    // RELOCATION renames `Foo$Bar` to `ddcroot/Foo$Bar` and the pool
4608    // lookups below must fall back to the pre-rename name, or the
4609    // class reads as off-pool — the framework-nesting fallback then
4610    // dots the `$` (`ddcroot.UserCustomStatusExtraParams.
4611    // CalendarAutomaticStatus` against a FLAT `...$...` declaration —
4612    // "不可见" ×138 on lark's R8-outer-deleted enum family). STRICTLY
4613    // the relocation (renamed == rp + "/" + orig): collision-renamed
4614    // nested families (weibo x0$a$b) keep their pre-existing
4615    // generic-loop treatment — a general orig fallback flipped their
4616    // `!pool.get(display)` dotting condition and regressed weibo
4617    // +483.
4618    let reloc_orig: Option<&str> = if orig_internal != internal {
4619        match crate::root_pkg_display() {
4620            Some(rp)
4621                if internal.len() > rp.len() + 1
4622                    && internal.starts_with(rp.as_str())
4623                    && internal.as_bytes()[rp.len()] == b'/'
4624                    && &internal[rp.len() + 1..] == orig_internal
4625                    && pool.get(orig_internal).is_some() =>
4626            {
4627                Some(orig_internal)
4628            }
4629            _ => None,
4630        }
4631    } else {
4632        None
4633    };
4634    let pool_name: &str = if pool.get(internal).is_some() {
4635        internal
4636    } else {
4637        reloc_orig.unwrap_or(internal)
4638    };
4639    // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
4640    // shape, `Outer$lruCache$1`) is emitted as its own top-level file
4641    // whose simple name keeps every `$` — the `$` boundaries inside
4642    // that unit are part of the NAME, not nesting. References must use
4643    // the flat unit name; the generic loop below dotted the first
4644    // boundary (`LruCacheKt.lruCache$1`) against the declaration
4645    // `class LruCacheKt$lruCache$1` — every use of the type failed and
4646    // javac's attribution for the whole file collapsed (3627 pure-
4647    // cascade files on weibo).
4648    if pool_name.contains('$') && pool.get(pool_name).is_some() {
4649        if let Some(root) = emission_root(pool, pool_name) {
4650            if root.contains('$') {
4651                let below = &pool_name[root.len()..];
4652                if below.is_empty() {
4653                    // The unit itself: the flat name IS the reference.
4654                    return sanitize_ref(&internal.replace('/', "."));
4655                }
4656                let segs: Vec<&str> = below[1..].split('$').collect();
4657                if segs.iter().any(|s| s.is_empty()) {
4658                    // R8 tails like `ThreadMsg$$$` cannot be dotted at
4659                    // all — keep the whole name flat.
4660                    return sanitize_ref(&internal.replace('/', "."));
4661                }
4662                let root_cow = crate::apply_class_rename(&root);
4663                let mut out = root_cow.replace('/', ".");
4664                for s in segs {
4665                    out.push('.');
4666                    out.push_str(s);
4667                }
4668                return sanitize_ref(&out);
4669            }
4670        }
4671    }
4672    let mut out = String::new();
4673    // `known` must test the ACCUMULATED internal prefix, not the bare
4674    // inter-`$` segment: the per-segment shape checked `pool.get("a")`
4675    // for the second level of `s5/o$a$b`, missed, and rendered the
4676    // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
4677    // nested type.
4678    let mut off = 0usize;
4679    loop {
4680        let rest = &internal[off..];
4681        match rest.find('$') {
4682            Some(i) => {
4683                let seg = &rest[..i];
4684                let prefix = &internal[..off + i];
4685                let known = pool.get(prefix).is_some()
4686                    || jdc_core::rename::is_renamed_display(prefix)
4687                    // The FULL name is not a pool class (also under
4688                    // the ddcroot-relocation pre-rename form — the
4689                    // migration hides pool classes from the renamed
4690                    // lookup): this `$` cannot be a literal name
4691                    // (pool literal classes — an app's own
4692                    // `View$OnUnhandledKeyEventListener` — keep their
4693                    // `$` here AND at their declaration), so it can
4694                    // only be an external framework nesting boundary
4695                    // (`View$OnClickListener` → `.OnClickListener`).
4696                    || !(pool.get(internal).is_some() || reloc_orig.is_some());
4697                // The `$` may only become a nesting dot when the tail
4698                // segment STARTS a Java identifier: R8's desugared-
4699                // library names carry `$` inside PACKAGE paths
4700                // (`j$/util/...` dotted into `j..util`) and suffixes
4701                // like `Collection$-EL` or anonymous `RequestId$1`
4702                // cannot be dotted under any reading.
4703                //
4704                // Sanitizer-escaped tails DO dot for POOL classes: the
4705                // injective `_u<hex>` escape turns any non-ASCII (or
4706                // ASCII-punctuation) leading char into `_`-initial —
4707                // a valid identifier start — and the declaration side
4708                // renders the member that way (weibo's unicode-named
4709                // nested `a$Ꮺ`: decl `class a_u2dIA` inside a, ctor
4710                // params dotted `a.a_u2dIA`, but casts stayed flat
4711                // `a$a_u2dIA` — 找不到符号 类 ×~500). Digit and `$`
4712                // tails stay flat (emission-unit files keep the `$`;
4713                // framework binary names are unreachable either way).
4714                let tail_next = rest[i + 1..].chars().next();
4715                let tail_ok = match tail_next {
4716                    Some(c) if c.is_ascii_alphabetic() || c == '_' => true,
4717                    // `/` = a `$` INSIDE the package path (desugar's
4718                    // `j$/util/...` — dotting produced `j..util`,
4719                    // deepseek 非法'.' ×3,661); digit tails are flat
4720                    // emission units; `-`-led tails fail
4721                    // clean_member_tail and emit flat too; `$` tails
4722                    // ($$-orphans) keep the old behavior.
4723                    Some(c) if !c.is_ascii_digit() && c != '$' && c != '/' && c != '-' => {
4724                        pool.get(internal).is_some()
4725                    }
4726                    _ => false,
4727                };
4728                out.push_str(&seg.replace('/', "."));
4729                out.push_str(if known && tail_ok { "." } else { "$" });
4730                off += i + 1;
4731            }
4732            None => {
4733                out.push_str(&rest.replace('/', "."));
4734                return sanitize_ref(&out);
4735            }
4736        }
4737    }
4738}
4739
4740/// The top-level EMISSION UNIT ancestor of a pool class: walking the
4741/// `$`-chain upward (outer_of), the first ancestor that is itself
4742/// emitted as its own compilation unit — a digit-tail rest marks a
4743/// flat boundary (see top_level_classes). Clean members render inline
4744/// in their outer, so only the unit's own name may carry `$`.
4745fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
4746    let mut cur = internal.to_string();
4747    loop {
4748        let outer = pool.outer_of(&cur)?;
4749        let rest = cur
4750            .strip_prefix(outer)
4751            .and_then(|t| t.strip_prefix('$'))
4752            .unwrap_or("");
4753        if !crate::clean_member_tail(rest) {
4754            return Some(cur);
4755        }
4756        cur = outer.to_string();
4757    }
4758}
4759
4760/// Class-file names may contain characters Java source identifiers
4761/// cannot (`Collection$-EL`); the deterministic mapping matches the
4762/// declaration sites (java_ident).
4763/// Every `.`-segment of a fully-qualified name must start a Java
4764/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
4765///
4766/// PUBLIC on purpose: this is the single source of truth for the
4767/// declaration↔file-name mapping. The CLI writer used to keep its own
4768/// copy (`sanitize_file_seg`) which had drifted — it lacked the lone-`_`
4769/// escape below — so `l.֡` declared `class __` inside a file named
4770/// `_.java`. Callers reach it through `sanitize_seg`/`sanitize_internal`.
4771///
4772/// INJECTIVE on the characters, which is the property the writer needs.
4773/// The old mapping folded every non-identifier character to `_`, so
4774/// `l.᩻ܶ`, `l.᩻ۡ` and `l.֫᩷` all became `l.__`: on bin.mt.plus 22,636
4775/// distinct classes in package `l` collapsed onto three file names, the
4776/// writer's EEXIST branch overwrote them, and 22,633 sources were
4777/// destroyed with exit code 0. `_u<hex>` keeps them distinct, is
4778/// self-describing (it encodes the original code point) and costs the
4779/// decompiler nothing — the alternative, minting 22,636 registry
4780/// renames, makes every type reference allocate and took this sample
4781/// from 5.2 s to 26.8 s.
4782///
4783/// The only residual collisions are *lookalikes*: a literal class named
4784/// `_u1a7b` beside the class `᩻` (same code point), or the pre-existing
4785/// keyword form `_do` beside `do`. Those are detected and repaired
4786/// deterministically by `lossy_sanitize_renames`, which is why that pass
4787/// still exists.
4788pub fn sanitize_fq(dotted: &str) -> String {
4789    dotted
4790        .split('.')
4791        .map(sanitize_fq_seg)
4792        .collect::<Vec<_>>()
4793        .join(".")
4794}
4795
4796/// One `.`-segment: escape, then repair identifier-position problems.
4797fn sanitize_fq_seg(seg: &str) -> String {
4798    // Ident-safe ASCII passes through unchanged (the overwhelmingly
4799    // common case, and the only one on the borrow-fast path); every
4800    // other character — non-ASCII, and ASCII punctuation such as the
4801    // `-` in `Collection$-EL` — becomes `_u<hex>`.
4802    let plain = seg
4803        .chars()
4804        .all(|c| c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$'));
4805    let mut out = if plain {
4806        seg.to_string()
4807    } else {
4808        use std::fmt::Write as _;
4809        let mut s = String::with_capacity(seg.len() + 8);
4810        for c in seg.chars() {
4811            if c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$') {
4812                s.push(c);
4813            } else {
4814                s.push_str("_u");
4815                let _ = write!(s, "{:x}", c as u32);
4816            }
4817        }
4818        s
4819    };
4820    // `out` is pure ASCII from here: the identifier-position repairs.
4821    if is_java_keyword_name(&out)
4822        || is_restricted_type_name(&out)
4823        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
4824    {
4825        out.insert(0, '_');
4826    }
4827    // A lone `_` is a reserved IDENTIFIER since Java 9.
4828    if out == "_" {
4829        out = "__".to_string();
4830    }
4831    out
4832}
4833
4834/// Restricted contextual TYPE names — legal as member/local names
4835/// (rt.jar compiles `var` locals), illegal in class declarations and
4836/// type references. Consulted only on CLASS-name paths.
4837pub(crate) fn is_restricted_type_name(s: &str) -> bool {
4838    matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
4839}
4840
4841fn is_java_keyword_name(s: &str) -> bool {
4842    matches!(
4843        s,
4844        "abstract"
4845            | "assert"
4846            | "boolean"
4847            | "break"
4848            | "byte"
4849            | "case"
4850            | "catch"
4851            | "char"
4852            | "class"
4853            | "const"
4854            | "continue"
4855            | "default"
4856            | "do"
4857            | "double"
4858            | "else"
4859            | "enum"
4860            | "extends"
4861            | "final"
4862            | "finally"
4863            | "float"
4864            | "for"
4865            | "goto"
4866            | "if"
4867            | "implements"
4868            | "import"
4869            | "instanceof"
4870            | "int"
4871            | "interface"
4872            | "long"
4873            | "native"
4874            | "new"
4875            | "package"
4876            | "private"
4877            | "protected"
4878            | "public"
4879            | "return"
4880            | "short"
4881            | "static"
4882            | "strictfp"
4883            | "super"
4884            | "switch"
4885            | "synchronized"
4886            | "this"
4887            | "throw"
4888            | "throws"
4889            | "transient"
4890            | "try"
4891            | "void"
4892            | "volatile"
4893            | "while"
4894            | "true"
4895            | "false"
4896            | "null"
4897            // `_` is a reserved identifier since Java 9 (Alipay's
4898            // instant-run fields are named `_`) — the `_<name>` mapping
4899            // turns it into `__`, matching jdc-core's call sites.
4900            | "_"
4901    )
4902}
4903
4904fn sanitize_ref(name: &str) -> String {
4905    sanitize_fq(name)
4906}
4907
4908/// Dotted source form of an internal name.
4909pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
4910    print_class_name(pool, internal)
4911}
4912
4913/// Unused import silencer.
4914#[allow(dead_code)]
4915fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
4916    let _ = java_type_to_generic;
4917}
4918
4919#[cfg(test)]
4920mod sanitize_tests {
4921    use super::sanitize_fq;
4922
4923    /// The property the file writer depends on: distinct class names must
4924    /// reach distinct output paths. The lossy predecessor of this mapping
4925    /// folded every non-identifier character to `_`, so 22,636 classes of
4926    /// `bin.mt.plus` (names built from Thai/Yi/Syriac code points) became
4927    /// three (`l.__`, `l.___`, `l._`) and all but three were overwritten.
4928    #[test]
4929    fn distinct_obfuscated_names_stay_distinct() {
4930        // The three names that collapsed onto `l/__.java` in the field.
4931        let names = ["l.᩻ܶ", "l.᩻ۡ", "l.֫᩷", "l.֡", "l.᩻᩶ۛ"];
4932        let mut mapped: Vec<String> = names.iter().map(|n| sanitize_fq(n)).collect();
4933        let before = mapped.len();
4934        mapped.sort();
4935        mapped.dedup();
4936        assert_eq!(mapped.len(), before, "sanitizer folded distinct names: {mapped:?}");
4937        assert!(
4938            mapped.iter().all(|m| m.is_ascii()),
4939            "sanitizer must stay ASCII: {mapped:?}"
4940        );
4941        // Self-describing: the escape carries the original code point.
4942        assert_eq!(sanitize_fq("l.᩻ܶ"), "l._u1a7b_u736");
4943    }
4944
4945    /// Every output must be a legal Java identifier segment: no leading
4946    /// digit, no keyword, not the lone `_` reserved since Java 9.
4947    #[test]
4948    fn output_is_a_legal_identifier() {
4949        for (input, want) in [
4950            ("do", "_do"),
4951            ("_", "__"),
4952            ("0Xx", "_0Xx"),
4953            ("Collection$-EL", "Collection$_u2dEL"),
4954            ("᩻", "_u1a7b"),
4955            ("a", "a"),
4956            ("A$B", "A$B"),
4957            ("x_y", "x_y"),
4958        ] {
4959            assert_eq!(sanitize_fq(input), want, "input {input:?}");
4960        }
4961    }
4962
4963    /// Documented residual: ident-safe ASCII passes through unchanged, so a
4964    /// literal `_u1a7b` collides with the single character U+1A7B. The
4965    /// collision is real — which is why `lossy_sanitize_renames` still owns
4966    /// a repair pass — and this test pins the shape the guard must catch.
4967    #[test]
4968    fn lookalike_collision_is_known_and_bounded() {
4969        assert_eq!(sanitize_fq("_u1a7b"), sanitize_fq("᩻"));
4970    }
4971}