passes: gate step-2 renames on the virtual family index (fixes override-chain split)

This commit is contained in:
Loki 2026-10-12 10:38:55 +02:00
parent 2069aa6646
commit 045cd3ed9c
Signed by: boba
GPG key ID: 253067914055423B
3 changed files with 171 additions and 15 deletions

View file

@ -13,6 +13,9 @@ use crate::namegen::NameGenerator;
/// `(owner, name, descriptor)` - matches a JVM reference.
pub(super) type MemberKey = (String, String, String);
/// `(method name, params)` - one virtual family.
pub(super) type FamilyKey = (String, String);
/// Rename maps for one run: `declared` is where a member was declared,
/// `alias` is subclasses that inherit the name without declaring it themselves.
#[derive(Debug, Default)]
@ -85,19 +88,18 @@ impl RenameMaps {
}
}
/// Rename virtual methods as a "family": one name for the whole hierarchy,
/// including bridge methods with covariant returns.
pub(super) fn rename_virtual_families(
/// Family = (name, params): the return type is ignored, so covariant
/// returns and bridge methods (`ACC_BRIDGE`) are captured together.
///
/// Also returns the families that must keep their original name: a locked
/// class or a `keep_member` name in the family locks it for everybody -
/// renaming only the other members would split the override chain.
pub(super) fn build_virtual_families(
program: &Program,
analysis: &Analysis,
maps: &mut RenameMaps,
cfg: &Config,
ctx: &mut Context,
) -> usize {
// Family = (name, params): the return type is ignored, so covariant
// returns and bridge methods (`ACC_BRIDGE`) are captured together.
let mut families: BTreeMap<(String, String), Vec<MemberKey>> = BTreeMap::new();
let mut locked_families: BTreeSet<(String, String)> = BTreeSet::new();
) -> (BTreeMap<FamilyKey, Vec<MemberKey>>, BTreeSet<FamilyKey>) {
let mut families: BTreeMap<FamilyKey, Vec<MemberKey>> = BTreeMap::new();
let mut locked: BTreeSet<FamilyKey> = BTreeSet::new();
for class in &program.classes {
let locked_class = analysis.is_locked(&class.this_class);
@ -113,7 +115,7 @@ pub(super) fn rename_virtual_families(
};
let family = (method.name.clone(), params);
if locked_class || analysis.keep.keeps_member(&method.name) {
locked_families.insert(family.clone());
locked.insert(family.clone());
}
families.entry(family).or_default().push((
class.this_class.clone(),
@ -122,7 +124,18 @@ pub(super) fn rename_virtual_families(
));
}
}
(families, locked)
}
/// Rename virtual methods as a "family": one name for the whole hierarchy,
/// including bridge methods with covariant returns.
pub(super) fn rename_virtual_families(
families: BTreeMap<(String, String), Vec<MemberKey>>,
locked_families: BTreeSet<FamilyKey>,
maps: &mut RenameMaps,
cfg: &Config,
ctx: &mut Context,
) -> usize {
let mut renamed = 0usize;
let mut family_index = 0u64;
for (family_key, members) in families {
@ -150,7 +163,7 @@ pub(super) fn rename_virtual_families(
}
/// `(<params>)<ret>` → `(<params>)`, or `None` if the descriptor is malformed.
fn params_of(descriptor: &str) -> Option<String> {
pub(super) fn params_of(descriptor: &str) -> Option<String> {
let close = descriptor.find(')')?;
if !descriptor.starts_with('(') {
return None;

View file

@ -23,7 +23,8 @@ use noxis_classfile::Program;
use noxis_core::{Context, CoreError, Pass, PassId, PassReport, field_key, method_key};
use self::assign::{
RenameMaps, generate_packages, parent_package, rename_virtual_families, simple_name, widen_all,
RenameMaps, build_virtual_families, generate_packages, params_of, parent_package,
rename_virtual_families, simple_name, widen_all,
};
use self::rewrite::rewrite_class;
use crate::namegen::NameGenerator;
@ -58,6 +59,13 @@ impl Pass for RenamePass {
let mut maps = RenameMaps::default();
let mut renamed_classes = 0usize;
// Family membership up front: step 2 must not rename a method that
// belongs to a shared or locked family on its own - a per-class
// rename splits the override chain (the JVM then fails dispatch
// with AbstractMethodError, real case: shaded gson's package-private
// `ReflectionHelper$RecordHelper.isRecord` chain).
let (families, locked_families) = build_virtual_families(program, &analysis);
// 1. Class names.
{
let packages = generate_packages(&cfg, &mut ctx.rng("rename/packages"));
@ -117,6 +125,35 @@ impl Pass for RenamePass {
method_names.reserve(method.name.clone());
continue;
}
// A method shared with another class must not get its
// name here: the family pass assigns one common name
// (aggressive mode), or the whole family keeps the
// original when it is locked. A per-class rename splits
// the override chain - the JVM then fails dispatch with
// `AbstractMethodError` (real case: shaded gson's
// package-private `ReflectionHelper$RecordHelper.isRecord`
// chain, each class renamed apart).
if let Some(params) = params_of(&method.descriptor) {
let key = (method.name.clone(), params);
if let Some(members) = families.get(&key) {
let hierarchy_related = members.iter().any(|(other, _, _)| {
other != &owner
&& (hierarchy.is_subclass_of(other, &owner)
|| hierarchy.is_subclass_of(&owner, other))
});
// Aggressive mode renames every shared family
// uniformly. Without it the family pass does not
// run, so only hierarchy-related members must stay
// together; unrelated same-name methods never meet
// at dispatch and may rename individually.
let shared = members.len() > 1
&& (cfg.rename.rename_public_members || hierarchy_related);
if shared || locked_families.contains(&key) {
method_names.reserve(method.name.clone());
continue;
}
}
}
let safety = method_rename_safety(
program,
&hierarchy,
@ -181,7 +218,8 @@ impl Pass for RenamePass {
// 3. Virtual families - aggressive mode only.
if cfg.rename.rename_public_members {
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
renamed_methods +=
rename_virtual_families(families, locked_families, &mut maps, &cfg, ctx);
}
// 4. Aliases for subclasses.