Skip to content

Commit a1108a6

Browse files
committed
fix(naming): use JADX local-name aliases
Match JADX ApplyVariableNames for type fallbacks (i/str/obj/iArr) and ProcessKotlinInternals for Intrinsics check strings. Keep high-score semantic names when the solver already recovered them.
1 parent 469c950 commit a1108a6

28 files changed

Lines changed: 695 additions & 408 deletions
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//! Local names that follow JADX's `ApplyVariableNames` and
2+
//! `ProcessKotlinInternals` rules.
3+
//!
4+
//! JADX never falls back to a DEX register (`v12`). It names a local from a
5+
//! Kotlin `Intrinsics` check string when one exists, otherwise from the value's
6+
//! type or defining call.
7+
8+
use std::collections::BTreeMap;
9+
10+
use crate::ir::{
11+
ArgType, InsnType, LiteralArg, MemberReference, PrimitiveType, SemanticExpression,
12+
SemanticNode, SemanticOperation, SemanticVisitor,
13+
};
14+
15+
const OBJECT_ALIASES: &[(&str, &str)] = &[
16+
("java.lang.String", "str"),
17+
("kotlin.String", "str"),
18+
("java.lang.Class", "cls"),
19+
("java.lang.Throwable", "th"),
20+
("java.lang.Object", "obj"),
21+
("kotlin.Any", "obj"),
22+
("java.util.Iterator", "it"),
23+
("kotlin.collections.Iterator", "it"),
24+
("java.util.HashMap", "map"),
25+
("java.lang.Boolean", "bool"),
26+
("java.lang.Short", "sh"),
27+
("java.lang.Integer", "num"),
28+
("java.lang.Character", "ch"),
29+
("java.lang.Byte", "b"),
30+
("java.lang.Float", "f"),
31+
("java.lang.Long", "l"),
32+
("java.lang.Double", "d"),
33+
("java.lang.StringBuilder", "sb"),
34+
("java.lang.Exception", "exc"),
35+
];
36+
37+
const INVOKE_PREFIXES: &[&str] = &["get", "set", "to", "parse", "read", "format"];
38+
39+
pub(crate) fn primitive_local_name(primitive: PrimitiveType) -> Option<&'static str> {
40+
match primitive {
41+
PrimitiveType::Boolean => Some("z"),
42+
PrimitiveType::Byte => Some("b"),
43+
PrimitiveType::Short => Some("s"),
44+
PrimitiveType::Char => Some("c"),
45+
PrimitiveType::Int => Some("i"),
46+
PrimitiveType::Long => Some("j"),
47+
PrimitiveType::Float => Some("f"),
48+
PrimitiveType::Double => Some("d"),
49+
PrimitiveType::Void | PrimitiveType::Object | PrimitiveType::Array => None,
50+
}
51+
}
52+
53+
pub(crate) fn object_alias(qualified: &str) -> Option<&'static str> {
54+
OBJECT_ALIASES
55+
.iter()
56+
.find(|(name, _)| *name == qualified)
57+
.map(|(_, alias)| *alias)
58+
}
59+
60+
pub(crate) fn class_local_name(simple: &str) -> String {
61+
if simple.is_empty() {
62+
return "obj".to_string();
63+
}
64+
let alphabetic = simple
65+
.chars()
66+
.filter(|character| character.is_alphabetic())
67+
.collect::<Vec<_>>();
68+
if !alphabetic.is_empty() && alphabetic.iter().all(|character| character.is_uppercase()) {
69+
return simple.to_lowercase();
70+
}
71+
let mut characters = simple.chars();
72+
let Some(first) = characters.next() else {
73+
return "obj".to_string();
74+
};
75+
let mut lowered = first.to_lowercase().collect::<String>();
76+
lowered.extend(characters);
77+
if lowered != simple {
78+
return lowered;
79+
}
80+
format!("{simple}Var")
81+
}
82+
83+
pub(crate) fn array_local_name(element: &str) -> String {
84+
format!("{element}Arr")
85+
}
86+
87+
pub(crate) fn class_local_name_from_segments<'a>(
88+
segments: impl IntoIterator<Item = &'a str>,
89+
) -> String {
90+
let segments = segments.into_iter().collect::<Vec<_>>();
91+
let qualified = segments.join(".");
92+
if let Some(alias) = object_alias(&qualified) {
93+
return alias.to_string();
94+
}
95+
class_local_name(segments.last().copied().unwrap_or(""))
96+
}
97+
98+
pub(crate) fn trim_intrinsic_name(name: &str) -> &str {
99+
name.strip_prefix("$this$")
100+
.or_else(|| name.strip_prefix('$'))
101+
.unwrap_or(name)
102+
}
103+
104+
pub(crate) fn invoke_local_name(method: &str) -> Option<String> {
105+
if method == "iterator" {
106+
return Some("it".to_string());
107+
}
108+
if method == "getInstance" {
109+
return None;
110+
}
111+
for prefix in INVOKE_PREFIXES {
112+
if let Some(rest) = method.strip_prefix(prefix) {
113+
if rest
114+
.chars()
115+
.next()
116+
.is_some_and(|character| character.is_ascii_uppercase())
117+
{
118+
return Some(class_local_name(rest));
119+
}
120+
}
121+
}
122+
None
123+
}
124+
125+
pub(crate) fn intrinsic_local_names(root: &SemanticNode) -> BTreeMap<u32, String> {
126+
let mut collector = IntrinsicNameCollector::default();
127+
collector.visit_node(root);
128+
collector.names
129+
}
130+
131+
#[derive(Default)]
132+
struct IntrinsicNameCollector {
133+
names: BTreeMap<u32, String>,
134+
}
135+
136+
impl SemanticVisitor for IntrinsicNameCollector {
137+
fn enter_operation(&mut self, operation: &SemanticOperation) {
138+
if let Some((variable, name)) = intrinsic_binding(operation) {
139+
self.names.entry(variable).or_insert(name);
140+
}
141+
}
142+
}
143+
144+
fn intrinsic_binding(operation: &SemanticOperation) -> Option<(u32, String)> {
145+
if operation.insn_type != InsnType::Invoke {
146+
return None;
147+
}
148+
let MemberReference::Method(method) = operation.payload.reference.as_ref()? else {
149+
return None;
150+
};
151+
if !is_kotlin_varname_source(method) {
152+
return None;
153+
}
154+
let operands = operation.operands();
155+
if operands.len() < 2 {
156+
return None;
157+
}
158+
let variable = register_variable(&operands[0])?;
159+
let name = const_string(operands.last()?)?;
160+
let name = trim_intrinsic_name(&name);
161+
if name.is_empty() {
162+
return None;
163+
}
164+
Some((variable, name.to_string()))
165+
}
166+
167+
fn is_kotlin_varname_source(method: &crate::ir::MethodReference) -> bool {
168+
let parameters = &method.descriptor.parameters;
169+
if method.descriptor.return_type != ArgType::VOID {
170+
return false;
171+
}
172+
let string = ArgType::object("java/lang/String");
173+
let object = ArgType::object("java/lang/Object");
174+
let signature_ok = matches!(
175+
parameters.as_slice(),
176+
[left, right] if left == &object && right == &string
177+
) || matches!(
178+
parameters.as_slice(),
179+
[left, middle, right] if left == &object && middle == &string && right == &string
180+
);
181+
if !signature_ok {
182+
return false;
183+
}
184+
let owner = method.owner.as_object().unwrap_or("");
185+
owner == "kotlin/jvm/internal/Intrinsics"
186+
|| owner.ends_with("/Intrinsics")
187+
|| method.name.starts_with("checkNotNull")
188+
|| method.name.starts_with("checkParameterIsNotNull")
189+
|| method.name.starts_with("checkExpressionValueIsNotNull")
190+
}
191+
192+
fn register_variable(expression: &SemanticExpression) -> Option<u32> {
193+
match expression {
194+
SemanticExpression::Register(register) => register.code_var,
195+
_ => None,
196+
}
197+
}
198+
199+
fn const_string(expression: &SemanticExpression) -> Option<String> {
200+
match expression {
201+
SemanticExpression::Operation(operation) if operation.insn_type == InsnType::ConstStr => {
202+
Some(operation.payload.string_value.as_ref()?.to_string_lossy())
203+
}
204+
SemanticExpression::Literal(LiteralArg { .. }) => None,
205+
_ => None,
206+
}
207+
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use super::*;
212+
213+
#[test]
214+
fn primitive_names_match_jadx_short_names() {
215+
assert_eq!(primitive_local_name(PrimitiveType::Int), Some("i"));
216+
assert_eq!(primitive_local_name(PrimitiveType::Boolean), Some("z"));
217+
assert_eq!(primitive_local_name(PrimitiveType::Long), Some("j"));
218+
}
219+
220+
#[test]
221+
fn object_aliases_match_jadx() {
222+
assert_eq!(object_alias("java.lang.String"), Some("str"));
223+
assert_eq!(object_alias("java.lang.Object"), Some("obj"));
224+
assert_eq!(object_alias("kotlin.Any"), Some("obj"));
225+
}
226+
227+
#[test]
228+
fn class_and_array_names_match_jadx() {
229+
assert_eq!(class_local_name("FooBar"), "fooBar");
230+
assert_eq!(class_local_name("HTML"), "html");
231+
assert_eq!(class_local_name("v0"), "v0Var");
232+
assert_eq!(array_local_name("i"), "iArr");
233+
assert_eq!(
234+
class_local_name_from_segments(["java", "lang", "String"]),
235+
"str"
236+
);
237+
}
238+
239+
#[test]
240+
fn intrinsic_and_invoke_names_match_jadx() {
241+
assert_eq!(trim_intrinsic_name("$this$foo"), "foo");
242+
assert_eq!(trim_intrinsic_name("$view"), "view");
243+
assert_eq!(invoke_local_name("getView"), Some("view".to_string()));
244+
assert_eq!(invoke_local_name("iterator"), Some("it".to_string()));
245+
}
246+
}

dexdec/src/analysis/java_backend/semantic_naming.rs

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use std::collections::{BTreeMap, BTreeSet};
22

3+
use crate::analysis::jadx_local_names;
34
use crate::ir::{
45
analysis::{
56
StructuralVariableRoleAnalysis, VariableNode, VariableRole, VariableRoleAnalysis,
67
VariableRoleScores, VariableSemanticGraph,
78
},
8-
ArgType, InsnType, MemberReference, SemanticNode,
9+
ArgType, InsnType, MemberReference, PrimitiveType, SemanticNode,
910
};
10-
use crate::language::java::{JavaIdentifier, JavaType};
11+
use crate::language::java::{JavaIdentifier, JavaPrimitiveType, JavaType};
1112

1213
use super::type_names::JavaTypeNameResolver;
1314

@@ -106,15 +107,9 @@ impl MethodResultSemantics for JavaMethodResultSemantics {
106107
return None;
107108
}
108109
let identifier = JavaIdentifier::from_hint(name);
109-
if let Some(noun) = name.strip_prefix("get").filter(|noun| {
110-
noun.chars()
111-
.next()
112-
.is_some_and(|character| character.is_ascii_uppercase())
113-
}) {
110+
if let Some(name) = jadx_local_names::invoke_local_name(name) {
114111
return Some(NameCandidate {
115-
name: self
116-
.morphology
117-
.lower_camel(&JavaIdentifier::from_hint(noun)),
112+
name: JavaIdentifier::from_hint(&name),
118113
score: 85,
119114
});
120115
}
@@ -188,23 +183,7 @@ impl<'a> StructuralNameModel<'a> {
188183
}
189184

190185
fn java_type_name(&self, ty: &JavaType) -> Option<JavaIdentifier> {
191-
match ty {
192-
JavaType::Class(class) => {
193-
let name = &class.segments.last()?.name;
194-
let source = name.as_str();
195-
(source != "Object" && !is_register_style_name(source))
196-
.then(|| self.morphology.lower_camel(name))
197-
}
198-
JavaType::Array(element) => self
199-
.java_type_name(element)
200-
.map(|name| self.morphology.plural(&name))
201-
.or_else(|| Some(JavaIdentifier::from_hint("values"))),
202-
JavaType::Variable(name) => Some(self.morphology.lower_camel(name)),
203-
JavaType::Primitive(crate::language::java::JavaPrimitiveType::Boolean) => {
204-
Some(JavaIdentifier::from_hint("flag"))
205-
}
206-
JavaType::Primitive(_) => None,
207-
}
186+
Some(JavaIdentifier::from_hint(&java_jadx_type_name(ty)?))
208187
}
209188

210189
fn lower_camel(name: &JavaIdentifier) -> JavaIdentifier {
@@ -429,9 +408,7 @@ impl<'a> ParameterNameRecovery<'a> {
429408
}
430409

431410
pub(super) fn candidate(&self, ty: &ArgType) -> Option<JavaIdentifier> {
432-
ty.is_reference()
433-
.then(|| self.model.type_name(ty))
434-
.flatten()
411+
self.model.type_name(ty)
435412
}
436413
}
437414

@@ -552,16 +529,23 @@ impl<'a> SemanticNameRecovery<'a> {
552529
) -> BTreeMap<u32, JavaIdentifier> {
553530
let graph = VariableSemanticGraph::analyze(root, source_types);
554531
let roles = StructuralVariableRoleAnalysis.analyze(&graph);
532+
let intrinsic_names = jadx_local_names::intrinsic_local_names(root);
555533
let excluded = parameter_variables
556534
.iter()
557535
.copied()
558536
.flatten()
559537
.chain(this_variable)
538+
.chain(intrinsic_names.keys().copied())
560539
.collect::<BTreeSet<_>>();
561540
let model = StructuralNameModel::for_graph(self.types, &graph);
562541
let mut names =
563542
ConstrainedNameSolver::new(StructuralNameModel::for_graph(self.types, &graph), 35)
564543
.solve(&graph, &roles, parameter_names, &excluded);
544+
for (identity, name) in intrinsic_names {
545+
names
546+
.entry(identity)
547+
.or_insert_with(|| JavaIdentifier::from_hint(&name));
548+
}
565549
fill_remaining_source_names(
566550
&graph,
567551
&roles,
@@ -574,10 +558,33 @@ impl<'a> SemanticNameRecovery<'a> {
574558
}
575559
}
576560

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-
})
561+
fn java_jadx_type_name(ty: &JavaType) -> Option<String> {
562+
match ty {
563+
JavaType::Primitive(primitive) => {
564+
jadx_local_names::primitive_local_name(java_primitive(*primitive)).map(str::to_string)
565+
}
566+
JavaType::Class(class) => Some(jadx_local_names::class_local_name_from_segments(
567+
class.segments.iter().map(|segment| segment.name.as_str()),
568+
)),
569+
JavaType::Variable(name) => Some(jadx_local_names::class_local_name(name.as_str())),
570+
JavaType::Array(element) => Some(jadx_local_names::array_local_name(&java_jadx_type_name(
571+
element,
572+
)?)),
573+
}
574+
}
575+
576+
fn java_primitive(primitive: JavaPrimitiveType) -> PrimitiveType {
577+
match primitive {
578+
JavaPrimitiveType::Void => PrimitiveType::Void,
579+
JavaPrimitiveType::Boolean => PrimitiveType::Boolean,
580+
JavaPrimitiveType::Byte => PrimitiveType::Byte,
581+
JavaPrimitiveType::Short => PrimitiveType::Short,
582+
JavaPrimitiveType::Char => PrimitiveType::Char,
583+
JavaPrimitiveType::Int => PrimitiveType::Int,
584+
JavaPrimitiveType::Long => PrimitiveType::Long,
585+
JavaPrimitiveType::Float => PrimitiveType::Float,
586+
JavaPrimitiveType::Double => PrimitiveType::Double,
587+
}
581588
}
582589

583590
fn fill_remaining_source_names(

0 commit comments

Comments
 (0)