Skip to content

Commit 469c950

Browse files
committed
fix(naming): recover leftover source locals without vN fallbacks
The constrained solver only names high-score source bindings, so unnamed locals fell back to register-shaped vN names. Name remaining source bindings from type, role, or value, and use the same fallback during lowering. Skip type names that already look like DEX registers.
1 parent 47e0028 commit 469c950

14 files changed

Lines changed: 269 additions & 91 deletions

dexdec/src/analysis/java_backend/semantic_naming.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ impl<'a> StructuralNameModel<'a> {
191191
match ty {
192192
JavaType::Class(class) => {
193193
let name = &class.segments.last()?.name;
194-
(name.as_str() != "Object").then(|| self.morphology.lower_camel(name))
194+
let source = name.as_str();
195+
(source != "Object" && !is_register_style_name(source))
196+
.then(|| self.morphology.lower_camel(name))
195197
}
196198
JavaType::Array(element) => self
197199
.java_type_name(element)
@@ -556,11 +558,64 @@ impl<'a> SemanticNameRecovery<'a> {
556558
.flatten()
557559
.chain(this_variable)
558560
.collect::<BTreeSet<_>>();
559-
ConstrainedNameSolver::new(StructuralNameModel::for_graph(self.types, &graph), 35).solve(
561+
let model = StructuralNameModel::for_graph(self.types, &graph);
562+
let mut names =
563+
ConstrainedNameSolver::new(StructuralNameModel::for_graph(self.types, &graph), 35)
564+
.solve(&graph, &roles, parameter_names, &excluded);
565+
fill_remaining_source_names(
560566
&graph,
561567
&roles,
568+
&model,
562569
parameter_names,
563570
&excluded,
564-
)
571+
&mut names,
572+
);
573+
names
574+
}
575+
}
576+
577+
fn is_register_style_name(name: &str) -> bool {
578+
name.strip_prefix('v').is_some_and(|digits| {
579+
!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
580+
})
581+
}
582+
583+
fn fill_remaining_source_names(
584+
graph: &VariableSemanticGraph,
585+
roles: &VariableRoleScores,
586+
model: &StructuralNameModel<'_>,
587+
reserved: &[JavaIdentifier],
588+
excluded: &BTreeSet<u32>,
589+
names: &mut BTreeMap<u32, JavaIdentifier>,
590+
) {
591+
let mut used = names.values().cloned().collect::<BTreeSet<_>>();
592+
for name in reserved {
593+
used.insert(name.clone());
594+
}
595+
for variable in graph.variables() {
596+
if !variable.is_source_binding() || excluded.contains(&variable.identity()) {
597+
continue;
598+
}
599+
if names.contains_key(&variable.identity()) {
600+
continue;
601+
}
602+
let preferred = model
603+
.type_name(variable.ty())
604+
.or_else(|| {
605+
roles
606+
.roles(variable.identity())
607+
.max_by_key(|(_, score)| *score)
608+
.filter(|(_, score)| *score > 0)
609+
.map(|(role, _)| {
610+
JavaIdentifier::from_hint(StructuralNameModel::role_name(role))
611+
})
612+
})
613+
.unwrap_or_else(|| JavaIdentifier::from_hint("value"));
614+
let name = if used.insert(preferred.clone()) {
615+
preferred
616+
} else {
617+
ConstrainedNameSolver::<StructuralNameModel>::claim_variant(&preferred, &mut used)
618+
};
619+
names.insert(variable.identity(), name);
565620
}
566621
}

dexdec/src/analysis/kotlin_backend/semantic_naming.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ impl<'a> StructuralNameModel<'a> {
191191
match ty {
192192
KotlinType::Class(class) => {
193193
let name = &class.segments.last()?.name;
194-
(name.as_str() != "Object").then(|| self.morphology.lower_camel(name))
194+
let source = name.as_str();
195+
(source != "Object" && !is_register_style_name(source))
196+
.then(|| self.morphology.lower_camel(name))
195197
}
196198
KotlinType::Array(element) => self
197199
.java_type_name(element)
@@ -556,11 +558,64 @@ impl<'a> SemanticNameRecovery<'a> {
556558
.flatten()
557559
.chain(this_variable)
558560
.collect::<BTreeSet<_>>();
559-
ConstrainedNameSolver::new(StructuralNameModel::for_graph(self.types, &graph), 35).solve(
561+
let model = StructuralNameModel::for_graph(self.types, &graph);
562+
let mut names =
563+
ConstrainedNameSolver::new(StructuralNameModel::for_graph(self.types, &graph), 35)
564+
.solve(&graph, &roles, parameter_names, &excluded);
565+
fill_remaining_source_names(
560566
&graph,
561567
&roles,
568+
&model,
562569
parameter_names,
563570
&excluded,
564-
)
571+
&mut names,
572+
);
573+
names
574+
}
575+
}
576+
577+
fn is_register_style_name(name: &str) -> bool {
578+
name.strip_prefix('v').is_some_and(|digits| {
579+
!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
580+
})
581+
}
582+
583+
fn fill_remaining_source_names(
584+
graph: &VariableSemanticGraph,
585+
roles: &VariableRoleScores,
586+
model: &StructuralNameModel<'_>,
587+
reserved: &[KotlinIdentifier],
588+
excluded: &BTreeSet<u32>,
589+
names: &mut BTreeMap<u32, KotlinIdentifier>,
590+
) {
591+
let mut used = names.values().cloned().collect::<BTreeSet<_>>();
592+
for name in reserved {
593+
used.insert(name.clone());
594+
}
595+
for variable in graph.variables() {
596+
if !variable.is_source_binding() || excluded.contains(&variable.identity()) {
597+
continue;
598+
}
599+
if names.contains_key(&variable.identity()) {
600+
continue;
601+
}
602+
let preferred = model
603+
.type_name(variable.ty())
604+
.or_else(|| {
605+
roles
606+
.roles(variable.identity())
607+
.max_by_key(|(_, score)| *score)
608+
.filter(|(_, score)| *score > 0)
609+
.map(|(role, _)| {
610+
KotlinIdentifier::from_hint(StructuralNameModel::role_name(role))
611+
})
612+
})
613+
.unwrap_or_else(|| KotlinIdentifier::from_hint("value"));
614+
let name = if used.insert(preferred.clone()) {
615+
preferred
616+
} else {
617+
ConstrainedNameSolver::<StructuralNameModel>::claim_variant(&preferred, &mut used)
618+
};
619+
names.insert(variable.identity(), name);
565620
}
566621
}

dexdec/src/language/java/dex.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ impl OuterInstanceBinding {
290290
}
291291
}
292292

293+
fn is_register_style_name(name: &str) -> bool {
294+
name.strip_prefix('v').is_some_and(|digits| {
295+
!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
296+
})
297+
}
298+
293299
impl DexJavaDialect {
294300
pub fn new(
295301
is_static: bool,
@@ -565,13 +571,40 @@ impl DexJavaDialect {
565571
if let Some(name) = self.names.get(&key) {
566572
return Ok(name.clone());
567573
}
568-
let name = self
569-
.name_scope
570-
.claim(JavaIdentifier::from_dex(&format!("v{}", key.raw())));
574+
let name = self.name_scope.claim(self.fallback_local_name(register));
571575
self.names.insert(key, name.clone());
572576
Ok(name)
573577
}
574578

579+
fn fallback_local_name(&self, register: &RegisterArg) -> JavaIdentifier {
580+
self.source_register_type(register)
581+
.and_then(Self::fallback_name_for_type)
582+
.unwrap_or_else(|| JavaIdentifier::from_hint("value"))
583+
}
584+
585+
fn fallback_name_for_type(ty: &JavaType) -> Option<JavaIdentifier> {
586+
match ty {
587+
JavaType::Class(class) => {
588+
let name = &class.segments.last()?.name;
589+
let source = name.as_str();
590+
(source != "Object" && !is_register_style_name(source)).then(|| {
591+
let mut characters = source.chars();
592+
let Some(first) = characters.next() else {
593+
return JavaIdentifier::from_hint("value");
594+
};
595+
let mut lowered = first.to_lowercase().collect::<String>();
596+
lowered.extend(characters);
597+
JavaIdentifier::from_hint(&lowered)
598+
})
599+
}
600+
JavaType::Array(_) => Some(JavaIdentifier::from_hint("values")),
601+
JavaType::Primitive(crate::language::java::JavaPrimitiveType::Boolean) => {
602+
Some(JavaIdentifier::from_hint("flag"))
603+
}
604+
_ => None,
605+
}
606+
}
607+
575608
fn arg(&mut self, arg: &SemanticExpression) -> Result<JavaExpr, JavaLoweringError> {
576609
if matches!(arg, SemanticExpression::Select { .. })
577610
&& self.expression_type(arg)? == &ArgType::BOOLEAN

dexdec/src/language/kotlin/dex.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,12 @@ impl OuterInstanceBinding {
677677
}
678678
}
679679

680+
fn is_register_style_name(name: &str) -> bool {
681+
name.strip_prefix('v').is_some_and(|digits| {
682+
!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
683+
})
684+
}
685+
680686
impl DexKotlinDialect {
681687
pub fn new(
682688
is_static: bool,
@@ -1025,13 +1031,42 @@ impl DexKotlinDialect {
10251031
if let Some(name) = self.names.get(&key) {
10261032
return Ok(name.clone());
10271033
}
1028-
let name = self
1029-
.name_scope
1030-
.claim(KotlinIdentifier::from_dex(&format!("v{}", key.raw())));
1034+
let name = self.name_scope.claim(self.fallback_local_name(register));
10311035
self.names.insert(key, name.clone());
10321036
Ok(name)
10331037
}
10341038

1039+
fn fallback_local_name(&self, register: &RegisterArg) -> KotlinIdentifier {
1040+
self.source_register_type(register)
1041+
.and_then(Self::fallback_name_for_type)
1042+
.unwrap_or_else(|| KotlinIdentifier::from_hint("value"))
1043+
}
1044+
1045+
fn fallback_name_for_type(ty: &KotlinType) -> Option<KotlinIdentifier> {
1046+
match ty {
1047+
KotlinType::Class(class) => {
1048+
let name = &class.segments.last()?.name;
1049+
let source = name.as_str();
1050+
(source != "Any" && source != "Object" && !is_register_style_name(source)).then(
1051+
|| {
1052+
let mut characters = source.chars();
1053+
let Some(first) = characters.next() else {
1054+
return KotlinIdentifier::from_hint("value");
1055+
};
1056+
let mut lowered = first.to_lowercase().collect::<String>();
1057+
lowered.extend(characters);
1058+
KotlinIdentifier::from_hint(&lowered)
1059+
},
1060+
)
1061+
}
1062+
KotlinType::Array(_) => Some(KotlinIdentifier::from_hint("values")),
1063+
KotlinType::Primitive(crate::language::kotlin::KotlinPrimitiveType::Boolean) => {
1064+
Some(KotlinIdentifier::from_hint("flag"))
1065+
}
1066+
_ => None,
1067+
}
1068+
}
1069+
10351070
fn arg(&mut self, arg: &SemanticExpression) -> Result<KotlinExpr, KotlinLoweringError> {
10361071
if matches!(arg, SemanticExpression::Select { .. })
10371072
&& self.expression_type(arg)? == &ArgType::BOOLEAN

dexdec/tests/testcases/expected/BreakContinue.kt.expected

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ public open class BreakContinue {
1515
public fun findPair(values: IntArray, p1: Int): Boolean {
1616
var index: Int = 0
1717
while (index < values.size) {
18-
val v3: Int = index + 1
19-
var index2: Int = v3
18+
val value: Int = index + 1
19+
var index2: Int = value
2020
while (index2 < values.size) {
2121
if (values[index] + values[index2] == p1) {
2222
return true
2323
}
2424
index2++
2525
}
26-
index = v3
26+
index = value
2727
}
2828
return false
2929
}
@@ -33,9 +33,9 @@ public open class BreakContinue {
3333
var result: Int = 0
3434
while (index < values.size) {
3535
if (values[index] > 0) {
36-
val v6: Int = result + values[index]
36+
val value: Int = result + values[index]
3737
index++
38-
result = v6
38+
result = value
3939
} else {
4040
index++
4141
}

dexdec/tests/testcases/expected/ComplexControlFlow.kt.expected

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ public open class ComplexControlFlow {
99
if (p0 > 10) {
1010
return 10
1111
}
12-
val v2: Int = p0 + 1
13-
if (v2 >= 5) {
14-
return v2
12+
val value: Int = p0 + 1
13+
if (value >= 5) {
14+
return value
1515
}
16-
p0 = v2
16+
p0 = value
1717
}
1818
}
1919

@@ -54,10 +54,10 @@ public open class ComplexControlFlow {
5454
}
5555

5656
public fun whileSwitch(p0: Int): Int {
57-
var v1: Int = 0
57+
var value: Int = 0
5858
var total: Int = 0
59-
while (v1 < 5) {
60-
when (p0 + v1) {
59+
while (value < 5) {
60+
when (p0 + value) {
6161
0 -> {
6262
total++
6363
}
@@ -75,9 +75,9 @@ public open class ComplexControlFlow {
7575
if (total > 10) {
7676
return total
7777
}
78-
v1++
78+
value++
7979
} else {
80-
v1++
80+
value++
8181
}
8282
}
8383
return total

dexdec/tests/testcases/expected/DeepNesting.kt.expected

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ public open class DeepNesting {
3434
}
3535

3636
public fun matrixSum(p0: Int, p1: Int): Int {
37-
var v2: Int = 0
37+
var value: Int = 0
3838
var result: Int = 0
39-
while (v2 < p0) {
40-
var v9: Int = 0
41-
while (v9 < p1) {
42-
result = result + v2 * p1 + v9
43-
v9++
39+
while (value < p0) {
40+
var value2: Int = 0
41+
while (value2 < p1) {
42+
result = result + value * p1 + value2
43+
value2++
4444
}
45-
v2++
45+
value++
4646
}
4747
return result
4848
}

dexdec/tests/testcases/expected/ExceptionControlFlow.kt.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ public open class ExceptionControlFlow {
5858
var total: Int = 0
5959
while (index < values!!.size) {
6060
var condition: Boolean = false
61-
var v12: Int = 0
61+
var value: Int = 0
6262
try {
6363
if (values[index] != 0) {
64-
v12 = 100 / values[index]
64+
value = 100 / values[index]
6565
} else {
6666
total += 2
6767
index++
@@ -75,7 +75,7 @@ public open class ExceptionControlFlow {
7575
throw exception
7676
}
7777
if (!condition) {
78-
val step: Int = total + v12
78+
val step: Int = total + value
7979
if (step > 50) {
8080
return step
8181
}

0 commit comments

Comments
 (0)