mod jdk;
pub use jdk::set_jdk_home;
mod vexec;
use cure_engine::kind::{BinOp, UnOp};
use cure_engine::{Config, Edit, Effect, Lang, LitRef, NodeKind, Report, RewriteCtx, Rule};
use cure_java_ast::{CompilationUnit, JavaAst, JavaId, JType, Lit, Member, NoopCandidate, NodeData, TypeDecl, TypeKind};
pub struct CastSimplify;
impl Rule<JavaAst> for CastSimplify {
fn name(&self) -> &'static str {
"cast_simplify"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Cast]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
let NodeData::Cast { ty } = lang.data(id) else {
return None;
};
let ty = ty.clone();
let inner = lang.children(id)[0];
if let NodeData::Cast { ty: inner_ty } = lang.data(inner) {
if *inner_ty == ty {
return Some(Edit::Replace {
target: id,
with: inner,
});
}
}
if let NodeData::VarRef { .. } = lang.data(inner) {
if lang.var_type(inner) == Some(&ty) {
return Some(Edit::Replace {
target: id,
with: inner,
});
}
}
if let NodeData::Cast { ty: inner_cast_ty } = lang.data(inner) {
let is_obj = matches!(inner_cast_ty,
JType::Ref(r) if r == "java.lang.Object" || r == "Object");
if is_obj {
let operand = lang.children(inner).first().copied()?;
let operand_is_object = match lang.data(operand) {
NodeData::Literal(_) | NodeData::This => true,
NodeData::VarRef { .. } => {
matches!(lang.var_type(operand),
Some(JType::Ref(r)) if r == "java.lang.Object" || r == "Object")
}
_ => false,
};
if operand_is_object {
return Some(Edit::Replace {
target: inner,
with: operand,
});
}
}
}
None
}
}
pub struct SelfCompare;
impl Rule<JavaAst> for SelfCompare {
fn name(&self) -> &'static str {
"self_compare"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Binary]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
use cure_engine::kind::BinOp::{Eq, Ne};
let lang = ctx.lang;
let NodeData::Binary { op } = lang.data(id) else {
return None;
};
if !matches!(op, Eq | Ne) {
return None;
}
let ch = lang.children(id);
let (NodeData::VarRef { name: n1 }, NodeData::VarRef { name: n2 }) =
(lang.data(ch[0]), lang.data(ch[1]))
else {
return None;
};
if n1 != n2 || !lang.is_exact_int(ch[0]) {
return None;
}
let val = lang.build_bool(*op == Eq);
Some(Edit::Replace {
target: id,
with: val,
})
}
}
pub struct StringBuilderFold;
impl Rule<JavaAst> for StringBuilderFold {
fn name(&self) -> &'static str {
"string_builder_fold"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Call]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let root = ctx.root();
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Call {
return None;
}
let callee = lang.children(id)[0];
if let NodeData::Member { name } = lang.data(callee) {
if lang.sn(*name) != "toString" || !lang.children(id)[1..].is_empty() {
return None;
}
} else {
return None;
}
let mut node = lang.children(callee)[0]; let mut parts: Vec<JavaId> = Vec::new();
loop {
match lang.data(node) {
NodeData::Call => {
let c = lang.children(node);
if let NodeData::Member { name } = lang.data(c[0]) {
if lang.sn(*name) == "append" && c.len() == 2 {
parts.push(c[1]);
node = lang.children(c[0])[0];
continue;
}
}
return None;
}
NodeData::New { ty, .. } => {
let is_sb = !lang.unit_shadows_lang
&& matches!(ty, JType::Ref(n) if n == "StringBuilder" || n == "java.lang.StringBuilder");
if !is_sb {
return None;
}
let args = lang.children(node).to_vec();
match args.len() {
0 => {} 1 => {
let a = args[0];
let stringy = matches!(lang.literal(a), Some(LitRef::Str(_)))
|| lang
.var_type(a)
.is_some_and(|t| matches!(t, JType::Ref(n) if n == "String"));
if stringy {
parts.push(a);
} else if !matches!(
lang.literal(a),
Some(LitRef::Int(_) | LitRef::Long(_))
) {
return None;
}
}
_ => return None,
}
break;
}
_ => return None,
}
}
if parts.iter().all(|&p| lang.literal(p).is_some())
&& lang.has_string_identity_compare(root)
{
return None;
}
if parts.is_empty() {
let empty = lang.build_str("");
return Some(Edit::Replace {
target: id,
with: empty,
});
}
parts.reverse();
let first = parts[0];
let stringy_first = matches!(lang.literal(first), Some(LitRef::Str(_)))
|| lang
.var_type(first)
.is_some_and(|t| matches!(t, JType::Ref(n) if n == "String"));
let mut acc = if stringy_first {
first
} else {
let empty = lang.build_str("");
lang.build_bin(BinOp::Add, empty, first)
};
for &p in &parts[1..] {
acc = lang.build_bin(BinOp::Add, acc, p);
}
Some(Edit::Replace {
target: id,
with: acc,
})
}
}
pub struct BoxUnboxChain;
const BOXERS: &[(&str, &str)] = &[
("Integer", "intValue"),
("java.lang.Integer", "intValue"),
("Long", "longValue"),
("java.lang.Long", "longValue"),
("Boolean", "booleanValue"),
("java.lang.Boolean", "booleanValue"),
("Double", "doubleValue"),
("java.lang.Double", "doubleValue"),
("Float", "floatValue"),
("java.lang.Float", "floatValue"),
("Character", "charValue"),
("java.lang.Character", "charValue"),
("Short", "shortValue"),
("java.lang.Short", "shortValue"),
("Byte", "byteValue"),
("java.lang.Byte", "byteValue"),
];
impl Rule<JavaAst> for BoxUnboxChain {
fn name(&self) -> &'static str {
"box_unbox_chain"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Call]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Call {
return None;
}
let callee = lang.children(id)[0];
let NodeData::Member { name: unbox } = lang.data(callee) else {
return None;
};
if !lang.children(id)[1..].is_empty() {
return None;
}
let inner = lang.children(callee)[0];
if lang.kind(inner) != NodeKind::Call {
return None;
}
let ich = lang.children(inner).to_vec();
let (box_name, arg): (String, JavaId) = if let NodeData::New { ty, .. } = lang.data(inner)
{
let n = match ty {
JType::Ref(r) => r.clone(),
_ => return None,
};
if ich.len() != 1 {
return None;
}
(n, ich[0])
} else {
let icallee = ich.first().copied()?;
if let NodeData::Member { name } = lang.data(icallee) {
let recv = lang.children(icallee)[0];
if lang.sn(*name) != "valueOf" {
return None;
}
{
let n = lang.var_name(recv)?;
(n.to_string(), *ich.get(1)?)
}
} else if let NodeData::VarRef { name } = lang.data(icallee) {
let _ = name;
return None;
} else {
return None;
}
};
let unbox_name = *unbox;
let expected = BOXERS
.iter()
.find(|(b, _)| *b == box_name.as_str())
.map(|(_, u)| *u)?;
if expected != lang.sn(unbox_name) {
return None;
}
let var_is = |t: fn(&JType) -> bool| lang.var_type(arg).is_some_and(t);
let lit_is_num = lang.literal(arg).is_some_and(|l| {
matches!(
l,
LitRef::Int(_) | LitRef::Long(_) | LitRef::Float(_) | LitRef::Double(_)
)
});
let ok = match box_name.trim_start_matches("java.lang.") {
"Integer" | "Long" | "Short" | "Byte" => lang.is_exact_int(arg),
"Double" | "Float" => {
lit_is_num
|| var_is(|t| matches!(t, JType::Double | JType::Float | JType::Int | JType::Long))
}
"Boolean" => lang.is_bool(arg),
"Character" => {
matches!(lang.literal(arg), Some(LitRef::Char(_))) || var_is(|t| *t == JType::Char)
}
_ => false,
};
if !ok {
return None;
}
Some(Edit::Replace {
target: id,
with: arg,
})
}
}
pub struct IteratorToForEach;
impl Rule<JavaAst> for IteratorToForEach {
fn name(&self) -> &'static str {
"iterator_to_for_each"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::For]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
let NodeData::For {
inits,
has_cond,
steps,
} = lang.data(id)
else {
return None;
};
if *inits != 1 || !*has_cond || *steps != 0 {
return None;
}
let ch = lang.children(id).to_vec();
let (init, cond, body) = (ch[0], ch[1], ch[2]);
let NodeData::VarDecl { name: it_name, .. } = lang.data(init) else {
return None;
};
let it_name = it_name.clone();
let init_call = lang.children(init).first().copied()?;
if lang.kind(init_call) != NodeKind::Call {
return None;
}
let ic = lang.children(init_call).to_vec();
let icallee = *ic.first()?;
let NodeData::Member { name: m0 } = lang.data(icallee) else {
return None;
};
if lang.sn(*m0) != "iterator" || ic.len() != 1 {
return None;
}
let iterable = lang.children(icallee)[0];
if subtree_has_var(&*lang, iterable, &it_name) {
return None;
}
if lang.kind(cond) != NodeKind::Call {
return None;
}
let cc = lang.children(cond).to_vec();
let ccallee = *cc.first()?;
let NodeData::Member { name: m1 } = lang.data(ccallee) else {
return None;
};
if lang.sn(*m1) != "hasNext" || cc.len() != 1 {
return None;
}
if lang.var_name(lang.children(ccallee)[0]) != Some(it_name.as_str()) {
return None;
}
if lang.kind(body) != NodeKind::Block {
return None;
}
let bch = lang.children(body).to_vec();
let first = *bch.first()?;
let NodeData::VarDecl { name: e_name, ty, .. } = lang.data(first) else {
return None;
};
let (e_name, ty) = (e_name.clone(), ty.clone());
let next_call = lang.children(first).first().copied()?;
if lang.kind(next_call) != NodeKind::Call {
return None;
}
let nc = lang.children(next_call).to_vec();
let ncallee = *nc.first()?;
let NodeData::Member { name: m2 } = lang.data(ncallee) else {
return None;
};
if lang.sn(*m2) != "next" || nc.len() != 1 {
return None;
}
if lang.var_name(lang.children(ncallee)[0]) != Some(it_name.as_str()) {
return None;
}
let rest = bch[1..].to_vec();
for &r in &rest {
if subtree_has_var(&*lang, r, &it_name) {
return None;
}
}
let new_body = lang.build_block(rest);
let with = lang.for_each(&e_name, ty, iterable, new_body);
Some(Edit::Replace {
target: id,
with,
})
}
}
fn unwrap_paren_cast(lang: &JavaAst, mut n: JavaId) -> JavaId {
loop {
match lang.kind(n) {
NodeKind::Paren | NodeKind::Cast => {
let ch = lang.children(n);
if ch.is_empty() {
return n;
}
n = ch[0];
}
_ => return n,
}
}
}
fn generic_elem_ty(ty: &JType) -> Option<JType> {
let name = match ty {
JType::Ref(n) => n.as_str(),
_ => return None,
};
let lt = name.find('<')?;
let gt = name.rfind('>')?;
if gt <= lt {
return None;
}
let inner = name[lt + 1..gt].trim();
if inner.is_empty() || inner.contains('<') {
return None; }
Some(JType::Ref(inner.to_string()))
}
fn statically_iterable(ty: &JType) -> bool {
match ty {
JType::Array(_) => true,
JType::Ref(name) => {
let base = name.split('<').next().unwrap_or(name).trim();
let last = base.rsplit('.').next().unwrap_or(base);
matches!(
last,
"Iterable"
| "Collection"
| "List"
| "ArrayList"
| "LinkedList"
| "Vector"
| "Stack"
| "AbstractList"
| "AbstractCollection"
| "Set"
| "HashSet"
| "LinkedHashSet"
| "TreeSet"
| "SortedSet"
| "NavigableSet"
| "AbstractSet"
| "Queue"
| "Deque"
| "ArrayDeque"
| "PriorityQueue"
| "BlockingQueue"
| "BlockingDeque"
| "ConcurrentLinkedQueue"
| "ConcurrentLinkedDeque"
| "ConcurrentSkipListSet"
| "CopyOnWriteArrayList"
| "CopyOnWriteArraySet"
| "EnumSet"
| "LinkedBlockingQueue"
| "LinkedBlockingDeque"
| "ArrayBlockingQueue"
| "PriorityBlockingQueue"
| "DelayQueue"
| "SynchronousQueue"
| "LinkedTransferQueue"
| "TransferQueue"
)
}
_ => false,
}
}
fn map_view_synthetic_ty(lang: &JavaAst, call: JavaId) -> Option<JType> {
if lang.kind(call) != NodeKind::Call {
return None;
}
let ch = lang.children(call).to_vec();
if ch.len() != 1 {
return None; }
let callee = *ch.first()?;
let NodeData::Member { name } = lang.data(callee) else { return None };
let view = lang.sn(*name);
if !matches!(view, "keySet" | "values") {
return None;
}
let recv = lang.children(callee).first().copied()?;
let NodeData::VarRef { name } = lang.data(recv) else { return None };
let recv_ty = lang
.var_type(recv)
.cloned()
.or_else(|| lang.field_types.get(lang.sn(*name)).cloned())?;
let JType::Ref(n) = recv_ty else { return None };
let lt = n.find('<')?;
let gt = n.rfind('>')?;
if gt <= lt {
return None; }
let args = n[lt + 1..gt].trim();
let depth = args.chars().fold(0i32, |d, c| match c {
'<' => d + 1,
'>' => d - 1,
_ => d,
});
let top = args.find(',').filter(|_| depth == 0)?;
let k = args[..top].trim();
let v = args[top + 1..].trim();
let base = n[..lt].trim();
let last = base.rsplit('.').next().unwrap_or(base);
let map_family = matches!(
last,
"Map"
| "SortedMap"
| "NavigableMap"
| "HashMap"
| "LinkedHashMap"
| "TreeMap"
| "Hashtable"
| "Properties"
| "ConcurrentMap"
| "ConcurrentHashMap"
| "ConcurrentSkipListMap"
| "ConcurrentNavigableMap"
| "EnumMap"
| "WeakHashMap"
| "IdentityHashMap"
| "AbstractMap"
);
if !map_family || k.is_empty() || v.is_empty() {
return None;
}
match view {
"keySet" => Some(JType::Ref(format!("java.util.Set<{k}>"))),
_ => Some(JType::Ref(format!("java.util.Collection<{v}>"))),
}
}
fn for_each_elem_ok(ty: &JType) -> bool {
match ty {
JType::Ref(n) => !n.trim_start().starts_with('?'),
_ => true,
}
}
fn collect_var_refs(lang: &JavaAst, root: JavaId, name: &str, out: &mut Vec<JavaId>) {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
if lang.kind(n) == NodeKind::VarRef && lang.var_name(n) == Some(name) {
out.push(n);
}
for &c in lang.children(n) {
stack.push(c);
}
}
}
fn parent_of_recv(lang: &JavaAst, root: JavaId, target: JavaId) -> JavaId {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
for &c in lang.children(n) {
if c == target {
return n;
}
stack.push(c);
}
}
root
}
fn subtree_has_var(lang: &JavaAst, id: JavaId, name: &str) -> bool {
let mut stack = vec![id];
while let Some(n) = stack.pop() {
if lang.kind(n) == NodeKind::VarRef && lang.var_name(n) == Some(name) {
return true;
}
for &c in lang.children(n) {
stack.push(c);
}
}
false
}
fn subtree_has_binding(lang: &JavaAst, id: JavaId, name: &str) -> bool {
let mut stack = vec![id];
while let Some(n) = stack.pop() {
match lang.data(n) {
NodeData::Catch { name: cn, .. } if cn == name => return true,
NodeData::ForEach { name: vn, .. } if vn == name => return true,
NodeData::Lambda { params_raw } => {
for part in
params_raw.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$')
{
if part == name {
return true;
}
}
}
NodeData::InstanceOf { bind: Some(b), .. } if b == name => return true,
NodeData::VarDecl { name: dn, .. } if dn == name => return true,
_ => {}
}
for &c in lang.children(n) {
stack.push(c);
}
}
false
}
pub struct NewStringFold;
impl Rule<JavaAst> for NewStringFold {
fn name(&self) -> &'static str {
"new_string_fold"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::New]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let root = ctx.root();
let lang = ctx.lang;
let NodeData::New { ty, .. } = lang.data(id) else {
return None;
};
if !matches!(ty, JType::Ref(n) if n == "String" || n == "java.lang.String") {
return None;
}
if lang.unit_shadows_lang {
return None;
}
let _ = root;
if lang.unit_string_identity {
return None;
}
let args = lang.children(id).to_vec();
match args.len() {
0 => {
let empty = lang.build_str("");
Some(Edit::Replace {
target: id,
with: empty,
})
}
1 => {
if matches!(lang.literal(args[0]), Some(LitRef::Str(_))) {
Some(Edit::Replace {
target: id,
with: args[0],
})
} else {
None
}
}
_ => None,
}
}
}
pub struct NewStringCharArrayFold;
impl Rule<JavaAst> for NewStringCharArrayFold {
fn name(&self) -> &'static str {
"new_string_char_array_fold"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::New]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let root = ctx.root();
let lang = ctx.lang;
let NodeData::New { ty, .. } = lang.data(id) else {
return None;
};
if !matches!(ty, JType::Ref(n) if n == "String" || n == "java.lang.String") {
return None;
}
let _ = root;
if lang.unit_shadows_lang || lang.unit_string_identity {
return None;
}
let args = lang.children(id).to_vec();
if args.len() != 1 {
return None;
}
let NodeData::NewArray { sized, .. } = lang.data(args[0]) else {
return None;
};
if *sized != 0 {
return None;
}
let arr_children = lang.children(args[0]).to_vec();
let lit = *arr_children.first()?;
if lang.kind(lit) != NodeKind::ArrayLit {
return None;
}
let mut text = String::new();
for &c in lang.children(lit) {
match lang.literal(c) {
Some(LitRef::Char(ch)) => text.push(ch),
_ => return None, }
}
let with = lang.build_str(&text);
Some(Edit::Replace {
target: id,
with,
})
}
}
pub struct EmptyFinallyStrip;
impl Rule<JavaAst> for EmptyFinallyStrip {
fn name(&self) -> &'static str {
"empty_finally_strip"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Try]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Try {
return None;
}
let ch = lang.children(id).to_vec();
let is_empty_block = |n: JavaId| -> bool {
lang.kind(n) == NodeKind::Block && lang.children(n).is_empty()
};
let try_idx = ch.iter().position(|&c| lang.kind(c) == NodeKind::Block)?;
if ch.len() < try_idx + 2 {
return None; }
let last = *ch.last()?;
if lang.kind(last) == NodeKind::Catch {
return None;
}
if !is_empty_block(last) {
return None;
}
Some(Edit::Splice {
node: id,
index: ch.len() - 1,
remove: 1,
insert: Vec::new(),
})
}
}
fn print_equiv(lang: &JavaAst, a: JavaId, b: JavaId) -> bool {
if lang.kind(a) != lang.kind(b) {
let (ia, ib) = (lang.kind(a), lang.kind(b));
let unwrap = |k: NodeKind, x: JavaId| -> Option<JavaId> {
if k == NodeKind::ExprStmt {
lang.children(x).first().copied()
} else {
Some(x)
}
};
return match (unwrap(ia, a), unwrap(ib, b)) {
(Some(x), Some(y)) => print_equiv_inner(lang, x, y),
_ => false,
};
}
print_equiv_inner(lang, a, b)
}
fn print_equiv_inner(lang: &JavaAst, a: JavaId, b: JavaId) -> bool {
if lang.kind(a) != lang.kind(b) {
return false;
}
match (lang.literal(a), lang.literal(b)) {
(Some(x), Some(y)) => {
if x != y {
return false;
}
}
(None, None) => {}
_ => return false,
}
let (ca, cb) = (lang.children(a), lang.children(b));
if ca.len() != cb.len() {
return false;
}
ca.iter().zip(cb.iter()).all(|(&x, &y)| print_equiv_inner(lang, x, y))
}
fn rest_reassigns_field(lang: &JavaAst, rest: &[JavaId], name: &str) -> bool {
for &root in rest {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
match lang.kind(n) {
NodeKind::Assign => {
let t = lang.children(n)[0];
if lang.kind(t) == NodeKind::VarRef && lang.var_name(t) == Some(name) {
return true;
}
}
NodeKind::Unary => {
if let NodeData::Unary { op } = lang.data(n) {
if op.is_incdec() {
let t = lang.children(n)[0];
if lang.kind(t) == NodeKind::VarRef
&& lang.var_name(t) == Some(name)
{
return true;
}
}
}
}
_ => {}
}
for &c in lang.children(n) {
stack.push(c);
}
}
}
false
}
pub struct StaticExec;
impl Rule<JavaAst> for StaticExec {
fn name(&self) -> &'static str {
"static_exec"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Block]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
if ctx.parent(id).is_some() {
return None;
}
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Block {
return None;
}
let stmts = lang.children(id).to_vec();
if stmts.len() < 3 {
return None; }
let mut ex = vexec::Exec::new(lang);
let run = ex.run_prefix(&stmts).ok()?;
if std::env::var("CURE_DBG_P1").is_ok() {
eprintln!(
"[P1-se] run ok: cuts={} total_completed={} budget={} log_len={} root_cut=({},{}) events={:?}",
run.cuts.len(),
run.cuts.iter().map(|c| c.completed).sum::<usize>(),
ex.budget_exhausted(),
ex.effect_log().len(),
run.cuts.last().map(|c| c.log_start).unwrap_or(0),
run.cuts.last().map(|c| c.log_end).unwrap_or(0),
ex.effect_log().iter().map(|e| match e {
vexec::EffectEvent::FieldWrite(_) => "FW",
vexec::EffectEvent::ClassLoad { .. } => "CL",
vexec::EffectEvent::OpaqueFieldWrite { .. } => "OFW",
}).collect::<Vec<_>>()
);
}
if ex.budget_exhausted() {
return None;
}
let root_idx = run.root_cut_index();
let total_completed: usize = run.cuts.iter().map(|c| c.completed).sum();
if total_completed < 3 {
return None;
}
let true_rest = run.true_rest();
if !true_rest.is_empty() {
for ev in ex.effect_log().iter() {
if let vexec::EffectEvent::FieldWrite(k) = ev {
if let Some(name) = lang.name_of_key(*k) {
if lang.final_fields.contains(&name)
&& rest_reassigns_field(lang, &true_rest, &name)
{
return None;
}
}
}
}
}
let log: Vec<vexec::EffectEvent> = ex.effect_log().to_vec();
let field_finals: Vec<(u32, vexec::VVal)> =
ex.field_writes().iter().map(|(k, v)| (*k, v.clone())).collect();
let decl_order: Vec<u32> = ex.decl_order().to_vec();
let ret_payload: Option<Option<vexec::VVal>> = match &run.early_exit {
Some(vexec::Flow::Return(v)) => Some(v.clone()),
_ => None,
};
let mut escaped_by_cut: Vec<Vec<(u32, vexec::VVal, u8, JType)>> = Vec::new();
let mut cut_live_names: Vec<std::collections::HashSet<String>> = Vec::new();
for (j, cut) in run.cuts.iter().enumerate() {
let range_end = if j == 0 {
decl_order.len()
} else {
run.cuts[j - 1].decl_start
};
let rest_j = run.rest_for_cut(j);
let mut esc: Vec<(u32, vexec::VVal, u8, JType)> = Vec::new();
let mut live_names: std::collections::HashSet<String> =
std::collections::HashSet::new();
for &k in &decl_order[cut.decl_start.min(range_end)..range_end] {
if ex.var_value(k).is_none() {
continue; }
let name = lang.name_of_key(k)?;
live_names.insert(name.clone());
let referenced = rest_j.iter().any(|&st| {
cure_engine::analysis::subtree_contains(lang, st, |n| {
lang.kind(n) == NodeKind::VarRef
&& lang.var_name(n) == Some(name.as_str())
})
});
if referenced {
esc.push((
k,
ex.var_value(k).cloned().unwrap_or(vexec::VVal::Undef),
ex.var_domain(k),
ex.var_decl_ty(k).unwrap_or(JType::Var),
));
}
}
escaped_by_cut.push(esc);
cut_live_names.push(live_names);
}
drop(ex);
let mut edits: Vec<Edit<JavaAst>> = Vec::new();
for (j, cut) in run.cuts.iter().enumerate() {
let block_id = if j == root_idx {
id
} else {
cut.block_hint?
};
let mut insert: Vec<JavaId> = Vec::new();
for pos in cut.log_start..cut.log_end {
match &log[pos] {
vexec::EffectEvent::FieldWrite(k) => {
let is_last = !log[pos + 1..]
.iter()
.any(|ev| matches!(ev, vexec::EffectEvent::FieldWrite(k2) if k2 == k));
if !is_last {
continue;
}
let name = lang.name_of_key(*k)?;
if lang.is_param_name(&name) {
return None;
}
let v = field_finals
.iter()
.find(|(fk, _)| fk == k)
.map(|(_, v)| v.clone())?;
let v = match (&v, lang.field_types.get(&name)) {
(vexec::VVal::L(x), Some(JType::Int)) => vexec::VVal::I(*x as i32),
(vexec::VVal::L(x), Some(JType::Byte)) => vexec::VVal::I(*x as i8 as i32),
(vexec::VVal::L(x), Some(JType::Short)) => vexec::VVal::I(*x as i16 as i32),
(vexec::VVal::L(x), Some(JType::Char)) => vexec::VVal::I(*x as u16 as i32),
_ => v,
};
let (val, mut extra) = materialize(lang, &v)?;
let tgt = lang.var(&name);
insert.push(lang.assign(tgt, val));
insert.append(&mut extra);
}
vexec::EffectEvent::OpaqueFieldWrite { k, stmt, call, args } => {
if std::env::var("CURE_DBG_P1").is_ok() {
eprintln!("[P1-mat] OpaqueFieldWrite entered k={}", k);
}
let name = lang.name_of_key(*k)?;
if lang.is_param_name(&name) {
return None;
}
let cch = lang.children(*call).to_vec();
let mut new_args: Vec<JavaId> = Vec::new();
let mut kept_original: Vec<bool> = Vec::new();
for (i, &arg) in cch.iter().enumerate().skip(1) {
let val = args.get(i - 1).cloned().flatten();
let replaced = match val {
Some(vexec::VVal::S(t)) => Some(lang.lit(Lit::Str(t))),
Some(vexec::VVal::I(x)) => Some(lang.lit(Lit::Int(x as i64))),
Some(vexec::VVal::L(x)) => Some(lang.lit(Lit::Long(x))),
Some(vexec::VVal::B(b)) => Some(lang.lit(Lit::Bool(b))),
_ => None,
};
kept_original.push(replaced.is_none());
new_args.push(replaced.unwrap_or_else(|| lang.copy_subtree(arg)));
}
let names_j = cut_live_names.get(j)?;
for ((i, &arg), &kept) in
cch.iter().enumerate().skip(1).zip(kept_original.iter())
{
let _ = i;
if kept
&& !names_j.is_empty()
&& cure_engine::analysis::subtree_contains(
lang,
arg,
|n| {
lang.kind(n) == NodeKind::VarRef
&& lang
.var_name(n)
.is_some_and(|nm| names_j.contains(nm))
},
)
{
return None;
}
}
let callee = *lang.children(*call).first()?;
let callee_copy = lang.copy_subtree(callee);
let mut ch = vec![callee_copy];
ch.extend(new_args);
let new_call = lang.clone_node(*call, ch);
let new_stmt = if lang.kind(*stmt) == NodeKind::ExprStmt {
lang.clone_node(*stmt, vec![new_call])
} else {
new_call
};
let tgt = lang.var(&name);
let assign = lang.assign(tgt, new_stmt);
insert.push(assign);
}
vexec::EffectEvent::ClassLoad { stmt, call, name } => {
let lit = lang.lit(Lit::Str(name.clone()));
let callee = *lang.children(*call).first()?;
let callee_copy = lang.copy_subtree(callee);
let new_call = lang.clone_node(*call, vec![callee_copy, lit]);
let new_stmt = if lang.kind(*stmt) == NodeKind::ExprStmt {
lang.clone_node(*stmt, vec![new_call])
} else {
new_call
};
let try_block = lang.block(vec![new_stmt]);
let catch_block = lang.block(vec![]);
let catch_arm = lang.catch_("Exception", "cure$ex", catch_block);
let wrapped = lang.try_(vec![], try_block, vec![catch_arm], None);
insert.push(wrapped);
}
}
}
let mut alias_first: std::collections::HashMap<usize, String> =
std::collections::HashMap::new();
for (k, v, dom, dty) in &escaped_by_cut[j] {
let name = lang.name_of_key(*k)?;
let (val, mut extra) = match v {
vexec::VVal::CA(a) => {
let addr = std::rc::Rc::as_ptr(a) as usize;
match alias_first.get(&addr) {
Some(first) => (lang.var(first), vec![]),
None => {
alias_first.insert(addr, name.clone());
materialize(lang, v)?
}
}
}
vexec::VVal::SA(a) => {
let addr = std::rc::Rc::as_ptr(a) as usize;
match alias_first.get(&addr) {
Some(first) => (lang.var(first), vec![]),
None => {
alias_first.insert(addr, name.clone());
materialize(lang, v)?
}
}
}
_ => materialize(lang, v)?,
};
let (ty, val) = match (*dom, val) {
(2, lit) if lang.kind(lit) == NodeKind::Literal => {
match lang.literal(lit) {
Some(LitRef::Int(v)) => {
match u32::try_from(v).ok().and_then(char::from_u32) {
Some(c) => (JType::Char, lang.lit(Lit::Char(c))),
None => (JType::Char, lit),
}
}
Some(LitRef::Long(v)) => {
match u32::try_from(v as i32 as i64).ok().and_then(char::from_u32) {
Some(c) => (JType::Char, lang.lit(Lit::Char(c))),
None => (JType::Char, lit),
}
}
_ => (JType::Char, lit),
}
}
(3, lit) => (JType::Byte, lit),
(4, lit) => (JType::Short, lit),
(5, lit) if lang.kind(lit) == NodeKind::Literal => {
if let Some(LitRef::Int(v)) = lang.literal(lit) {
(JType::Long, lang.build_int(v, true))
} else {
(JType::Long, lit)
}
}
(_, lit) => {
if matches!(dty, JType::Ref(_)) {
(dty.clone(), lit)
} else {
(JType::Var, lit)
}
}
};
let stmt = lang.var_decl(&name, ty, Some(val));
insert.push(stmt);
insert.append(&mut extra);
}
if j == root_idx {
if let Some(v) = ret_payload.clone().flatten() {
let (val, mut extra) = materialize(lang, &v)?;
let stmt = lang.ret(Some(val));
insert.push(stmt);
insert.append(&mut extra);
}
}
if insert.is_empty() && cut.completed == 0 {
continue;
}
{
let removed = lang.children(block_id).to_vec();
if insert.len() == cut.completed
&& cut.completed <= removed.len()
&& insert
.iter()
.zip(removed[..cut.completed].iter())
.all(|(&n, &o)| print_equiv(lang, n, o))
{
continue;
}
}
edits.push(Edit::Splice {
node: block_id,
index: 0,
remove: cut.completed,
insert,
});
}
if std::env::var("CURE_DBG_P1").is_ok() {
eprintln!("[P1-se] edits={} (empty→reject)", edits.len());
}
if edits.is_empty() {
return None;
}
let candidate = if edits.len() == 1 {
edits.pop().unwrap()
} else {
Edit::Multi(edits)
};
if candidate.cost_reduction(lang) <= 0 {
return None;
}
Some(candidate)
}
}
fn materialize(lang: &mut JavaAst, v: &vexec::VVal) -> Option<(JavaId, Vec<JavaId>)> {
use vexec::VVal;
match v {
VVal::I(x) => Some((lang.lit(Lit::Int(*x as i64)), vec![])),
VVal::L(x) => Some((lang.lit(Lit::Long(*x)), vec![])),
VVal::B(b) => Some((lang.lit(Lit::Bool(*b)), vec![])),
VVal::S(s) => Some((lang.lit(Lit::Str(s.clone())), vec![])),
VVal::CA(a) => {
let chars: Vec<char> = a.borrow().clone();
let lits: Vec<JavaId> = chars.iter().map(|&c| lang.lit(Lit::Char(c))).collect();
let lit_arr = lang.array_lit(lits);
let node = lang.new_array(JType::Char, 1, 0, vec![], Some(lit_arr));
Some((node, vec![]))
}
VVal::SA(elems) => {
let arr = elems.borrow().clone();
if arr.iter().all(|e| matches!(e, VVal::S(_))) {
let lits: Vec<JavaId> = arr
.iter()
.map(|e| match e {
VVal::S(sv) => lang.lit(Lit::Str(sv.clone())),
_ => unreachable!(),
})
.collect();
let lit_arr = lang.array_lit(lits);
let node =
lang.new_array(JType::Ref("String".to_string()), 1, 0, vec![], Some(lit_arr));
Some((node, vec![]))
} else {
None
}
}
VVal::Undef => None,
}
}
pub struct TryUnwrapRethrow;
impl Rule<JavaAst> for TryUnwrapRethrow {
fn name(&self) -> &'static str {
"try_unwrap_rethrow"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Try]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Try {
return None;
}
let ch = lang.children(id).to_vec();
let try_block = *ch.iter().find(|&&c| lang.kind(c) == NodeKind::Block)?;
let catches: Vec<JavaId> =
ch.iter().copied().filter(|&c| lang.kind(c) == NodeKind::Catch).collect();
if catches.is_empty() {
return None;
}
let rethrow_only = |c: JavaId| -> bool {
let block = match lang.children(c).first() {
Some(&b) => b,
None => return false,
};
let stmts = lang.children(block);
if stmts.len() != 1 {
return false;
}
let stmt = stmts[0];
if lang.kind(stmt) != NodeKind::Throw {
return false;
}
let Some(&thrown) = lang.children(stmt).first() else { return false };
if lang.kind(thrown) != NodeKind::VarRef {
return false;
}
match &lang.node(c).data {
NodeData::Catch { name, .. } => lang.var_name(thrown) == Some(name.as_str()),
_ => false,
}
};
let all_rethrow = catches.iter().all(|&c| rethrow_only(c));
if all_rethrow {
let extras: Vec<JavaId> = ch
.iter()
.copied()
.filter(|&c| c != try_block && lang.kind(c) != NodeKind::Catch)
.collect();
if extras.is_empty() {
return Some(Edit::Replace {
target: id,
with: try_block,
});
}
return None;
}
let catch_slots: Vec<usize> = ch
.iter()
.enumerate()
.filter(|(_, &c)| lang.kind(c) == NodeKind::Catch)
.map(|(i, _)| i)
.collect();
for &slot in &catch_slots {
let c = ch[slot];
if !rethrow_only(c) {
continue;
}
let later_all_rethrow = catch_slots
.iter()
.filter(|&&s2| s2 > slot)
.all(|&s2| rethrow_only(ch[s2]));
if !later_all_rethrow {
continue;
}
return Some(Edit::Splice {
node: id,
index: slot,
remove: 1,
insert: vec![],
});
}
None
}
}
pub struct TryUnwrapNoCatch;
impl Rule<JavaAst> for TryUnwrapNoCatch {
fn name(&self) -> &'static str {
"try_unwrap_no_catch"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Try]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Try {
return None;
}
let ch = lang.children(id).to_vec();
if ch.is_empty() {
return None;
}
let has_catch = ch.iter().any(|&c| lang.kind(c) == NodeKind::Catch);
if has_catch {
return None;
}
let try_block = *ch.iter().find(|&&c| lang.kind(c) == NodeKind::Block)?;
let rest: Vec<JavaId> = ch
.iter()
.copied()
.filter(|&c| c != try_block)
.collect();
for &r in &rest {
let is_empty_block = lang.kind(r) == NodeKind::Block && lang.children(r).is_empty();
if !is_empty_block {
return None; }
}
Some(Edit::Replace {
target: id,
with: try_block,
})
}
}
pub struct LoopHeadBreak;
impl Rule<JavaAst> for LoopHeadBreak {
fn name(&self) -> &'static str {
"loop_head_break"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::While, NodeKind::DoWhile]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
let body = match lang.kind(id) {
NodeKind::While => {
let ch = lang.children(id).to_vec();
if !matches!(lang.literal(ch[0]), Some(LitRef::Bool(true))) {
return None;
}
ch[1]
}
NodeKind::DoWhile => {
let ch = lang.children(id).to_vec();
if !matches!(lang.literal(ch[1]), Some(LitRef::Bool(true))) {
return None;
}
ch[0]
}
_ => return None,
};
if lang.kind(body) != NodeKind::Block {
return None;
}
let bch = lang.children(body).to_vec();
let is_do = lang.kind(id) == NodeKind::DoWhile;
if is_do && bch.len() != 1 {
return None;
}
let first = *bch.first()?;
if lang.kind(first) != NodeKind::If {
return None;
}
let ich = lang.children(first).to_vec();
let c = ich[0];
let bare_break = |n: JavaId| matches!(lang.data(n), NodeData::Break { label: None });
let is_break = |n: JavaId| match lang.kind(n) {
NodeKind::Break => bare_break(n),
NodeKind::Block if lang.children(n).len() == 1 => bare_break(lang.children(n)[0]),
_ => false,
};
let bare_continue = |n: JavaId| matches!(lang.data(n), NodeData::Continue { label: None });
let ends_with_continue = |n: JavaId| -> bool {
match lang.kind(n) {
NodeKind::Continue => bare_continue(n),
NodeKind::Block => lang
.children(n)
.last()
.is_some_and(|&l| lang.kind(l) == NodeKind::Continue && bare_continue(l)),
_ => false,
}
};
let strip_tail_continue = |n: JavaId| -> Vec<JavaId> {
match lang.kind(n) {
NodeKind::Continue => vec![],
NodeKind::Block => {
let ch = lang.children(n).to_vec();
if ch
.last()
.is_some_and(|&l| lang.kind(l) == NodeKind::Continue && bare_continue(l))
{
ch[..ch.len() - 1].to_vec()
} else {
ch
}
}
_ => vec![n],
}
};
match ich.len() {
2 => {
if is_do || !is_break(ich[1]) {
return None;
}
let mut rest: Vec<JavaId> = bch[1..].to_vec();
if let Some(&last) = rest.last() {
if lang.kind(last) == NodeKind::Continue && bare_continue(last) {
rest.pop();
}
}
let nc = lang.build_unary(UnOp::Not, c);
let inner = lang.build_block(rest);
let w = lang.while_(nc, inner);
Some(Edit::Replace {
target: id,
with: w,
})
}
3 => {
let (then, els) = (ich[1], ich[2]);
if ends_with_continue(then) && is_break(els) {
let rest = strip_tail_continue(then);
let inner = lang.build_block(rest);
let w = lang.while_(c, inner);
return Some(Edit::Replace {
target: id,
with: w,
});
}
if is_break(then) && ends_with_continue(els) {
let rest = strip_tail_continue(els);
let inner = lang.build_block(rest);
let nc = lang.build_unary(UnOp::Not, c);
let w = lang.while_(nc, inner);
return Some(Edit::Replace {
target: id,
with: w,
});
}
if !is_do && is_break(then) {
let mut all = strip_tail_continue(els);
all.extend(bch[1..].iter().copied());
let inner = lang.build_block(all);
let nc = lang.build_unary(UnOp::Not, c);
let w = lang.while_(nc, inner);
return Some(Edit::Replace {
target: id,
with: w,
});
}
None
}
_ => None,
}
}
}
pub struct WhileIteratorToForEach;
impl Rule<JavaAst> for WhileIteratorToForEach {
fn name(&self) -> &'static str {
"while_iterator_to_for_each"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::While]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let parent = ctx.parent(id)?;
let idx = ctx.index(id)?;
let lang = ctx.lang;
if lang.kind(id) != NodeKind::While {
return None;
}
let wch = lang.children(id).to_vec();
let body = wch[1];
if lang.kind(parent) != NodeKind::Block {
return None;
}
if idx == 0 {
return None;
}
let stmts = lang.children(parent).to_vec();
let cond = wch[0];
let it_hint = match lang.kind(cond) {
NodeKind::Call => {
let cc0 = lang.children(cond).to_vec();
match lang.data(*cc0.first()?) {
NodeData::Member { name } if lang.sn(*name) == "hasNext" => {
lang.var_name(lang.children(cc0[0])[0]).map(|x| x.to_string())
}
_ => None,
}
}
_ => None,
};
let mut decl = None;
if let Some(hint) = it_hint {
let mut up = idx;
for _ in 0..4 {
if up == 0 {
break;
}
up -= 1;
let st = stmts[up];
if lang.kind(st) == NodeKind::VarDecl && lang.var_name(st) == Some(hint.as_str()) {
decl = Some(st);
break;
}
if subtree_has_var(lang, st, &hint) {
break;
}
}
}
let decl = decl?;
let it_name = lang.var_name(decl)?.to_string();
if stmts[idx + 1..]
.iter()
.any(|&s| subtree_has_var(lang, s, &it_name))
{
return None;
}
let init_call = lang.children(decl).first().copied()?;
if lang.kind(init_call) != NodeKind::Call {
return None;
}
let ic = lang.children(init_call).to_vec();
let NodeData::Member { name: m0 } = lang.data(*ic.first()?) else {
return None;
};
if lang.sn(*m0) != "iterator" || ic.len() != 1 {
return None;
}
let iterable = lang.children(ic[0])[0];
if subtree_has_var(&*lang, iterable, &it_name) {
return None;
}
{
let ok = match lang.var_type(iterable) {
Some(t) => statically_iterable(t),
None => map_view_synthetic_ty(lang, iterable).is_some(),
};
if !ok {
return None;
}
}
if lang.kind(cond) != NodeKind::Call {
return None;
}
let cc = lang.children(cond).to_vec();
let NodeData::Member { name: m1 } = lang.data(*cc.first()?) else {
return None;
};
if lang.sn(*m1) != "hasNext" || cc.len() != 1 {
return None;
}
if lang.var_name(lang.children(cc[0])[0]) != Some(it_name.as_str()) {
return None;
}
if lang.kind(body) != NodeKind::Block {
return None;
}
let bch = lang.children(body).to_vec();
let first = *bch.first()?;
if lang.kind(first) != NodeKind::VarDecl {
let it_refs_in_body: Vec<JavaId> = {
let mut v = Vec::new();
collect_var_refs(&*lang, body, &it_name, &mut v);
v
};
if it_refs_in_body.len() != 1 {
return None;
}
let next_recv = it_refs_in_body[0];
let member = parent_of_recv(lang, body, next_recv);
if lang.kind(member) != NodeKind::Member {
return None;
}
let NodeData::Member { name: mn } = lang.data(member) else {
return None;
};
if lang.sn(*mn) != "next" {
return None;
}
let next_call = parent_of_recv(lang, body, member);
if lang.kind(next_call) != NodeKind::Call {
return None;
}
if lang.children(next_call).len() != 1 {
return None;
}
let mut wrapped = next_call;
loop {
let par = parent_of_recv(lang, body, wrapped);
match lang.kind(par) {
NodeKind::Cast | NodeKind::Paren => wrapped = par,
_ => break,
}
}
{
let mut cur = wrapped;
while cur != body {
let par = parent_of_recv(lang, body, cur);
if par == body || par == cur {
break; }
let ch = lang.children(par).to_vec();
let idx = ch.iter().position(|&c| c == cur);
match lang.kind(par) {
NodeKind::Block
| NodeKind::ExprStmt
| NodeKind::Label
| NodeKind::Return
| NodeKind::Throw => {}
NodeKind::Ternary => {
if idx.unwrap_or(0) > 0 {
return None;
}
}
NodeKind::Binary => {
if let Some(op) = lang.bin_op(par) {
if matches!(op, BinOp::And | BinOp::Or) && idx == Some(1) {
return None; }
}
}
NodeKind::If
| NodeKind::While
| NodeKind::DoWhile
| NodeKind::For
| NodeKind::Switch
| NodeKind::Try
| NodeKind::Catch
| NodeKind::Synchronized
| NodeKind::ForEach => {
return None;
}
NodeKind::Lambda | NodeKind::Assert => {
return None;
}
_ => {} }
cur = par;
}
}
let it_decl_elem = match lang.data(decl) {
NodeData::VarDecl { ty, .. } => generic_elem_ty(ty),
_ => None,
};
let elem = match it_decl_elem {
Some(t) => t,
None => match lang.var_type(iterable) {
Some(t) => generic_elem_ty(t).unwrap_or(JType::Ref("Object".into())),
None => JType::Ref("Object".into()),
},
};
let elem = if for_each_elem_ok(&elem) {
elem
} else {
JType::Ref("Object".into())
};
let is_object = matches!(&elem, JType::Ref(n) if n == "Object");
let (replace_target, e_ty) = match lang.data(wrapped) {
NodeData::Cast { .. } if is_object => (next_call, elem),
NodeData::Cast { ty } => (wrapped, if is_object { ty.clone() } else { elem }),
_ => (wrapped, elem),
};
let taken = |nm: &str| {
subtree_has_var(&*lang, body, nm)
|| subtree_has_binding(&*lang, body, nm)
};
let e_name = if taken("e") {
if taken("e2") { "e3" } else { "e2" }
} else {
"e"
};
let e_ref = lang.var(e_name);
let foreach = lang.for_each(e_name, e_ty, iterable, body);
let wrapped_parent = parent_of_recv(lang, body, wrapped);
if lang.kind(wrapped_parent) == NodeKind::ExprStmt
&& *lang.children(wrapped_parent).first().unwrap_or(&wrapped) == wrapped
{
let stmt_block = parent_of_recv(lang, body, wrapped_parent);
if let Some(si) = lang
.children(stmt_block)
.iter()
.position(|&c| c == wrapped_parent)
{
return Some(Edit::Multi(vec![
Edit::Splice {
node: stmt_block,
index: si,
remove: 1,
insert: vec![],
},
Edit::Splice {
node: parent,
index: idx,
remove: 1,
insert: vec![foreach],
},
Edit::Delete { node: decl },
]));
}
}
return Some(Edit::Multi(vec![
Edit::Replace {
target: replace_target,
with: e_ref,
},
Edit::Splice {
node: parent,
index: idx,
remove: 1,
insert: vec![foreach],
},
Edit::Delete { node: decl },
]));
}
let NodeData::VarDecl { name: e_name, ty, .. } = lang.data(first) else {
return None;
};
let (e_name, ty) = (e_name.clone(), ty.clone());
let next_call = lang.children(first).first().copied()?;
let next_call = unwrap_paren_cast(&*lang, next_call);
if lang.kind(next_call) != NodeKind::Call {
return None;
}
let nc = lang.children(next_call).to_vec();
let NodeData::Member { name: m2 } = lang.data(*nc.first()?) else {
return None;
};
if lang.sn(*m2) != "next" || nc.len() != 1 {
return None;
}
if lang.var_name(lang.children(nc[0])[0]) != Some(it_name.as_str()) {
return None;
}
let elem_is_object = match lang.var_type(iterable) {
Some(it_t) => generic_elem_ty(it_t).is_none(),
None => map_view_synthetic_ty(lang, iterable)
.as_ref()
.and_then(generic_elem_ty)
.is_none(),
};
if elem_is_object && ty != JType::Ref("Object".into()) {
return None;
}
let ty = if for_each_elem_ok(&ty) { ty } else { JType::Ref("Object".into()) };
let rest = bch[1..].to_vec();
for &r in &rest {
if subtree_has_var(&*lang, r, &it_name) {
return None;
}
}
let new_body = lang.build_block(rest);
let foreach = lang.for_each(&e_name, ty, iterable, new_body);
Some(Edit::Multi(vec![
Edit::Splice {
node: parent,
index: idx,
remove: 1,
insert: vec![foreach],
},
Edit::Delete { node: decl },
]))
}
}
pub struct ConcatValueOfDrop;
impl Rule<JavaAst> for ConcatValueOfDrop {
fn name(&self) -> &'static str {
"concat_value_of_drop"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Binary]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let root = ctx.root();
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Binary {
return None;
}
if lang.bin_op(id) != Some(BinOp::Add) {
return None;
}
let ch = lang.children(id).to_vec();
let (l, r) = (ch[0], ch[1]);
let becomes_constant = |x: JavaId, other: JavaId| {
lang.literal(x).is_some() && matches!(lang.literal(other), Some(LitRef::Str(_)))
};
let is_value_of = |n: JavaId| -> Option<JavaId> {
if lang.kind(n) != NodeKind::Call {
return None;
}
let c = lang.children(n).to_vec();
if c.len() != 2 {
return None;
}
let callee = c[0];
if let NodeData::Member { name } = lang.data(callee) {
if lang.sn(*name) == "valueOf" && lang.var_name(lang.children(callee)[0]) == Some("String") {
return Some(c[1]);
}
}
if let NodeData::VarRef { name } = lang.data(callee) {
if lang.sn(*name) == "valueOf" {
return Some(c[1]);
}
}
None
};
let stringy = |n: JavaId| -> bool {
matches!(lang.literal(n), Some(LitRef::Str(_)))
|| lang
.var_type(n)
.is_some_and(|t| matches!(t, JType::Ref(n) if n == "String"))
};
if let Some(x) = is_value_of(l) {
if stringy(r) {
if becomes_constant(x, r) && lang.has_string_identity_compare(root) {
return None;
}
let with = lang.build_bin(BinOp::Add, x, r);
return Some(Edit::Replace {
target: id,
with,
});
}
}
if let Some(x) = is_value_of(r) {
if stringy(l) {
if becomes_constant(x, l) && lang.has_string_identity_compare(root) {
return None;
}
let with = lang.build_bin(BinOp::Add, l, x);
return Some(Edit::Replace {
target: id,
with,
});
}
}
None
}
}
pub struct StringBuilderStatements;
fn count_var_uses_in(lang: &JavaAst, root: JavaId, name: &str) -> usize {
let mut n = 0;
let mut stack = vec![root];
while let Some(x) = stack.pop() {
if lang.kind(x) == NodeKind::Assign && lang.assign_op(x).is_none() {
let ch = lang.children(x).to_vec();
if let Some(&t) = ch.first() {
let is_named_target =
lang.kind(t) == NodeKind::VarRef && lang.var_name(t) == Some(name);
if !is_named_target {
stack.push(t);
}
for &c in &ch[1..] {
stack.push(c);
}
}
continue;
}
if lang.kind(x) == NodeKind::VarRef && lang.var_name(x) == Some(name) {
n += 1;
}
for &c in lang.children(x) {
stack.push(c);
}
}
n
}
impl Rule<JavaAst> for StringBuilderStatements {
fn name(&self) -> &'static str {
"string_builder_statements"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::VarDecl]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let parent = ctx.parent(id)?;
let idx = ctx.index(id)?;
let root = ctx.root();
let lang = ctx.lang;
if lang.kind(id) != NodeKind::VarDecl {
return None;
}
if lang.kind(parent) != NodeKind::Block {
return None;
}
let v0 = lang.var_name(id)?.to_string();
let init = *lang.children(id).first()?;
if lang.kind(init) != NodeKind::Call {
return None;
}
let ic = lang.children(init).to_vec();
if ic.len() != 2 {
return None;
}
let NodeData::Member { name: m0 } = lang.data(ic[0]) else {
return None;
};
if lang.sn(*m0) != "append" {
return None;
}
let sb_new = lang.children(ic[0])[0];
let (ctor_arg, a0) = match lang.data(sb_new) {
NodeData::New { ty, .. } => {
let is_sb = !lang.unit_shadows_lang
&& matches!(ty, JType::Ref(n) if n == "StringBuilder" || n == "java.lang.StringBuilder");
if !is_sb {
return None;
}
let args = lang.children(sb_new).to_vec();
match args.len() {
0 => (None, ic[1]),
1 => {
let a = args[0];
let stringy = matches!(lang.literal(a), Some(LitRef::Str(_)))
|| lang
.var_type(a)
.is_some_and(|t| matches!(t, JType::Ref(n) if n == "String"));
if stringy {
(Some(a), ic[1])
} else if matches!(
lang.literal(a),
Some(LitRef::Int(_) | LitRef::Long(_))
) {
(None, ic[1])
} else {
return None; }
}
_ => return None,
}
}
_ => return None,
};
let stmts = lang.children(parent).to_vec();
let mut parts: Vec<JavaId> = vec![a0];
let mut expected_reads: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
let mut chain_len = 1usize; let mut cur = v0.clone();
while idx + chain_len < stmts.len() {
let s = stmts[idx + chain_len];
let (recv, arg, new_var, consumed) = match lang.data(s) {
NodeData::Assign { op: None } => {
let ch = lang.children(s).to_vec();
if ch.len() != 2 {
break;
}
let (t, v) = (ch[0], ch[1]);
if lang.kind(t) != NodeKind::VarRef
|| lang.var_name(t) != Some(cur.as_str())
|| lang.kind(v) != NodeKind::Call
{
break;
}
let vc = lang.children(v).to_vec();
if vc.len() != 2 {
break;
}
let NodeData::Member { name: mn } = lang.data(vc[0]) else {
break;
};
if lang.sn(*mn) != "append" {
break;
}
let recv = lang.children(vc[0])[0];
(recv, vc[1], false, s)
}
NodeData::ExprStmt => {
let ch = lang.children(s).to_vec();
if ch.len() != 1 {
break;
}
let inner = ch[0];
if !matches!(lang.data(inner), NodeData::Assign { op: None }) {
break;
}
let ach = lang.children(inner).to_vec();
if ach.len() != 2 {
break;
}
let (t, v) = (ach[0], ach[1]);
if lang.kind(t) != NodeKind::VarRef
|| lang.var_name(t) != Some(cur.as_str())
|| lang.kind(v) != NodeKind::Call
{
break;
}
let vc = lang.children(v).to_vec();
if vc.len() != 2 {
break;
}
let NodeData::Member { name: mn } = lang.data(vc[0]) else {
break;
};
if lang.sn(*mn) != "append" {
break;
}
let recv = lang.children(vc[0])[0];
(recv, vc[1], false, s)
}
NodeData::VarDecl { .. } => {
let init = match lang.children(s).first() {
Some(&x) => x,
None => break,
};
if lang.kind(init) != NodeKind::Call {
break;
}
let vc = lang.children(init).to_vec();
if vc.len() != 2 {
break;
}
let NodeData::Member { name: mn } = lang.data(vc[0]) else {
break;
};
if lang.sn(*mn) != "append" {
break;
}
let recv = lang.children(vc[0])[0];
if lang.var_name(recv) != Some(cur.as_str()) {
break;
}
(recv, vc[1], true, s)
}
_ => break,
};
if lang.var_name(recv) != Some(cur.as_str()) {
break;
}
parts.push(arg);
*expected_reads.entry(cur.clone()).or_insert(0) += 1;
if new_var {
cur = lang.var_name(consumed)?.to_string();
}
chain_len += 1;
let _ = consumed;
}
let final_stmt = *stmts.get(idx + chain_len)?;
let NodeData::VarDecl { .. } = lang.data(final_stmt) else {
return None;
};
let s_init = *lang.children(final_stmt).first()?;
if lang.kind(s_init) != NodeKind::Call {
return None;
}
let sc = lang.children(s_init).to_vec();
if sc.len() != 1 {
return None;
}
let NodeData::Member { name: mn } = lang.data(sc[0]) else {
return None;
};
if lang.sn(*mn) != "toString" {
return None;
}
if lang.var_name(lang.children(sc[0])[0]) != Some(cur.as_str()) {
return None;
}
*expected_reads.entry(cur.clone()).or_insert(0) += 1; for (v, exp) in &expected_reads {
if count_var_uses_in(&*lang, parent, v) != *exp {
return None;
}
}
let mut all_parts: Vec<JavaId> = Vec::new();
if let Some(ca) = ctor_arg {
all_parts.push(ca);
}
all_parts.extend(parts);
if all_parts.iter().all(|&p| lang.literal(p).is_some())
&& lang.has_string_identity_compare(root)
{
return None;
}
if all_parts.is_empty() {
let empty = lang.build_str("");
return Some(Edit::Multi(vec![
Edit::Replace {
target: s_init,
with: empty,
},
Edit::Splice {
node: parent,
index: idx,
remove: chain_len,
insert: Vec::new(),
},
]));
}
let first = all_parts[0];
let stringy_first = matches!(lang.literal(first), Some(LitRef::Str(_)))
|| lang
.var_type(first)
.is_some_and(|t| matches!(t, JType::Ref(n) if n == "String"));
let mut acc = if stringy_first {
first
} else {
let empty = lang.build_str("");
lang.build_bin(BinOp::Add, empty, first)
};
for &p in &all_parts[1..] {
acc = lang.build_bin(BinOp::Add, acc, p);
}
Some(Edit::Multi(vec![
Edit::Replace {
target: s_init,
with: acc,
},
Edit::Splice {
node: parent,
index: idx,
remove: chain_len,
insert: Vec::new(),
},
]))
}
}
pub struct TrailingContinueJava;
impl Rule<JavaAst> for TrailingContinueJava {
fn name(&self) -> &'static str {
"trailing_continue"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::While, NodeKind::For, NodeKind::DoWhile, NodeKind::ForEach]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let parent = ctx.parent(id)?;
let lang = ctx.lang;
let body = match lang.kind(id) {
NodeKind::While | NodeKind::For | NodeKind::DoWhile | NodeKind::ForEach => {
*lang.children(id).last()?
}
_ => return None,
};
if lang.kind(body) != NodeKind::Block {
return None;
}
let ch = lang.children(body).to_vec();
let last = *ch.last()?;
if lang.kind(last) != NodeKind::Continue {
return None;
}
let label = match lang.data(last) {
NodeData::Continue { label } => label.clone(),
_ => return None,
};
match label {
None => Some(Edit::Delete { node: last }),
Some(l) => {
if lang.kind(parent) == NodeKind::Label {
if let NodeData::Label { name } = lang.data(parent) {
if *name == l {
return Some(Edit::Delete { node: last });
}
}
}
None
}
}
}
}
pub struct XorNoise;
impl Rule<JavaAst> for XorNoise {
fn name(&self) -> &'static str {
"xor_noise"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Binary]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Binary {
return None;
}
let op = lang.bin_op(id)?;
if op != BinOp::BitXor {
return None;
}
let ch = lang.children(id).to_vec();
let (l, r) = (ch[0], ch[1]);
let int_lit = |n: JavaId| lang.literal(n).and_then(|x| x.as_int());
if int_lit(r) == Some(0) {
return Some(Edit::Replace {
target: id,
with: l,
});
}
if int_lit(l) == Some(0) {
return Some(Edit::Replace {
target: id,
with: r,
});
}
if lang.kind(l) != NodeKind::Binary || lang.bin_op(l) != Some(BinOp::BitXor) {
return None;
}
let ich = lang.children(l).to_vec();
let k1 = lang.literal(ich[1]).and_then(|x| x.as_int())?;
let k2 = int_lit(r)?;
let x = ich[0];
let k = k1 ^ k2;
let with = if k == 0 {
x
} else {
let lit = lang.build_int(k, false);
lang.build_bin(BinOp::BitXor, x, lit)
};
Some(Edit::Replace {
target: id,
with,
})
}
}
pub struct StrLenFold;
impl Rule<JavaAst> for StrLenFold {
fn name(&self) -> &'static str {
"str_len_fold"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Call]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let _at_stmt_pos = ctx
.parent(id)
.map(|p| ctx.lang.kind(p) == NodeKind::ExprStmt)
.unwrap_or(false);
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Call {
return None;
}
let ch = lang.children(id).to_vec();
if ch.len() != 1 {
return None;
}
let NodeData::Member { name } = lang.data(ch[0]) else {
return None;
};
if lang.sn(*name) != "length" {
return None;
}
let recv = lang.children(ch[0])[0];
if let Some(LitRef::Str(s)) = lang.literal(recv) {
let with = lang.build_int(s.chars().count() as i64, false);
return Some(Edit::Replace {
target: id,
with,
});
}
None
}
}
pub struct LiteralEval;
impl Rule<JavaAst> for LiteralEval {
fn name(&self) -> &'static str {
"literal_eval"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Call, NodeKind::Index, NodeKind::Cast]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let root = ctx.root();
let lang = ctx.lang;
let edit = match lang.kind(id) {
NodeKind::Call => self.eval_call(&mut *lang, id),
NodeKind::Index => self.eval_index(&mut *lang, id),
NodeKind::Cast => eval_cast_literal(&mut *lang, id),
_ => None,
};
if let Some(Edit::Replace { with, .. }) = &edit {
if matches!(lang.literal(*with), Some(LitRef::Str(_)))
&& lang.has_string_identity_compare(root)
{
if std::env::var("CURE_DEBUG_GATE").is_ok() {
eprintln!("[gate] blocked literal_eval at {id:?}");
}
return None;
}
}
edit
}
}
impl LiteralEval {
fn eval_call(&self, lang: &mut JavaAst, id: JavaId) -> Option<Edit<JavaAst>> {
let ch = lang.children(id).to_vec();
let callee = ch[0];
let NodeData::Member { name: method } = lang.data(callee) else {
return None;
};
let method = *method;
let recv = lang.children(callee)[0];
let args: Vec<Lit> = ch[1..]
.iter()
.map(|&a| litref_to_lit(lang.literal(a)))
.collect::<Option<Vec<_>>>()?;
let result: Option<Lit> = if let Some(LitRef::Str(s)) = lang.literal(recv) {
eval_str_method(s, lang.sn(method), &args)
} else if let Some(cls) = lang.var_name(recv) {
eval_static_method(cls, lang.sn(method), &args)
} else {
None
};
let lit = result?;
let with = lit_to_node(lang, &lit);
Some(Edit::Replace {
target: id,
with,
})
}
fn eval_index(&self, lang: &mut JavaAst, id: JavaId) -> Option<Edit<JavaAst>> {
let ch = lang.children(id).to_vec();
let arr = match lang.kind(ch[0]) {
NodeKind::ArrayLit => ch[0],
NodeKind::NewArray => {
let nc = lang.children(ch[0]);
let NodeData::NewArray { sized, .. } = lang.data(ch[0]) else {
return None;
};
if *sized != 0 || nc.len() != 1 || lang.kind(nc[0]) != NodeKind::ArrayLit {
return None;
}
nc[0]
}
_ => return None,
};
let idx = lang.literal(ch[1])?.as_int()?;
let elems = lang.children(arr).to_vec();
if idx < 0 || idx >= elems.len() as i64 {
return None;
}
Some(Edit::Replace {
target: id,
with: elems[idx as usize],
})
}
}
fn litref_to_lit(l: Option<LitRef<'_>>) -> Option<Lit> {
Some(match l? {
LitRef::Str(s) => Lit::Str(s.to_string()),
LitRef::Int(v) => Lit::Int(v),
LitRef::Long(v) => Lit::Long(v),
LitRef::Bool(b) => Lit::Bool(b),
LitRef::Char(c) => Lit::Char(c),
LitRef::Float(v) => Lit::Float(v),
LitRef::Double(v) => Lit::Double(v),
LitRef::Null => return None,
})
}
fn lit_to_node(lang: &mut JavaAst, lit: &Lit) -> JavaId {
match lit {
Lit::Str(s) => lang.build_str(s),
Lit::Int(v) => lang.build_int(*v, false),
Lit::Long(v) => lang.build_int(*v, true),
Lit::Bool(b) => lang.build_bool(*b),
Lit::Char(c) => lang.build_char(*c),
Lit::Float(v) => lang.lit(Lit::Float(*v)),
Lit::Double(v) => lang.lit(Lit::Double(*v)),
_ => lang.lit(Lit::Null),
}
}
fn eval_cast_literal(lang: &mut JavaAst, id: JavaId) -> Option<Edit<JavaAst>> {
let NodeData::Cast { ty } = lang.data(id) else {
return None;
};
let ty = ty.clone();
let inner = lang.children(id)[0];
let lit = litref_to_lit(lang.literal(inner))?;
let with = match (&ty, &lit) {
(JType::Char, Lit::Int(v)) if (0_i64..=0xFFFF).contains(v) => {
lang.build_char(char::from_u32(*v as u32)?)
}
(JType::Char, Lit::Char(_)) => inner,
(JType::Int, Lit::Char(c)) => lang.build_int(*c as i64, false),
(JType::Long, Lit::Char(c)) => lang.build_int(*c as i64, true),
(JType::Long, Lit::Int(v)) => lang.build_int(*v, true),
(JType::Int, Lit::Long(v)) if *v >= i32::MIN as i64 && *v <= i32::MAX as i64 => {
lang.build_int(*v, false)
}
(JType::Int, Lit::Int(_)) => inner,
_ => return None,
};
Some(Edit::Replace {
target: id,
with,
})
}
fn eval_str_method(s: &str, method: &str, args: &[Lit]) -> Option<Lit> {
let chars: Vec<char> = s.chars().collect();
let n = chars.len() as i64;
Some(match (method, args) {
("isEmpty", []) => Lit::Bool(s.is_empty()),
("intern", []) => Lit::Str(s.to_string()),
("trim", []) => Lit::Str(s.trim_matches(|c: char| c <= ' ').to_string()),
("concat", [Lit::Str(b)]) => Lit::Str(format!("{s}{b}")),
("startsWith", [Lit::Str(p)]) => Lit::Bool(s.starts_with(p.as_str())),
("endsWith", [Lit::Str(p)]) => Lit::Bool(s.ends_with(p.as_str())),
("contains", [Lit::Str(p)]) => Lit::Bool(s.contains(p.as_str())),
("equals", [Lit::Str(p)]) => Lit::Bool(s == p),
("equalsIgnoreCase", [Lit::Str(p)]) => {
Lit::Bool(s.to_lowercase() == p.to_lowercase())
}
("indexOf", [Lit::Char(c)]) => Lit::Int(
s.find(*c).map(|i| i as i64).unwrap_or(-1),
),
("indexOf", [Lit::Str(p)]) => Lit::Int(
s.find(p.as_str()).map(|i| i as i64).unwrap_or(-1),
),
("length", []) => Lit::Int(n),
("charAt", [Lit::Int(i)]) if *i >= 0 && *i < n => {
Lit::Char(chars[*i as usize])
}
("substring", [Lit::Int(b)]) if *b >= 0 && *b <= n => {
Lit::Str(s.chars().skip(*b as usize).collect())
}
("substring", [Lit::Int(b), Lit::Int(e)]) if *b >= 0 && *b <= *e && *e <= n => {
Lit::Str(chars[*b as usize..*e as usize].iter().collect())
}
("replace", [Lit::Char(a), Lit::Char(b)]) => {
Lit::Str(s.chars().map(|c| if c == *a { *b } else { c }).collect())
}
("replace", [Lit::Str(a), Lit::Str(b)]) if !a.is_empty() => {
Lit::Str(s.replace(a.as_str(), b))
}
("hashCode", []) => Lit::Int(
s.chars()
.fold(0i32, |h, c| h.wrapping_mul(31).wrapping_add(c as i32))
as i64,
),
_ => return None,
})
}
fn eval_static_method(cls: &str, method: &str, args: &[Lit]) -> Option<Lit> {
let cls = cls.rsplit('.').next().unwrap_or(cls);
Some(match (cls, method, args) {
("Integer", "parseInt", [Lit::Str(s)]) => {
Lit::Int(s.trim().parse::<i64>().ok()?)
}
("Integer", "toString", [Lit::Int(v)]) => Lit::Str(v.to_string()),
("Integer", "toString", [Lit::Long(v)]) => Lit::Str(v.to_string()),
("Long", "parseLong", [Lit::Str(s)]) => Lit::Long(s.trim().parse::<i64>().ok()?),
("Long", "toString", [Lit::Int(v)]) => Lit::Str(v.to_string()),
("Long", "toString", [Lit::Long(v)]) => Lit::Str(v.to_string()),
("Boolean", "parseBoolean", [Lit::Str(s)]) => {
Lit::Bool(s.eq_ignore_ascii_case("true"))
}
("Boolean", "toString", [Lit::Bool(b)]) => Lit::Str(b.to_string()),
("String", "valueOf", [Lit::Str(s)]) => Lit::Str(s.clone()),
("String", "valueOf", [Lit::Int(v)]) => Lit::Str(v.to_string()),
("String", "valueOf", [Lit::Long(v)]) => Lit::Str(v.to_string()),
("String", "valueOf", [Lit::Char(c)]) => Lit::Str(c.to_string()),
("String", "valueOf", [Lit::Bool(b)]) => Lit::Str(b.to_string()),
("String", "format", fmt_args @ [Lit::Str(_), ..]) => {
let fmt = match &fmt_args[0] {
Lit::Str(s) => s.clone(),
_ => return None,
};
let out = eval_string_format(&fmt, &fmt_args[1..])?;
Lit::Str(out)
}
("Math", "abs", [Lit::Int(v)]) => Lit::Int(v.wrapping_abs()),
("Math", "abs", [Lit::Long(v)]) => Lit::Long(v.wrapping_abs()),
("Math", "max", [Lit::Int(a), Lit::Int(b)]) => Lit::Int(*a.max(b)),
("Math", "min", [Lit::Int(a), Lit::Int(b)]) => Lit::Int(*a.min(b)),
("Math", "max", [Lit::Long(a), Lit::Long(b)]) => Lit::Long(*a.max(b)),
("Math", "min", [Lit::Long(a), Lit::Long(b)]) => Lit::Long(*a.min(b)),
("Character", "isDigit", [Lit::Char(c)]) => Lit::Bool(c.is_ascii_digit()),
("Character", "isLetter", [Lit::Char(c)]) => Lit::Bool(c.is_alphabetic()),
("Character", "isWhitespace", [Lit::Char(c)]) => Lit::Bool(c.is_whitespace()),
("Character", "isUpperCase", [Lit::Char(c)]) => Lit::Bool(c.is_uppercase()),
("Character", "isLowerCase", [Lit::Char(c)]) => Lit::Bool(c.is_lowercase()),
("Character", "toUpperCase", [Lit::Char(c)]) => Lit::Char(c.to_uppercase().next()?),
("Character", "toLowerCase", [Lit::Char(c)]) => Lit::Char(c.to_lowercase().next()?),
("Character", "toString", [Lit::Char(c)]) => Lit::Str(c.to_string()),
_ => return None,
})
}
fn eval_string_format(fmt: &str, args: &[Lit]) -> Option<String> {
let mut out = String::new();
let mut it = fmt.chars().peekable();
let mut arg_i = 0usize;
while let Some(c) = it.next() {
if c != '%' {
out.push(c);
continue;
}
match it.next()? {
'%' => out.push('%'),
's' => {
let a = args.get(arg_i)?;
arg_i += 1;
match a {
Lit::Str(s) => out.push_str(s),
Lit::Char(ch) => out.push(*ch),
Lit::Bool(b) => out.push_str(&b.to_string()),
_ => out.push_str(&lit_debug_str(a)),
}
}
'd' => {
let a = args.get(arg_i)?;
arg_i += 1;
match a {
Lit::Int(v) => out.push_str(&v.to_string()),
Lit::Long(v) => out.push_str(&v.to_string()),
Lit::Char(ch) => out.push_str(&(*ch as i64).to_string()),
_ => return None,
}
}
_ => return None,
}
}
if arg_i != args.len() {
return None; }
Some(out)
}
fn lit_debug_str(l: &Lit) -> String {
match l {
Lit::Int(v) => v.to_string(),
Lit::Long(v) => v.to_string(),
_ => "?".into(),
}
}
fn member_name(ast: &JavaAst, m: JavaId) -> Option<String> {
match ast.data(m) {
NodeData::Member { name } => Some(ast.sn(*name).to_string()),
_ => None,
}
}
pub struct UrlDecodeFold;
impl Rule<JavaAst> for UrlDecodeFold {
fn name(&self) -> &'static str {
"url_decode_fold"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Call]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
let ch = lang.children(id).to_vec();
if ch.len() != 3 {
return None; }
let callee = ch[0];
if lang.kind(callee) != NodeKind::Member {
return None;
}
if member_name(lang, callee).as_deref() != Some("decode") {
return None;
}
let recv = *lang.children(callee).first()?;
let recv_name = match lang.kind(recv) {
NodeKind::Member => member_name(lang, recv)?,
NodeKind::VarRef => lang.var_name(recv)?.to_string(),
_ => return None,
};
if !recv_name.ends_with("URLDecoder") {
return None;
}
let s_lit = match lang.literal(ch[1])? {
LitRef::Str(s) => s.to_string(),
_ => return None,
};
let cs = match lang.literal(ch[2])? {
LitRef::Str(s) => s.to_string(),
_ => return None,
};
if cs != "UTF-8" {
return None;
}
if s_lit.len() > 8192 {
return None;
}
let decoded = url_decode(&s_lit)?;
if decoded.chars().any(|c| {
c.is_control() || (c as u32) >= 0xD800 && (c as u32) <= 0xDFFF
}) {
return None;
}
let lit = lang.lit(Lit::Str(decoded));
Some(Edit::Replace { target: id, with: lit })
}
}
fn url_decode(s: &str) -> Option<String> {
let b = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(b.len());
let mut i = 0usize;
while i < b.len() {
match b[i] {
b'+' => {
out.push(b' ');
i += 1;
}
b'%' => {
if i + 3 > b.len() {
return None;
}
let hi = hex_val(b[i + 1])?;
let lo = hex_val(b[i + 2])?;
out.push(hi * 16 + lo);
i += 3;
}
c => {
out.push(c);
i += 1;
}
}
}
String::from_utf8(out).ok()
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
pub struct Base64NewStringFold;
impl Rule<JavaAst> for Base64NewStringFold {
fn name(&self) -> &'static str {
"base64_new_string_fold"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::New]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let root = ctx.root();
let lang = ctx.lang;
let NodeData::New { ty, .. } = lang.data(id) else {
return None;
};
if !matches!(ty, JType::Ref(n) if n == "String" || n == "java.lang.String") {
return None;
}
if lang.has_string_identity_compare(root) {
return None;
}
let args = lang.children(id).to_vec();
if args.len() != 1 {
return None;
}
if lang.kind(args[0]) != NodeKind::Call {
return None;
}
let dc = lang.children(args[0]).to_vec();
if dc.len() != 2 {
return None;
}
let NodeData::Member { name: dn } = lang.data(dc[0]) else {
return None;
};
if lang.sn(*dn) != "decode" {
return None;
}
let recv = lang.children(dc[0])[0];
if lang.kind(recv) != NodeKind::Call {
return None;
}
let gc = lang.children(recv).to_vec();
let NodeData::Member { name: gn } = lang.data(*gc.first()?) else {
return None;
};
let url_safe = match lang.sn(*gn) {
"getDecoder" => false,
"getUrlDecoder" => true,
_ => return None,
};
let owner = lang.children(gc[0])[0];
let owner_ok = match lang.data(owner) {
NodeData::Member { name } => lang.sn(*name) == "Base64",
_ => matches!(lang.var_name(owner), Some(n) if n == "Base64" || n.ends_with(".Base64")),
};
if !owner_ok {
return None;
}
let LitRef::Str(b64) = lang.literal(dc[1])? else {
return None;
};
let bytes = base64_decode(b64, url_safe)?;
let text = String::from_utf8(bytes).ok()?;
let with = lang.build_str(&text);
Some(Edit::Replace {
target: id,
with,
})
}
}
fn base64_decode(s: &str, url_safe: bool) -> Option<Vec<u8>> {
let val = |c: char| -> Option<u32> {
match c {
'A'..='Z' => Some(c as u32 - 'A' as u32),
'a'..='z' => Some(c as u32 - 'a' as u32 + 26),
'0'..='9' => Some(c as u32 - '0' as u32 + 52),
'+' if !url_safe => Some(62),
'/' if !url_safe => Some(63),
'-' if url_safe => Some(62),
'_' if url_safe => Some(63),
_ => None,
}
};
let clean: String = s.chars().filter(|&c| c != '=' && !c.is_whitespace()).collect();
if clean.len() % 4 == 1 {
return None;
}
let mut out = Vec::new();
let mut buf = 0u32;
let mut bits = 0u32;
for c in clean.chars() {
buf = (buf << 6) | val(c)?;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
}
Some(out)
}
pub struct CffRecover;
struct CffCase {
body: Vec<JavaId>,
tail: CffTail,
}
enum CffTail {
Goto(i64),
Cond { cond: JavaId, a: i64, b: i64 },
Term(JavaId),
Exit,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Flow {
Done,
Stop,
Cycle(usize),
Cond(i64),
External(i64),
}
struct CffCtx<'a> {
lang: &'a mut JavaAst,
cases: std::collections::HashMap<i64, CffCase>,
default_term: Option<Vec<JavaId>>,
used_default: bool,
reached: std::collections::HashSet<i64>,
emitted: std::collections::HashSet<i64>,
}
impl Rule<JavaAst> for CffRecover {
fn name(&self) -> &'static str {
"cff_recover"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::While]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let parent = ctx.parent(id)?;
let idx = ctx.index(id)?;
let lang = ctx.lang;
if lang.kind(id) != NodeKind::While {
return None;
}
let wch = lang.children(id).to_vec();
if !matches!(lang.literal(wch[0]), Some(LitRef::Bool(true))) {
return None;
}
if lang.kind(wch[1]) != NodeKind::Block {
return None;
}
let body_ch = lang.children(wch[1]).to_vec();
let (switch, exit_if) = match body_ch.len() {
1 => (body_ch[0], None),
2 => (body_ch[0], Some(body_ch[1])),
_ => return None,
};
if lang.kind(switch) != NodeKind::Switch {
return None;
}
let sch = lang.children(switch).to_vec();
if lang.kind(sch[0]) != NodeKind::VarRef || !lang.is_local_var(sch[0]) {
return None;
}
let v = lang.var_name(sch[0])?.to_string();
let sentinel = match exit_if {
None => None,
Some(ei) => {
if lang.kind(ei) != NodeKind::If {
return None;
}
let ich = lang.children(ei).to_vec();
if ich.len() != 2 || lang.bin_op(ich[0]) != Some(BinOp::Eq) {
return None;
}
let cch = lang.children(ich[0]).to_vec();
let sent = if lang.kind(cch[0]) == NodeKind::VarRef
&& lang.var_name(cch[0]) == Some(v.as_str())
{
lang.literal(cch[1]).and_then(|x| x.as_int())
} else if lang.kind(cch[1]) == NodeKind::VarRef
&& lang.var_name(cch[1]) == Some(v.as_str())
{
lang.literal(cch[0]).and_then(|x| x.as_int())
} else {
return None;
}?;
let then_ok = match lang.kind(ich[1]) {
NodeKind::Break => matches!(lang.data(ich[1]), NodeData::Break { label: None }),
NodeKind::Block if lang.children(ich[1]).len() == 1 => {
matches!(lang.data(lang.children(ich[1])[0]), NodeData::Break { label: None })
}
_ => false,
};
if !then_ok {
return None;
}
Some(sent)
}
};
let mut cases: std::collections::HashMap<i64, CffCase> = Default::default();
let mut default_term: Option<Vec<JavaId>> = None;
for &c in &sch[1..] {
let NodeData::Case { labels, is_default, arrow } = lang.data(c) else {
return None;
};
let (labels, is_default, arrow) = (*labels, *is_default, *arrow);
let ch = lang.children(c).to_vec();
let mut stmts: Vec<JavaId> = ch[labels as usize..].to_vec();
if !arrow
&& stmts
.last()
.is_some_and(|&n| matches!(lang.data(n), NodeData::Break { label: None }))
{
stmts.pop();
}
if stmts.len() == 1 && lang.kind(stmts[0]) == NodeKind::Block {
let mut inner = lang.children(stmts[0]).to_vec();
if !arrow
&& inner
.last()
.is_some_and(|&n| matches!(lang.data(n), NodeData::Break { label: None }))
{
inner.pop();
}
stmts = inner;
}
if is_default {
if stmts.len() == 1 && lang.kind(stmts[0]) == NodeKind::Return {
default_term = Some(stmts);
continue;
}
return None;
}
if labels != 1 {
return None;
}
let state = match lang.literal(ch[0]) {
Some(LitRef::Int(v)) => v,
_ => return None,
};
let tail = parse_cff_tail(lang, &v, &mut stmts)?;
for &st in &stmts {
if subtree_has_var(lang, st, &v) {
return None;
}
}
cases.insert(state, CffCase { body: stmts, tail });
}
if cases.is_empty() {
return None;
}
if idx == 0 {
return None;
}
let block_ch = lang.children(parent).to_vec();
let mut init = None;
let mut init_idx = idx;
{
let mut up = idx;
for _ in 0..8 {
if up == 0 {
break;
}
up -= 1;
let st = block_ch[up];
match scan_cff_event(lang, st, &v) {
Some(CffEvent::Read) => return None,
Some(CffEvent::Write) | Some(CffEvent::Decl) => {
init = parse_cff_init(lang, &v, st);
init_idx = up;
break;
}
None => continue,
}
}
}
let init = init?;
for &st in &block_ch[idx + 1..] {
if subtree_has_var(lang, st, &v) {
return None;
}
}
let mut to_exit: Vec<i64> = Vec::new();
for (k, ci) in cases.iter() {
if let CffTail::Goto(g) = ci.tail {
if !cases.contains_key(&g) {
if !(sentinel == Some(g) || default_term.is_some()) {
return None;
}
to_exit.push(*k);
}
}
}
for k in to_exit {
if let Some(ci) = cases.get_mut(&k) {
ci.tail = CffTail::Exit;
}
}
let mut c = CffCtx {
lang,
cases,
default_term,
used_default: false,
reached: Default::default(),
emitted: Default::default(),
};
let stmts = cff_run(&mut c, init)?;
if stmts.is_empty() {
return None;
}
Some(Edit::Multi(vec![
Edit::Splice {
node: parent,
index: idx,
remove: 1,
insert: stmts,
},
Edit::Delete {
node: block_ch[init_idx],
},
]))
}
}
fn parse_cff_tail(lang: &JavaAst, v: &str, stmts: &mut Vec<JavaId>) -> Option<CffTail> {
let last = *stmts.last()?;
if lang.kind(last) == NodeKind::Return {
stmts.pop();
return Some(CffTail::Term(last));
}
let assign_of = |n: JavaId| -> Option<JavaId> {
let inner = match lang.kind(n) {
NodeKind::Assign => n,
NodeKind::ExprStmt => {
let ch = lang.children(n);
if ch.len() == 1 && lang.kind(ch[0]) == NodeKind::Assign {
ch[0]
} else {
return None;
}
}
_ => return None,
};
if lang.assign_op(inner).is_some() {
return None;
}
let ch = lang.children(inner).to_vec();
if lang.kind(ch[0]) != NodeKind::VarRef || lang.var_name(ch[0]) != Some(v) {
return None;
}
Some(inner)
};
if let Some(assign) = assign_of(last) {
let value = lang.children(assign)[1];
if let Some(k) = lang.literal(value).and_then(|x| x.as_int()) {
stmts.pop();
return Some(CffTail::Goto(k));
}
if lang.kind(value) == NodeKind::Ternary {
let tch = lang.children(value).to_vec();
let a = lang.literal(tch[1]).and_then(|x| x.as_int())?;
let b = lang.literal(tch[2]).and_then(|x| x.as_int())?;
stmts.pop();
return Some(CffTail::Cond { cond: tch[0], a, b });
}
return None;
}
if lang.kind(last) == NodeKind::If {
let ich = lang.children(last).to_vec();
if ich.len() != 3 {
return None;
}
let arm = |n: JavaId| -> Option<i64> {
let target = match lang.kind(n) {
NodeKind::Assign | NodeKind::ExprStmt => assign_of(n)?,
NodeKind::Block if lang.children(n).len() == 1 => assign_of(lang.children(n)[0])?,
_ => return None,
};
lang.literal(lang.children(target)[1]).and_then(|x| x.as_int())
};
let a = arm(ich[1])?;
let b = arm(ich[2])?;
stmts.pop();
return Some(CffTail::Cond { cond: ich[0], a, b });
}
None
}
enum CffEvent {
Read,
Write,
Decl,
}
fn scan_cff_event(lang: &JavaAst, node: JavaId, name: &str) -> Option<CffEvent> {
match lang.kind(node) {
NodeKind::VarRef => {
if lang.var_name(node) == Some(name) {
Some(CffEvent::Read)
} else {
None
}
}
NodeKind::VarDecl => {
if lang.var_name(node) == Some(name) {
Some(CffEvent::Decl)
} else {
for &c in lang.children(node) {
if let Some(e) = scan_cff_event(lang, c, name) {
return Some(e);
}
}
None
}
}
NodeKind::Assign => {
let ch = lang.children(node);
if let Some(&val) = ch.get(1) {
if let Some(e) = scan_cff_event(lang, val, name) {
if matches!(e, CffEvent::Read) {
return Some(e);
}
}
}
if lang.assign_op(node).is_none() {
if let Some(&t) = ch.first() {
if lang.kind(t) == NodeKind::VarRef && lang.var_name(t) == Some(name) {
return Some(CffEvent::Write);
}
}
}
None
}
NodeKind::If | NodeKind::While => scan_cff_event(lang, *lang.children(node).first()?, name),
NodeKind::Ternary => scan_cff_event(lang, *lang.children(node).first()?, name),
NodeKind::Binary => {
let op = lang.bin_op(node)?;
let ch = lang.children(node);
if op.is_short_circuit() {
scan_cff_event(lang, *ch.first()?, name)
} else {
for &c in ch {
if let Some(e) = scan_cff_event(lang, c, name) {
return Some(e);
}
}
None
}
}
_ => {
for &c in lang.children(node) {
if let Some(e) = scan_cff_event(lang, c, name) {
return Some(e);
}
}
None
}
}
}
fn parse_cff_init(lang: &JavaAst, v: &str, stmt: JavaId) -> Option<i64> {
match lang.kind(stmt) {
NodeKind::VarDecl => {
let ch = lang.children(stmt).to_vec();
if ch.len() != 1 || lang.var_name(stmt) != Some(v) {
return None;
}
lang.literal(ch[0]).and_then(|x| x.as_int())
}
NodeKind::Assign => {
if lang.assign_op(stmt).is_some() {
return None;
}
let ch = lang.children(stmt).to_vec();
if lang.kind(ch[0]) != NodeKind::VarRef || lang.var_name(ch[0]) != Some(v) {
return None;
}
lang.literal(ch[1]).and_then(|x| x.as_int())
}
NodeKind::ExprStmt => {
let ch = lang.children(stmt).to_vec();
if ch.len() == 1 {
parse_cff_init(lang, v, ch[0])
} else {
None
}
}
_ => None,
}
}
fn flatten(pairs: &[(i64, Vec<JavaId>)], out: &mut Vec<JavaId>, c: &mut CffCtx) {
for (s, stmts) in pairs {
c.emitted.insert(*s);
out.extend(stmts.iter().copied());
}
}
fn flatten_prefix(pairs: &[(i64, Vec<JavaId>)], m: i64, out: &mut Vec<JavaId>) {
for (s, stmts) in pairs {
if *s == m {
break;
}
out.extend(stmts.iter().copied());
}
}
fn suffix_pairs(pairs: &[(i64, Vec<JavaId>)], m: i64) -> Vec<(i64, Vec<JavaId>)> {
let pos = pairs.iter().position(|(s, _)| *s == m).unwrap();
pairs[pos..].to_vec()
}
fn collect(c: &mut CffCtx, start: i64, stop: Option<i64>) -> Option<(Vec<(i64, Vec<JavaId>)>, Flow)> {
let mut path: Vec<(i64, Vec<JavaId>)> = Vec::new();
let mut cur = start;
let mut guard = 0usize;
loop {
guard += 1;
if guard > 10_000 {
return None;
}
if Some(cur) == stop {
return Some((path, Flow::Stop));
}
if let Some(i) = path.iter().position(|(s, _)| *s == cur) {
return Some((path, Flow::Cycle(i)));
}
if c.emitted.contains(&cur) {
return Some((path, Flow::External(cur)));
}
c.reached.insert(cur);
let ci = c.cases.get(&cur)?;
let mut body = ci.body.clone();
match ci.tail {
CffTail::Goto(k) => {
path.push((cur, body));
cur = k;
}
CffTail::Term(r) => {
body.push(r);
path.push((cur, body));
return Some((path, Flow::Done));
}
CffTail::Exit => {
if c.default_term.is_some() {
if let Some(dt) = c.default_term.clone() {
body.extend(dt);
c.used_default = true;
}
}
path.push((cur, body));
return Some((path, Flow::Done));
}
CffTail::Cond { .. } => return Some((path, Flow::Cond(cur))),
}
}
}
fn cff_run(c: &mut CffCtx, init: i64) -> Option<Vec<JavaId>> {
let (path, flow) = collect(c, init, None)?;
cff_structure(c, path, flow)
}
fn cff_run_from(c: &mut CffCtx, start: i64) -> Option<Vec<JavaId>> {
let (path, flow) = collect(c, start, None)?;
cff_structure(c, path, flow)
}
fn cff_structure(c: &mut CffCtx, mut path: Vec<(i64, Vec<JavaId>)>, mut flow: Flow) -> Option<Vec<JavaId>> {
let mut stmts: Vec<JavaId> = Vec::new();
loop {
match flow {
Flow::Done => {
flatten(&path, &mut stmts, c);
return Some(stmts);
}
Flow::Cycle(i) => {
flatten(&path[..i], &mut stmts, c);
let mut suffix = Vec::new();
flatten(&path[i..], &mut suffix, c);
let inner = c.lang.build_block(suffix);
let cond = c.lang.build_bool(true);
let w = c.lang.while_(cond, inner);
stmts.push(w);
return Some(stmts);
}
Flow::Stop | Flow::External(_) => return None,
Flow::Cond(state) => {
flatten(&path, &mut stmts, c);
path.clear();
let ci = c.cases.get(&state)?;
stmts.extend(ci.body.iter().copied());
c.emitted.insert(state);
let CffTail::Cond { cond, a, b } = ci.tail else {
return None;
};
let (pa, fa) = collect(c, a, Some(state))?;
if fa == Flow::Stop {
let mut lb = Vec::new();
flatten(&pa, &mut lb, c);
let inner = c.lang.build_block(lb);
let w = c.lang.while_(cond, inner);
stmts.push(w);
let (p2, f2) = collect(c, b, None)?;
path = p2;
flow = f2;
continue;
}
let (pb, fb) = collect(c, b, Some(state))?;
if fb == Flow::Stop {
let mut lb = Vec::new();
flatten(&pb, &mut lb, c);
let inner = c.lang.build_block(lb);
let nc = c.lang.build_unary(UnOp::Not, cond);
let w = c.lang.while_(nc, inner);
stmts.push(w);
let (p2, f2) = collect(c, a, None)?;
path = p2;
flow = f2;
continue;
}
let common = pa
.iter()
.find(|(s, _)| pb.iter().any(|(t, _)| *s == *t))
.map(|(s, _)| *s);
let same_end = match (fa, fb) {
(Flow::Done, Flow::Done) => true,
(Flow::Cond(x), Flow::Cond(y)) => x == y,
_ => false,
};
match common {
Some(m) if same_end => {
let mut pre_a = Vec::new();
let mut pre_b = Vec::new();
flatten_prefix(&pa, m, &mut pre_a);
flatten_prefix(&pb, m, &mut pre_b);
let ba = c.lang.build_block(pre_a);
let bb = c.lang.build_block(pre_b);
let ifs = c.lang.if_(cond, ba, Some(bb));
stmts.push(ifs);
path = suffix_pairs(&pa, m);
flow = fa;
continue;
}
Some(_) => return None,
None => {
let mut fa_stmts = Vec::new();
let mut fb_stmts = Vec::new();
flatten(&pa, &mut fa_stmts, c);
flatten(&pb, &mut fb_stmts, c);
match (fa, fb) {
(Flow::Done, Flow::Done) => {
let ba = c.lang.build_block(fa_stmts);
let bb = c.lang.build_block(fb_stmts);
let ifs = c.lang.if_(cond, ba, Some(bb));
stmts.push(ifs);
return Some(stmts);
}
(Flow::Cond(x), Flow::Cond(y)) if x == y => {
let ba = c.lang.build_block(fa_stmts);
let bb = c.lang.build_block(fb_stmts);
let ifs = c.lang.if_(cond, ba, Some(bb));
stmts.push(ifs);
let (p2, f2) = collect(c, x, None)?;
path = p2;
flow = f2;
continue;
}
(Flow::Done, Flow::Cond(x)) => {
let cont = cff_run_from(c, x)?;
fb_stmts.extend(cont);
let ba = c.lang.build_block(fa_stmts);
let bb = c.lang.build_block(fb_stmts);
let ifs = c.lang.if_(cond, ba, Some(bb));
stmts.push(ifs);
return Some(stmts);
}
(Flow::Cond(x), Flow::Done) => {
let cont = cff_run_from(c, x)?;
fa_stmts.extend(cont);
let ba = c.lang.build_block(fa_stmts);
let bb = c.lang.build_block(fb_stmts);
let ifs = c.lang.if_(cond, ba, Some(bb));
stmts.push(ifs);
return Some(stmts);
}
_ => return None,
}
}
}
}
}
}
}
pub fn collect_unit_consts(ast: &mut JavaAst, unit: &CompilationUnit) {
ast.const_fields.clear();
ast.const_scalars.clear();
ast.inline_methods.clear();
ast.field_types.clear();
ast.final_fields.clear();
ast.raw_generic_names = {
let mut m: std::collections::HashSet<String> = [
"List", "ArrayList", "LinkedList", "Collection", "Set", "HashSet",
"TreeSet", "LinkedHashSet", "Map", "HashMap", "TreeMap", "LinkedHashMap",
"Hashtable", "SortedMap", "SortedSet", "NavigableMap", "NavigableSet",
"Iterable", "Iterator", "ListIterator", "Comparator", "Optional",
"Stream", "Class", "Comparable", "Entry", "AbstractList", "AbstractMap",
"WeakHashMap", "IdentityHashMap", "ConcurrentHashMap", "ConcurrentMap",
"Queue", "Deque", "ArrayDeque", "Vector", "Stack", "Enumeration",
"Callable", "Future", "Supplier", "Function", "BiFunction", "Consumer",
"Predicate", "UnaryOperator", "BinaryOperator", "UnmodifiableList",
"AbstractSet", "AbstractCollection", "AbstractQueue", "Spliterator",
"EnumMap", "EnumSet", "PriorityQueue", "DelayQueue", "LinkedBlockingQueue",
"LinkedTransferQueue", "ConcurrentLinkedQueue", "ConcurrentSkipListMap",
"ConcurrentSkipListSet", "CopyOnWriteArrayList", "CopyOnWriteArraySet",
"ArrayBlockingQueue", "SynchronousQueue", "ThreadLocal", "Reference",
"SoftReference", "WeakReference", "AtomicReference", "AtomicStampedReference",
]
.iter()
.map(|x| x.to_string())
.collect();
fn collect_generics(ty: &TypeDecl, m: &mut std::collections::HashSet<String>) {
if !ty.ty_params.trim().is_empty() {
m.insert(ty.name.clone());
}
for mem in &ty.members {
if let Member::Type(t) = mem {
collect_generics(t, m);
}
}
}
for ty in &unit.types {
collect_generics(ty, &mut m);
}
m
};
ast.unit_shadows_lang = {
fn decl_shadows(ty: &TypeDecl) -> bool {
if ty.name == "String" || ty.name == "StringBuilder" {
return true;
}
ty.members.iter().any(|m| match m {
Member::Type(t) => decl_shadows(t),
_ => false,
})
}
unit.types.iter().any(decl_shadows)
};
let mut shadowed: std::collections::HashSet<String> = Default::default();
let mut write_counts: std::collections::HashMap<String, u32> = Default::default();
let mut field_decls: std::collections::HashMap<String, u32> = Default::default();
let mut bodies: Vec<JavaId> = Vec::new();
for ty in &unit.types {
collect_bodies_recursive(ty, &mut bodies, &mut shadowed, &mut field_decls);
}
for ty in &unit.types {
collect_field_types(ty, &mut ast.field_types, &mut ast.final_fields);
}
for &root in &bodies {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
if ast.kind(n) == NodeKind::VarDecl {
if let Some(nm) = ast.var_name(n) {
shadowed.insert(nm.to_string());
}
}
match ast.data(n) {
NodeData::Assign { .. } => {
let t = ast.children(n)[0];
if ast.kind(t) == NodeKind::Member {
if let NodeData::Member { name: sym } = ast.data(t) {
*write_counts.entry(ast.sn(*sym).to_string()).or_insert(0) += 1;
}
}
let mut ts = vec![t];
while let Some(x) = ts.pop() {
if ast.kind(x) == NodeKind::VarRef {
if let Some(nm) = ast.var_name(x) {
*write_counts.entry(nm.to_string()).or_insert(0) += 1;
}
}
for &c in ast.children(x) {
ts.push(c);
}
}
}
NodeData::Unary { op } if op.is_incdec() => {
let t = ast.children(n)[0];
if ast.kind(t) == NodeKind::VarRef {
if let Some(nm) = ast.var_name(t) {
*write_counts.entry(nm.to_string()).or_insert(0) += 1;
}
}
}
_ => {}
}
for &c in ast.children(n) {
stack.push(c);
}
}
}
for ty in &unit.types {
let mut own_lits: std::collections::HashMap<String, JavaId> = Default::default();
collect_own_clinit_lits(ast, ty, &mut own_lits);
for m in &ty.members {
if let Member::Field { mods, ty: fty, declarators } = m {
let words: Vec<&str> = mods.split_whitespace().collect();
let is_static = words.contains(&"static");
let is_final = words.contains(&"final");
let is_private = words.contains(&"private");
for d in declarators {
if field_decls.get(&d.name).copied().unwrap_or(0) != 1 {
continue;
}
let init = match d.init {
Some(init) => {
if !is_final
|| !is_const_init(ast, init)
|| shadowed.contains(&d.name)
|| write_counts.get(&d.name).copied().unwrap_or(0) != 0
{
continue;
}
init
}
None => {
if !is_static
|| (!is_final && !is_private)
|| shadowed.contains(&d.name)
|| write_counts.get(&d.name).copied().unwrap_or(0) != 1
{
continue;
}
match own_lits.get(&d.name) {
Some(&init) if is_const_init(ast, init) => init,
_ => continue,
}
}
};
if ast.const_fields.contains_key(&d.name) {
ast.const_fields.remove(&d.name);
ast.const_scalars.remove(&d.name);
} else {
let scalar_ok = scalar_lit_of(ast, init)
.is_some_and(|l| lit_type_matches_decl(fty, l));
ast.const_fields.insert(d.name.clone(), init);
if scalar_ok {
ast.const_scalars.insert(d.name.clone());
}
}
}
}
}
}
{
let identity_assert = [
"assertSame",
"assertNotSame",
"isSameInstanceAs",
"isNotSameInstanceAs",
"referenceEquals",
"same",
];
let mut hit = false;
for root in &bodies {
let mut stack = vec![*root];
while let Some(n) = stack.pop() {
if let NodeData::Call = ast.data(n) {
if let Some(&callee) = ast.children(n).first() {
let nm = match ast.data(callee) {
NodeData::Member { name } => ast.sn(*name),
NodeData::VarRef { name } => ast.sn(*name),
_ => "",
};
if identity_assert.contains(&nm) {
hit = true;
break;
}
}
}
for &c in ast.children(n) {
stack.push(c);
}
}
if hit {
break;
}
}
ast.unit_string_identity =
ast.any_string_identity(&bodies) || hit;
}
let mut method_name_count: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for_each_type(unit, &mut |ty: &TypeDecl| {
for m in &ty.members {
if let Member::Method { name, .. } = m {
*method_name_count.entry(name.clone()).or_insert(0) += 1;
}
}
});
for ty in &unit.types {
for m in &ty.members {
if let Member::Method { mods, name, params, body: Some(b), .. } = m {
let words: Vec<&str> = mods.split_whitespace().collect();
let non_overridable = words.contains(&"private")
|| words.contains(&"static")
|| words.contains(&"final");
if !non_overridable {
continue;
}
let ch = ast.children(*b).to_vec();
if ch.len() == 1 && ast.kind(ch[0]) == NodeKind::Return {
if let Some(&expr) = ast.children(ch[0]).first() {
if params.len() <= 3
&& !subtree_has_anon(ast, expr)
&& !params.iter().any(|p| p.varargs)
&& !subtree_calls_self(ast, expr, name)
&& !subtree_has_type_sensitive(ast, expr)
&& method_name_count.get(name).copied() == Some(1)
{
let ps: Vec<String> = params.iter().map(|p| p.name.clone()).collect();
let owner = ty.name.rsplit('.').next().unwrap_or(&ty.name).to_string();
ast.inline_methods.insert(name.clone(), (ps, expr, owner));
}
}
}
}
}
}
}
fn is_const_init(ast: &JavaAst, init: JavaId) -> bool {
match ast.kind(init) {
NodeKind::Literal => true,
NodeKind::ArrayLit => ast.children(init).iter().all(|&e| ast.kind(e) == NodeKind::Literal),
NodeKind::NewArray => {
let ch = ast.children(init).to_vec();
ch.len() == 1
&& ast.kind(ch[0]) == NodeKind::ArrayLit
&& ast.children(ch[0]).iter().all(|&e| ast.kind(e) == NodeKind::Literal)
}
_ => false,
}
}
fn collect_bodies_recursive(
ty: &TypeDecl,
bodies: &mut Vec<JavaId>,
shadowed: &mut std::collections::HashSet<String>,
field_decls: &mut std::collections::HashMap<String, u32>,
) {
if ty.kind == TypeKind::Enum {
for c in &ty.enum_constants {
if c.trim() == "," {
continue;
}
let last = c.split('(').next().unwrap_or(c)
.split_whitespace()
.next_back()
.unwrap_or("");
let name: String = last
.chars()
.take_while(|ch| ch.is_alphanumeric() || *ch == '_' || *ch == '$')
.collect();
if !name.is_empty() {
*field_decls.entry(name).or_insert(0) += 1;
}
}
}
for m in &ty.members {
match m {
Member::Method { body: Some(b), params, .. }
| Member::Constructor { body: Some(b), params, .. } => {
bodies.push(*b);
for p in params {
shadowed.insert(p.name.clone());
}
}
Member::Initializer { body, .. } => bodies.push(*body),
Member::Field { declarators, .. } => {
for d in declarators {
*field_decls.entry(d.name.clone()).or_insert(0) += 1;
if let Some(init) = d.init {
bodies.push(init);
}
}
}
Member::Type(t) => {
collect_bodies_recursive(t, bodies, shadowed, field_decls);
}
_ => {}
}
}
}
fn collect_field_types(
ty: &TypeDecl,
out: &mut std::collections::HashMap<String, JType>,
finals: &mut std::collections::HashSet<String>,
) {
for m in &ty.members {
match m {
Member::Field { mods, ty: fty, declarators } => {
let is_final = mods.split_whitespace().any(|w| w == "final");
for d in declarators {
if out.contains_key(&d.name) {
out.remove(&d.name);
finals.remove(&d.name);
} else {
out.insert(d.name.clone(), fty.clone());
if is_final {
finals.insert(d.name.clone());
}
}
}
}
Member::Type(t) => collect_field_types(t, out, finals),
_ => {}
}
}
}
fn collect_own_clinit_lits(
ast: &JavaAst,
ty: &TypeDecl,
out: &mut std::collections::HashMap<String, JavaId>,
) {
for m in &ty.members {
if let Member::Initializer { is_static: true, body } = m {
if ast.kind(*body) != NodeKind::Block {
continue;
}
for &st in ast.children(*body) {
let asg = match ast.kind(st) {
NodeKind::ExprStmt => {
let Some(&c) = ast.children(st).first() else {
continue;
};
if ast.kind(c) == NodeKind::Assign {
c
} else {
continue;
}
}
NodeKind::Assign => st,
_ => continue,
};
let ch = ast.children(asg).to_vec();
if ast.kind(ch[0]) != NodeKind::VarRef {
continue;
}
let Some(nm) = ast.var_name(ch[0]) else { continue };
if is_const_init(ast, ch[1]) {
out.insert(nm.to_string(), ch[1]);
}
}
}
}
}
fn scalar_lit_of(ast: &JavaAst, init: JavaId) -> Option<cure_engine::kind::LitRef<'_>> {
if ast.kind(init) == NodeKind::Literal {
ast.literal(init)
} else {
None
}
}
fn lit_type_matches_decl(fty: &JType, lit: cure_engine::kind::LitRef) -> bool {
use cure_engine::kind::LitRef;
match (fty, lit) {
(JType::Ref(r), LitRef::Str(_)) => {
let base = r.split('<').next().unwrap_or(r).trim();
base == "String" || base == "java.lang.String"
}
(JType::Char, LitRef::Char(_)) => true,
(JType::Bool, LitRef::Bool(_)) => true,
(JType::Int, LitRef::Int(_)) => true,
(JType::Long, LitRef::Long(_)) => true,
(JType::Float, LitRef::Float(_)) => true,
(JType::Double, LitRef::Double(_)) => true,
_ => false,
}
}
fn subtree_has_type_sensitive(ast: &JavaAst, root: JavaId) -> bool {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
match ast.kind(n) {
NodeKind::Switch | NodeKind::InstanceOf | NodeKind::Raw => return true,
_ => {}
}
for &c in ast.children(n) {
stack.push(c);
}
}
false
}
fn subtree_has_anon(ast: &JavaAst, root: JavaId) -> bool {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
if let NodeData::New { anon_raw: Some(_), .. } = ast.data(n) {
return true;
}
for &c in ast.children(n) {
stack.push(c);
}
}
false
}
fn subtree_calls_self(ast: &JavaAst, root: JavaId, name: &str) -> bool {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
if ast.kind(n) == NodeKind::Call {
let callee = ast.children(n)[0];
if ast.var_name(callee) == Some(name) {
return true;
}
if let NodeData::Member { name: m } = ast.data(callee) {
if ast.sn(*m) == name {
return true;
}
}
}
for &c in ast.children(n) {
stack.push(c);
}
}
false
}
pub struct StaticArrayIndexFold;
impl Rule<JavaAst> for StaticArrayIndexFold {
fn name(&self) -> &'static str {
"static_array_index_fold"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Index]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Index {
return None;
}
let ch = lang.children(id).to_vec();
if lang.kind(ch[0]) != NodeKind::VarRef {
return None;
}
let name = lang.var_name(ch[0])?.to_string();
let idx = lang.literal(ch[1]).and_then(|x| x.as_int())?;
let init = *lang.const_fields.get(&name)?;
let arr = match lang.kind(init) {
NodeKind::ArrayLit => init,
NodeKind::NewArray => {
let ic = lang.children(init).to_vec();
if ic.len() == 1 && lang.kind(ic[0]) == NodeKind::ArrayLit {
ic[0]
} else {
return None;
}
}
_ => return None,
};
let elems = lang.children(arr).to_vec();
if idx < 0 || idx >= elems.len() as i64 {
return None;
}
let elem = lang.copy_subtree(elems[idx as usize]);
Some(Edit::Replace {
target: id,
with: elem,
})
}
}
pub struct DeadPureCall;
impl Rule<JavaAst> for DeadPureCall {
fn name(&self) -> &'static str {
"dead_pure_call"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::ExprStmt]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::ExprStmt {
return None;
}
let Some(&call) = lang.children(id).first() else { return None };
if lang.kind(call) != NodeKind::Call {
return None;
}
let cch = lang.children(call).to_vec();
let Some(&callee) = cch.first() else { return None };
let (recv, name) = match lang.data(callee) {
NodeData::Member { name } => {
let Some(&recv_expr) = lang.children(callee).first() else { return None };
let recv_name = match lang.data(recv_expr) {
NodeData::VarRef { name } => lang.sn(*name).to_string(),
NodeData::Member { name } => lang.sn(*name).to_string(),
_ => return None,
};
if !recv_name
.chars()
.next()
.is_some_and(|c| c.is_uppercase())
{
return None;
}
(recv_name, lang.sn(*name))
}
_ => return None,
};
let args = &cch[1..];
let pure = match (recv.as_str(), name) {
("Long" | "Integer" | "Float" | "Double" | "Short" | "Byte" | "Character", "compare") => {
args.iter().all(|&a| match lang.data(a) {
NodeData::Literal(
Lit::Int(_) | Lit::Long(_) | Lit::Float(_) | Lit::Double(_),
) => true,
NodeData::Literal(Lit::NumRaw { .. }) => true,
NodeData::VarRef { .. } => {
let t = lang.var_type(a);
matches!(
t,
Some(JType::Int | JType::Long | JType::Float | JType::Double | JType::Char
| JType::Short | JType::Byte)
)
}
_ => false,
})
}
("Objects", "toString") => args.len() == 1,
_ => false,
};
if !pure {
return None;
}
Some(Edit::Delete { node: id })
}
}
pub struct CtorDelegationHoist;
fn hoist_resolve_arg(
lang: &mut JavaAst,
assign: &std::collections::HashMap<String, JavaId>,
a: JavaId,
visiting: &mut std::collections::HashSet<String>,
) -> Option<JavaId> {
let name = match lang.data(a) {
NodeData::VarRef { name } => lang.sn(*name).to_string(),
_ => return Some(a), };
let Some(&expr) = assign.get(&name) else {
return Some(a);
};
if !visiting.insert(name.clone()) {
return None; }
let r = hoist_subst(lang, assign, expr, visiting);
visiting.remove(&name);
r
}
fn hoist_subst(
lang: &mut JavaAst,
assign: &std::collections::HashMap<String, JavaId>,
e: JavaId,
visiting: &mut std::collections::HashSet<String>,
) -> Option<JavaId> {
match lang.data(e) {
NodeData::VarRef { name } => {
let n = lang.sn(*name).to_string();
if let Some(&src) = assign.get(&n) {
if !visiting.insert(n.clone()) {
return None;
}
let r = hoist_subst(lang, assign, src, visiting);
visiting.remove(&n);
r
} else {
Some(e)
}
}
_ => {
let ch = lang.children(e).to_vec();
let mut new_ch = Vec::with_capacity(ch.len());
for c in ch {
new_ch.push(hoist_subst(lang, assign, c, visiting)?);
}
Some(lang.clone_node(e, new_ch))
}
}
}
impl Rule<JavaAst> for CtorDelegationHoist {
fn name(&self) -> &'static str {
"ctor_delegation_hoist"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Block]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
if ctx.parent(id).is_some() {
return None;
}
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Block {
return None;
}
let stmts = lang.children(id).to_vec();
let mut ctor_idx: Option<usize> = None;
for (i, &st) in stmts.iter().enumerate() {
if *lang.data(st) != NodeData::ExprStmt {
continue;
}
let Some(&inner) = lang.children(st).first() else { continue };
if lang.kind(inner) != NodeKind::Call {
continue;
}
let Some(&callee) = lang.children(inner).first() else { continue };
if matches!(lang.data(callee), NodeData::This | NodeData::Super) {
ctor_idx = Some(i);
break;
}
}
let ci = ctor_idx?;
if ci == 0 {
return None; }
let mut last_assign: std::collections::HashMap<String, JavaId> =
std::collections::HashMap::new();
for &st in &stmts[..ci] {
let (name, value) = match lang.data(st) {
NodeData::VarDecl { name, .. } => {
let init = lang.children(st).first().copied();
match init {
Some(e) => (name.clone(), e),
None => continue, }
}
NodeData::ExprStmt => {
let Some(&a) = lang.children(st).first() else {
return None;
};
if lang.kind(a) != NodeKind::Assign
|| <JavaAst as Lang>::assign_op(&*lang, a).is_some()
{
return None;
}
let ch = lang.children(a).to_vec();
let (t, v) = (*ch.first()?, *ch.get(1)?);
if lang.kind(t) != NodeKind::VarRef || !lang.is_local_var(t) {
return None; }
let vn = lang.var_name(t)?;
(vn.to_string(), v)
}
_ => return None, };
last_assign.insert(name, value);
}
let call_stmt = stmts[ci];
let call = *lang.children(call_stmt).first()?;
let cch = lang.children(call).to_vec();
let callee = *cch.first()?;
let mut new_args: Vec<JavaId> = Vec::with_capacity(cch.len() - 1);
for &a in &cch[1..] {
new_args.push(hoist_resolve_arg(lang, &last_assign, a, &mut Default::default())?);
}
let callee_copy = lang.copy_subtree(callee);
let new_call = lang.call(callee_copy, new_args);
let new_stmt = lang.expr_stmt(new_call);
let mut insert: Vec<JavaId> = Vec::with_capacity(ci + 1);
insert.push(new_stmt);
insert.extend(stmts[..ci].iter().copied());
Some(Edit::Splice {
node: id,
index: 0,
remove: ci + 1,
insert,
})
}
}
pub struct TrailingElseFlatten;
impl Rule<JavaAst> for TrailingElseFlatten {
fn name(&self) -> &'static str {
"trailing_else_flatten"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::If]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let (parent, idx) = (ctx.parent(id), ctx.index(id));
let lang = ctx.lang;
if lang.kind(id) != NodeKind::If {
return None;
}
let ch = lang.children(id).to_vec();
if ch.len() != 3 {
return None;
}
let (_cond, then, els) = (ch[0], ch[1], ch[2]);
if lang.kind(then) != NodeKind::Block {
return None;
}
let then_stmts = lang.children(then).to_vec();
let then_terminal = then_stmts
.last()
.is_some_and(|&s| matches!(lang.kind(s), NodeKind::Return | NodeKind::Throw));
if !then_terminal {
return None;
}
if lang.kind(els) == NodeKind::Block {
let else_stmts = lang.children(els).to_vec();
if else_stmts.len() == 1 && lang.kind(else_stmts[0]) == NodeKind::Return {
if lang.children(else_stmts[0]).is_empty() {
return Some(Edit::Splice {
node: id,
index: 0,
remove: 3,
insert: vec![ch[0], then],
});
}
}
}
let parent = parent?;
if lang.kind(parent) != NodeKind::Block {
return None;
}
let idx = idx?;
let new_if = lang.clone_node(id, vec![ch[0], then]);
let mut insert: Vec<JavaId> = vec![new_if];
if lang.kind(els) == NodeKind::Block {
insert.extend(lang.children(els).to_vec());
} else {
insert.push(els);
}
Some(Edit::Splice {
node: parent,
index: idx,
remove: 1,
insert,
})
}
}
pub struct TrailingIfContinue;
impl Rule<JavaAst> for TrailingIfContinue {
fn name(&self) -> &'static str {
"trailing_if_continue"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::If]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let parent = ctx.parent(id);
let grandparent = parent.and_then(|p| ctx.parent(p));
let lang = ctx.lang;
if lang.kind(id) != NodeKind::If {
return None;
}
let ch = lang.children(id).to_vec();
if ch.len() != 2 {
return None;
}
let then = ch[1];
if lang.kind(then) != NodeKind::Block {
return None;
}
let tbody = lang.children(then).to_vec();
if tbody.len() != 1 || lang.kind(tbody[0]) != NodeKind::Continue {
return None;
}
if !matches!(lang.data(tbody[0]), NodeData::Continue { label: None }) {
return None;
}
let p = parent?;
if lang.kind(p) != NodeKind::Block {
return None;
}
let pch = lang.children(p).to_vec();
if pch.last() != Some(&id) {
return None;
}
let gp = grandparent?;
match lang.kind(gp) {
NodeKind::While | NodeKind::For | NodeKind::ForEach | NodeKind::DoWhile => {
let gch = lang.children(gp).to_vec();
if gch.last() == Some(&p) {
Some(Edit::Delete { node: id })
} else {
None
}
}
_ => None,
}
}
}
pub struct TrailingAssignReturn;
impl Rule<JavaAst> for TrailingAssignReturn {
fn name(&self) -> &'static str {
"trailing_assign_return"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Assign]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let parent = ctx.parent(id);
let grandparent = parent.and_then(|p| ctx.parent(p));
let lang = ctx.lang;
if !matches!(lang.data(id), NodeData::Assign { op: None }) {
return None;
}
let ch = lang.children(id).to_vec();
if ch.len() != 2 {
return None;
}
let (target, value) = (ch[0], ch[1]);
if lang.kind(target) != NodeKind::VarRef || !lang.is_local_var(target) {
return None;
}
let name = lang.var_name(target)?.to_string();
let parent = parent?;
let stmt = if lang.kind(parent) == NodeKind::ExprStmt {
parent
} else if lang.kind(parent) == NodeKind::Block {
id
} else {
return None;
};
let block = if lang.kind(parent) == NodeKind::ExprStmt {
grandparent?
} else {
parent
};
if lang.kind(block) != NodeKind::Block {
return None;
}
let stmts = lang.children(block).to_vec();
let idx = stmts.iter().position(|&x| x == stmt)?;
let next = *stmts.get(idx + 1)?;
let (flow_val, is_return) = match lang.kind(next) {
NodeKind::Return => (lang.children(next).first().copied(), true),
NodeKind::Throw => (lang.children(next).first().copied(), false),
_ => return None,
};
let flow_val = flow_val?;
if lang.kind(flow_val) != NodeKind::VarRef || lang.var_name(flow_val) != Some(name.as_str()) {
return None;
}
for &s in &stmts[idx + 2..] {
if cure_engine::analysis::subtree_contains(lang, s, |n| {
lang.kind(n) == NodeKind::VarRef && lang.var_name(n) == Some(name.as_str())
}) {
return None;
}
}
let new_flow = if is_return {
lang.ret(Some(value))
} else {
lang.throw(value)
};
Some(Edit::Multi(vec![
Edit::Replace { target: next, with: new_flow },
Edit::Delete { node: stmt },
]))
}
}
pub struct ConstFieldPropagate;
impl Rule<JavaAst> for ConstFieldPropagate {
fn name(&self) -> &'static str {
"const_field_propagate"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::VarRef]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let parent = ctx.parent(id);
let _grandparent = parent.and_then(|p| ctx.parent(p));
let lang = ctx.lang;
if lang.kind(id) != NodeKind::VarRef {
return None;
}
let name = lang.var_name(id)?.to_string();
if !lang.const_scalars.contains(&name) {
return None;
}
if let Some(p) = parent {
let is_first = lang.children(p).first() == Some(&id);
match lang.kind(p) {
NodeKind::Assign if is_first => return None,
NodeKind::Unary if is_first => {
if let NodeData::Unary { op } = lang.data(p) {
if op.is_incdec() {
return None;
}
}
}
NodeKind::Call if is_first => return None,
_ => {}
}
}
let init = *lang.const_fields.get(&name)?;
let lit = lang.copy_subtree(init);
Some(Edit::Replace { target: id, with: lit })
}
}
pub struct NoopPrivateCall;
impl Rule<JavaAst> for NoopPrivateCall {
fn name(&self) -> &'static str {
"noop_private_call"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::ExprStmt]
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let lang = ctx.lang;
if lang.kind(id) != NodeKind::ExprStmt {
return None;
}
let ch = lang.children(id).to_vec();
if ch.len() != 1 || lang.kind(ch[0]) != NodeKind::Call {
return None;
}
let call = ch[0];
let cch = lang.children(call).to_vec();
if cch.is_empty() || lang.kind(cch[0]) != NodeKind::VarRef {
return None; }
let name = lang.var_name(cch[0])?.to_string();
let arity = cch.len() - 1;
let cands = lang.noop_private_methods.get(&name)?.get(&arity)?;
if cands.is_empty() {
return None;
}
let arg_types: Vec<Option<JType>> =
cch[1..].iter().map(|&a| static_arg_type(lang, a)).collect();
let mut applicable_noop = false;
let mut applicable_count = 0usize;
for c in cands {
let ok = c
.params
.iter()
.zip(arg_types.iter())
.all(|(pt, at)| arg_applicable(at.as_ref(), pt));
if ok {
applicable_count += 1;
applicable_noop = c.noop;
}
}
if applicable_count != 1 || !applicable_noop {
return None;
}
for &arg in &cch[1..] {
if lang.effect(arg) > Effect::MayRead {
return None;
}
}
Some(Edit::Delete { node: id })
}
}
fn static_arg_type(lang: &JavaAst, arg: JavaId) -> Option<JType> {
match lang.kind(arg) {
NodeKind::Literal => match lang.literal(arg)? {
LitRef::Str(_) => Some(JType::Ref("String".to_string())),
LitRef::Char(_) => Some(JType::Char),
LitRef::Bool(_) => Some(JType::Bool),
LitRef::Int(_) => Some(JType::Int),
LitRef::Long(_) => Some(JType::Long),
LitRef::Float(_) => Some(JType::Float),
LitRef::Double(_) => Some(JType::Double),
LitRef::Null => None,
},
NodeKind::VarRef => {
let nm = lang.var_name(arg)?;
lang.var_type(arg)
.cloned()
.or_else(|| lang.field_types.get(nm).cloned())
}
_ => None,
}
}
fn arg_applicable(at: Option<&JType>, pt: &JType) -> bool {
let Some(a) = at else { return true };
match pt {
JType::Ref(p) => {
let pbase = p.split('<').next().unwrap_or(p).trim();
pbase == "Object"
|| pbase == "java.lang.Object"
|| match a {
JType::Ref(arend) => {
let abase = arend.split('<').next().unwrap_or(arend).trim();
abase == pbase
}
_ => false,
}
}
JType::Array(_) => false, prim => match a {
p2 => std::mem::discriminant(p2) == std::mem::discriminant(prim),
},
}
}
pub struct ConstMethodInline;
impl Rule<JavaAst> for ConstMethodInline {
fn name(&self) -> &'static str {
"const_method_inline"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Call]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let at_stmt_pos = ctx
.parent(id)
.map(|p| ctx.lang.kind(p) == NodeKind::ExprStmt)
.unwrap_or(false);
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Call {
return None;
}
let ch = lang.children(id).to_vec();
let (method_name, recv_class) = match lang.data(ch[0]) {
NodeData::VarRef { name } => (*name, None),
NodeData::Member { name } => {
let recv = lang.children(ch[0])[0];
match lang.var_name(recv) {
Some(r) if r == "this" => (*name, None),
Some(r) if r.chars().next().is_some_and(|c| c.is_uppercase()) => {
(*name, Some(r.to_string()))
}
_ => return None,
}
}
_ => return None,
};
let entry = lang.inline_methods.get(lang.sn(method_name)).cloned()?;
let (params, body_expr, owner) = entry;
if let Some(recv) = recv_class {
if recv != owner {
return None;
}
}
{
let names: Vec<String> = lang.inline_methods.keys().cloned().collect();
for nm in &names {
if subtree_calls_self(lang, body_expr, nm) {
return None;
}
}
}
let args = ch[1..].to_vec();
if args.len() != params.len() || params.is_empty() {
return None;
}
for &a in &args {
lang.literal(a)?;
}
let map: std::collections::HashMap<String, JavaId> = params
.iter()
.zip(args.iter())
.map(|(p, &a)| (p.clone(), a))
.collect();
let with = copy_subst(lang, body_expr, &map);
if at_stmt_pos {
match lang.kind(with) {
NodeKind::Call | NodeKind::New => {}
_ => return None,
}
}
Some(Edit::Replace {
target: id,
with,
})
}
}
fn copy_subst(
lang: &mut JavaAst,
node: JavaId,
map: &std::collections::HashMap<String, JavaId>,
) -> JavaId {
if let NodeData::VarRef { name } = lang.data(node) {
if let Some(&repl) = map.get(lang.sn(*name)) {
return lang.copy_subtree(repl);
}
}
let children: Vec<JavaId> = lang.children(node).to_vec();
let new_children: Vec<JavaId> = children
.iter()
.map(|&c| copy_subst(lang, c, map))
.collect();
lang.clone_node(node, new_children)
}
pub struct TwrRecover;
fn twr_is_throwable(ty_raw: &str) -> bool {
ty_raw == "Throwable" || ty_raw == "java.lang.Throwable"
}
fn unwrap_expr_stmt(lang: &JavaAst, n: JavaId) -> JavaId {
if lang.kind(n) == NodeKind::ExprStmt {
if let Some(&e) = lang.children(n).first() {
return e;
}
}
n
}
fn twr_stmts(lang: &JavaAst, n: JavaId) -> Vec<JavaId> {
if lang.kind(n) == NodeKind::Block {
lang.children(n).to_vec()
} else {
vec![n]
}
}
fn twr_is_close_call(lang: &JavaAst, e: JavaId, r: &str) -> bool {
if lang.kind(e) != NodeKind::Call {
return false;
}
let ch = lang.children(e);
if ch.len() != 1 {
return false;
}
match lang.data(ch[0]) {
NodeData::Member { name } if lang.sn(*name) == "close" => {
let Some(&recv) = lang.children(ch[0]).first() else {
return false;
};
lang.kind(recv) == NodeKind::VarRef && lang.var_name(recv) == Some(r)
}
_ => false,
}
}
fn twr_is_close_stmt(lang: &JavaAst, n: JavaId, r: &str) -> bool {
if lang.kind(n) != NodeKind::ExprStmt {
return false;
}
lang.children(n)
.first()
.is_some_and(|&e| twr_is_close_call(lang, e, r))
}
fn twr_is_add_suppressed(lang: &JavaAst, n: JavaId, primary: &str, sup: &str) -> bool {
if lang.kind(n) != NodeKind::ExprStmt {
return false;
}
let Some(&e) = lang.children(n).first() else {
return false;
};
if lang.kind(e) != NodeKind::Call {
return false;
}
let ch = lang.children(e);
if ch.len() != 2 {
return false;
}
match lang.data(ch[0]) {
NodeData::Member { name } if lang.sn(*name) == "addSuppressed" => {
let Some(&recv) = lang.children(ch[0]).first() else {
return false;
};
lang.kind(recv) == NodeKind::VarRef
&& lang.var_name(recv) == Some(primary)
&& lang.kind(ch[1]) == NodeKind::VarRef
&& lang.var_name(ch[1]) == Some(sup)
}
_ => false,
}
}
fn node_contains(lang: &JavaAst, root: JavaId, target: JavaId) -> bool {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
if n == target {
return true;
}
stack.extend(lang.children(n).iter().copied());
}
false
}
fn refs_escape(lang: &JavaAst, root: JavaId, name: &str, allowed: &[JavaId]) -> bool {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
if lang.kind(n) == NodeKind::VarRef && lang.var_name(n) == Some(name) {
let ok = allowed
.iter()
.any(|&a| a == n || node_contains(lang, a, n));
if !ok {
return true;
}
}
if lang.kind(n) == NodeKind::VarDecl {
if let Some(nm) = lang.var_name(n) {
if nm == name && !allowed.contains(&n) {
return true;
}
}
}
stack.extend(lang.children(n).iter().copied());
}
false
}
fn twr_close_with_suppressed(
lang: &JavaAst,
try_node: JavaId,
primary: &str,
) -> Option<(String, String)> {
let ch = lang.children(try_node).to_vec();
if ch.len() != 2 || lang.kind(ch[0]) != NodeKind::Block {
return None;
}
let NodeData::Catch { ty_raw, name: sup } = lang.data(ch[1]) else {
return None;
};
if !twr_is_throwable(ty_raw) {
return None;
}
let sup = sup.clone();
let catch_stmts = twr_stmts(lang, *lang.children(ch[1]).first()?);
if catch_stmts.len() != 1 || !twr_is_add_suppressed(lang, catch_stmts[0], primary, &sup) {
return None;
}
let close_stmts = twr_stmts(lang, ch[0]);
if close_stmts.len() != 1 {
return None;
}
if lang.kind(close_stmts[0]) != NodeKind::ExprStmt {
return None;
}
let e = *lang.children(close_stmts[0]).first()?;
let cc = lang.children(e);
if cc.len() != 1 {
return None;
}
let NodeData::Member { name } = lang.data(cc[0]) else {
return None;
};
if lang.sn(*name) != "close" {
return None;
}
let recv = *lang.children(cc[0]).first()?;
if lang.kind(recv) != NodeKind::VarRef {
return None;
}
Some((lang.var_name(recv)?.to_string(), sup))
}
impl Rule<JavaAst> for TwrRecover {
fn name(&self) -> &'static str {
"twr_recover"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Try]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let (parent, idx, walk_root) = (ctx.parent(id)?, ctx.index(id)?, ctx.root());
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Try {
return None;
}
let ch = lang.children(id).to_vec();
let try_idx = ch
.iter()
.position(|&c| lang.kind(c) == NodeKind::Block)?;
if try_idx != 0 {
return None;
}
let try_block = ch[0];
if lang.kind(parent) != NodeKind::Block {
return None;
}
let pch = lang.children(parent).to_vec();
if ch.len() == 2 {
if let NodeData::Catch { ty_raw, name: t } = lang.data(ch[1]) {
if twr_is_throwable(ty_raw) {
let t = t.clone();
let catch_stmts = twr_stmts(lang, *lang.children(ch[1]).first()?);
if catch_stmts.len() == 2 {
let (inner_try, throw_stmt) = (catch_stmts[0], catch_stmts[1]);
let throw_ok = lang.kind(throw_stmt) == NodeKind::Throw
&& lang
.children(throw_stmt)
.first()
.is_some_and(|&x| {
lang.kind(x) == NodeKind::VarRef
&& lang.var_name(x) == Some(t.as_str())
});
if throw_ok && lang.kind(inner_try) == NodeKind::Try {
if let Some((r, sup)) =
twr_close_with_suppressed(lang, inner_try, t.as_str())
{
if idx >= 1 && idx + 1 < pch.len() {
let decl_r = pch[idx - 1];
let trailing = pch[idx + 1];
if let NodeData::VarDecl { name, ty, .. } = lang.data(decl_r) {
if name == &r {
let (name, ty) = (name.clone(), ty.clone());
let init =
lang.children(decl_r).first().copied();
if let Some(init) = init {
let provably_new = lang.kind(init)
== NodeKind::New;
let close_ok =
twr_is_close_stmt(lang, trailing, &r);
if provably_new && close_ok {
let root = walk_root;
let r_ok = !refs_escape(
lang,
root,
&r,
&[
decl_r,
init,
*lang
.children(inner_try)
.first()
.unwrap_or(&inner_try),
trailing,
try_block,
],
);
let t_ok = !refs_escape(
lang, root, &t,
&[throw_stmt, ch[1]],
);
let sup_ok = !refs_escape(
lang, root, &sup,
&[ch[1]],
);
if r_ok && t_ok && sup_ok {
let init_copy =
lang.copy_subtree(init);
let res = lang.var_decl(
&name, ty,
Some(init_copy),
);
let new_try = lang.try_(
vec![res],
try_block,
Vec::new(),
None,
);
return Some(Edit::Splice {
node: parent,
index: idx - 1,
remove: 3,
insert: vec![new_try],
});
}
}
}
}
}
}
}
}
}
}
}
}
if ch.len() == 3 {
let NodeData::Catch { ty_raw, name: t } = lang.data(ch[1]) else {
return None;
};
if !twr_is_throwable(ty_raw) {
return None;
}
let t = t.clone();
if lang.kind(ch[2]) != NodeKind::Block {
return None;
}
let fin_stmts = twr_stmts(lang, ch[2]);
if fin_stmts.len() != 1 || lang.kind(fin_stmts[0]) != NodeKind::If {
return None;
}
let catch_stmts = twr_stmts(lang, *lang.children(ch[1]).first()?);
if catch_stmts.len() != 2 {
return None;
}
let (assign_stmt, throw_stmt) = (catch_stmts[0], catch_stmts[1]);
let assign_expr = unwrap_expr_stmt(lang, assign_stmt);
let NodeData::Assign { op: None } = lang.data(assign_expr) else {
return None;
};
let as_ch = lang.children(assign_expr);
if as_ch.len() != 2 {
return None;
}
let primary = match lang.data(as_ch[0]) {
NodeData::VarRef { name } => lang.sn(*name).to_string(),
_ => return None,
};
let assign_ok = lang.kind(as_ch[1]) == NodeKind::VarRef
&& lang.var_name(as_ch[1]) == Some(t.as_str())
&& lang.kind(as_ch[0]) == NodeKind::VarRef
&& lang.var_name(as_ch[0]) == Some(primary.as_str());
let throw_ok = lang.kind(throw_stmt) == NodeKind::Throw
&& lang.children(throw_stmt)
.first()
.is_some_and(|&x| {
lang.kind(x) == NodeKind::VarRef
&& lang.var_name(x) == Some(t.as_str())
});
if !assign_ok || !throw_ok {
return None;
}
let if1 = fin_stmts[0];
let i1 = lang.children(if1).to_vec();
if i1.len() < 2 {
return None;
}
let cond1 = i1[0];
let NodeData::Binary { op: BinOp::Ne } = lang.data(cond1) else {
return None;
};
let c1 = lang.children(cond1);
let r = match lang.data(c1[0]) {
NodeData::VarRef { name } if lang.literal(c1[1]) == Some(LitRef::Null) => {
lang.sn(*name).to_string()
}
_ => {
return None;
}
};
let then1 = i1[1];
let inner_stmts = twr_stmts(lang, then1);
if inner_stmts.len() != 1 || lang.kind(inner_stmts[0]) != NodeKind::If {
return None;
}
let if2 = inner_stmts[0];
let i2 = lang.children(if2).to_vec();
if i2.len() != 3 {
return None;
}
let NodeData::Binary { op: BinOp::Ne } = lang.data(i2[0]) else {
return None;
};
let c2 = lang.children(i2[0]);
let primary_ok = lang.kind(c2[0]) == NodeKind::VarRef
&& lang.var_name(c2[0]) == Some(primary.as_str())
&& lang.literal(c2[1]) == Some(LitRef::Null);
if !primary_ok {
return None;
}
let (then2, els2) = (i2[1], i2[2]);
let t2_stmts = twr_stmts(lang, then2);
if t2_stmts.len() != 1 || lang.kind(t2_stmts[0]) != NodeKind::Try {
return None;
}
let Some((r2, sup)) =
twr_close_with_suppressed(lang, t2_stmts[0], primary.as_str())
else {
return None;
};
if r2 != r {
return None;
}
let e2_stmts = twr_stmts(lang, els2);
if e2_stmts.len() != 1 || !twr_is_close_stmt(lang, e2_stmts[0], &r) {
return None;
}
if idx < 2 {
return None;
}
let decl_r = pch[idx - 2];
let decl_primary = pch[idx - 1];
let (r_decl, primary_decl) = match (lang.data(decl_r), lang.data(decl_primary)) {
(
NodeData::VarDecl { name: rn, .. },
NodeData::VarDecl { name: pn, .. },
) => (rn.clone(), pn.clone()),
_ => return None,
};
if r_decl != r || primary_decl != primary {
return None;
}
let pinit_null = lang
.children(decl_primary)
.first()
.is_some_and(|&n| lang.literal(n) == Some(LitRef::Null));
if !pinit_null {
return None;
}
let r_init = lang.children(decl_r).first().copied()?;
let ty = match lang.data(decl_r) {
NodeData::VarDecl { ty, .. } => ty.clone(),
_ => return None,
};
let root = walk_root;
let r_allowed = [decl_r, r_init, ch[2], try_block];
let t_allowed = [throw_stmt, assign_stmt, ch[1], ch[2]];
let p_allowed = [decl_primary, assign_stmt, ch[2]];
if refs_escape(lang, root, &r, &r_allowed)
|| refs_escape(lang, root, &primary, &p_allowed)
|| refs_escape(lang, root, &t, &t_allowed)
|| refs_escape(lang, root, &sup, &[ch[2]])
{
return None;
}
let init_copy = lang.copy_subtree(r_init);
let res = lang.var_decl(&r, ty, Some(init_copy));
let new_try = lang.try_(vec![res], try_block, Vec::new(), None);
return Some(Edit::Splice {
node: parent,
index: idx - 2,
remove: 3,
insert: vec![new_try],
});
}
None
}
}
pub struct StringSwitchRecover;
fn subtree_sig(lang: &JavaAst, n: JavaId, out: &mut String) {
out.push_str(&format!("{:?} ", lang.data(n)));
for &c in lang.children(n) {
subtree_sig(lang, c, out);
}
}
fn ssw_bodies(lang: &JavaAst, sw: JavaId) -> Option<Vec<(Option<i64>, Vec<JavaId>)>> {
let ch = lang.children(sw).to_vec();
let mut out = Vec::new();
for &c in &ch[1..] {
let NodeData::Case { labels, is_default, .. } = lang.data(c) else {
return None;
};
let is_default = *is_default;
let cc = lang.children(c);
let label = if is_default {
None
} else {
if *labels != 1 {
return None; }
let lab = cc[0];
let lit = lang.literal(lab)?;
let v = match lit {
LitRef::Int(v) | LitRef::Long(v) => v,
_ => return None, };
Some(v)
};
out.push((label, cc[*labels as usize..].to_vec()));
}
Some(out)
}
fn ssw_body_terminal(lang: &JavaAst, stmts: &[JavaId]) -> bool {
let Some(&last) = stmts.last() else {
return false; };
let last = if lang.kind(last) == NodeKind::Block {
match lang.children(last).last() {
Some(&l) => l,
None => return false,
}
} else {
last
};
matches!(
lang.kind(last),
NodeKind::Return | NodeKind::Break | NodeKind::Throw | NodeKind::Continue
)
}
impl Rule<JavaAst> for StringSwitchRecover {
fn name(&self) -> &'static str {
"string_switch_recover"
}
fn kinds(&self) -> &'static [NodeKind] {
&[NodeKind::Switch]
}
fn structural(&self) -> bool {
true
}
fn check(&self, ctx: RewriteCtx<'_, JavaAst>, id: JavaId) -> Option<Edit<JavaAst>> {
let (parent, idx, ctx_root) = (ctx.parent(id)?, ctx.index(id)?, ctx.root());
let lang = ctx.lang;
if lang.kind(id) != NodeKind::Switch {
return None;
}
let ch = lang.children(id).to_vec();
if ch.len() < 2 {
return None;
}
let x = match lang.data(ch[0]) {
NodeData::Call => {
let cc = lang.children(ch[0]);
if cc.len() != 1 {
return None;
}
match lang.data(cc[0]) {
NodeData::Member { name } if lang.sn(*name) == "hashCode" => {
let recv = *lang.children(cc[0]).first()?;
if lang.kind(recv) != NodeKind::VarRef {
return None;
}
lang.var_name(recv)?.to_string()
}
_ => return None,
}
}
_ => {
return None;
}
};
struct CaseInfo {
lit: String,
k: i64,
inner_switch: JavaId,
}
let mut infos: Vec<CaseInfo> = Vec::new();
let mut v_name: Option<String> = None;
let mut v_nodes: Vec<JavaId> = Vec::new(); let mut guard_nodes: Vec<JavaId> = Vec::new();
for &c in &ch[1..] {
let NodeData::Case { labels, is_default, .. } = lang.data(c) else {
return None;
};
let is_default = *is_default;
if is_default {
return None; }
if *labels != 1 {
return None;
}
let cc = lang.children(c);
let h = match lang.literal(cc[0])? {
LitRef::Int(v) | LitRef::Long(v) => v as i32,
_ => return None,
};
let body = cc[1..].to_vec();
let body = if body.len() == 1 && lang.kind(body[0]) == NodeKind::Block {
lang.children(body[0]).to_vec()
} else {
body
};
let mut rest = body;
let mut _e_decl: Option<(String, JavaId)> = None;
if let Some(&first) = rest.first() {
if let NodeData::VarDecl { name, .. } = lang.data(first) {
if let Some(&init) = lang.children(first).first() {
if is_x_equals(lang, init, &x) {
_e_decl = Some((name.clone(), first));
rest = rest[1..].to_vec();
}
}
}
}
if rest.len() != 1 || lang.kind(rest[0]) != NodeKind::If {
return None;
}
let ifn = rest[0];
let ic = lang.children(ifn).to_vec();
if ic.len() != 3 {
return None;
}
let NodeData::Unary { op: UnOp::Not } = lang.data(ic[0]) else {
return None;
};
let guard = *lang.children(ic[0]).first()?;
let lit = match lang.data(guard) {
NodeData::VarRef { name } => {
let Some((en, decl)) = &_e_decl else {
return None;
};
if lang.sn(*name) != en {
return None;
}
let init = *lang.children(*decl).first()?;
equals_lit(lang, init, &x)?
}
NodeData::Call => equals_lit(lang, guard, &x)?,
_ => return None,
};
guard_nodes.push(guard);
if let Some((_, decl)) = &_e_decl {
guard_nodes.push(*decl);
}
let then_ok = twr_stmts(lang, ic[1]).len() == 1
&& matches!(lang.data(twr_stmts(lang, ic[1])[0]),
NodeData::Break { label: None });
if !then_ok {
return None;
}
let els = twr_stmts(lang, ic[2]);
let (k, inner_switch, assign_node) = match els.len() {
2 => {
let assign_expr = unwrap_expr_stmt(lang, els[0]);
let NodeData::Assign { op: None } = lang.data(assign_expr) else {
return None;
};
let a_ch = lang.children(assign_expr);
let vn = match lang.data(a_ch[0]) {
NodeData::VarRef { name } => lang.sn(*name).to_string(),
_ => return None,
};
let k = match lang.literal(a_ch[1])? {
LitRef::Int(v) | LitRef::Long(v) => v,
_ => return None,
};
if lang.kind(els[1]) != NodeKind::Switch {
return None;
}
let sel = *lang.children(els[1]).first()?;
if lang.kind(sel) != NodeKind::VarRef
|| lang.var_name(sel) != Some(vn.as_str())
{
return None;
}
match &v_name {
Some(v) if *v != vn => return None,
_ => v_name = Some(vn),
}
(k, els[1], Some(els[0]))
}
1 => {
if lang.kind(els[0]) != NodeKind::Switch {
return None;
}
let sel = *lang.children(els[0]).first()?;
let k = match lang.literal(sel)? {
LitRef::Int(v) | LitRef::Long(v) => v,
_ => return None,
};
(k, els[0], None)
}
_ => return None,
};
if let Some(a) = assign_node {
v_nodes.push(a);
}
v_nodes.push(*lang.children(inner_switch).first()?);
let hlit: i32 = lit
.chars()
.fold(0i32, |acc, ch| acc.wrapping_mul(31).wrapping_add(ch as i32));
if hlit != h {
return None;
}
let _ = assign_node;
infos.push(CaseInfo {
lit,
k,
inner_switch,
});
}
if infos.is_empty() {
return None;
}
if lang.kind(parent) != NodeKind::Block {
return None;
}
let pch = lang.children(parent).to_vec();
let mut k = idx + 1;
if k < pch.len() {
let texpr = unwrap_expr_stmt(lang, pch[k]);
if let NodeData::Assign { op: None } = lang.data(texpr) {
let a_ch = lang.children(texpr);
if a_ch.len() == 2
&& lang.kind(a_ch[0]) == NodeKind::VarRef
&& lang.var_name(a_ch[0]) == v_name.as_deref()
&& lang.literal(a_ch[1]) == Some(LitRef::Int(-1))
{
v_nodes.push(pch[k]);
k += 1;
}
}
}
if k >= pch.len() || lang.kind(pch[k]) != NodeKind::Switch {
return None;
}
let trailing = pch[k];
let tsel = *lang.children(trailing).first()?;
match lang.data(tsel) {
NodeData::VarRef { name } if Some(lang.sn(*name)) == v_name.as_deref() => {
v_nodes.push(tsel);
}
NodeData::Literal(Lit::Int(_)) | NodeData::Literal(Lit::Long(_)) => {}
_ => {
lang.literal(tsel)?;
}
}
fn switch_cases_sig(lang: &JavaAst, sw: JavaId, out: &mut String) {
for &c in &lang.children(sw)[1..] {
subtree_sig(lang, c, out);
}
}
let mut sig0 = String::new();
switch_cases_sig(lang, infos[0].inner_switch, &mut sig0);
for info in &infos[1..] {
let mut sig = String::new();
switch_cases_sig(lang, info.inner_switch, &mut sig);
if sig != sig0 {
return None;
}
}
{
let mut sig = String::new();
switch_cases_sig(lang, trailing, &mut sig);
if sig != sig0 {
return None;
}
}
let bodies = ssw_bodies(lang, infos[0].inner_switch)?;
let mut body_map: std::collections::HashMap<i64, Vec<JavaId>> =
std::collections::HashMap::new();
let mut default_body: Option<Vec<JavaId>> = None;
for (lab, stmts) in &bodies {
match lab {
Some(v) => {
body_map.insert(*v, stmts.clone());
}
None => default_body = Some(stmts.clone()),
}
}
let Some(default_body) = default_body else {
return None;
};
for stmts in body_map.values().chain(std::iter::once(&default_body)) {
if !ssw_body_terminal(lang, stmts) {
return None;
}
}
let root = ctx_root;
if let Some(vn) = &v_name {
let mut allowed: Vec<JavaId> = v_nodes.clone();
allowed.push(trailing);
if idx >= 1 {
let cand = pch[idx - 1];
if let NodeData::VarDecl { name, .. } = lang.data(cand) {
if name == vn {
allowed.push(cand);
if idx != 1 {
allowed.pop();
}
}
}
}
if refs_escape(lang, root, vn, &allowed) {
return None;
}
}
let mut new_cases: Vec<JavaId> = Vec::new();
for info in &infos {
let Some(body) = body_map.get(&info.k) else {
return None;
};
let lab = lang.lit(Lit::Str(info.lit.clone()));
let case = lang.case_(vec![lab], false, false, body.clone());
new_cases.push(case);
}
let def_case = lang.case_(Vec::new(), true, false, default_body);
new_cases.push(def_case);
let sel = lang.var(&x);
let new_switch = lang.switch_(sel, new_cases);
let remove = k - idx + 1;
Some(Edit::Splice {
node: parent,
index: idx,
remove,
insert: vec![new_switch],
})
}
}
fn equals_lit(lang: &JavaAst, e: JavaId, x: &str) -> Option<String> {
if lang.kind(e) != NodeKind::Call {
return None;
}
let cc = lang.children(e);
if cc.len() != 2 {
return None;
}
match lang.data(cc[0]) {
NodeData::Member { name } if lang.sn(*name) == "equals" => {
let recv = *lang.children(cc[0]).first()?;
if lang.kind(recv) != NodeKind::VarRef || lang.var_name(recv) != Some(x) {
return None;
}
match lang.literal(cc[1])? {
LitRef::Str(s) => Some(s.to_string()),
_ => None,
}
}
_ => None,
}
}
fn is_x_equals(lang: &JavaAst, e: JavaId, x: &str) -> bool {
lang.kind(e) == NodeKind::Call && equals_lit(lang, e, x).is_some()
}
pub fn default_java_rules() -> Vec<Box<dyn Rule<JavaAst>>> {
let mut rules: Vec<Box<dyn Rule<JavaAst>>> = cure_engine::rules::default_rules();
rules.push(Box::new(CastSimplify));
rules.push(Box::new(SelfCompare));
rules.push(Box::new(StringBuilderFold));
rules.push(Box::new(BoxUnboxChain));
rules.push(Box::new(IteratorToForEach));
rules.push(Box::new(NewStringFold));
rules.push(Box::new(NewStringCharArrayFold));
rules.push(Box::new(EmptyFinallyStrip));
rules.push(Box::new(TryUnwrapNoCatch));
rules.push(Box::new(TryUnwrapRethrow));
rules.push(Box::new(StaticExec));
rules.push(Box::new(LoopHeadBreak));
rules.push(Box::new(WhileIteratorToForEach));
rules.push(Box::new(ConcatValueOfDrop));
rules.push(Box::new(StringBuilderStatements));
rules.push(Box::new(TrailingContinueJava));
rules.push(Box::new(XorNoise));
rules.push(Box::new(StrLenFold));
rules.push(Box::new(LiteralEval));
rules.push(Box::new(Base64NewStringFold));
rules.push(Box::new(UrlDecodeFold));
rules.push(Box::new(CffRecover));
rules.push(Box::new(StaticArrayIndexFold));
rules.push(Box::new(TrailingAssignReturn));
rules.push(Box::new(DeadPureCall));
rules.push(Box::new(CtorDelegationHoist));
rules.push(Box::new(TrailingElseFlatten));
rules.push(Box::new(TrailingIfContinue));
rules.push(Box::new(ConstFieldPropagate));
rules.push(Box::new(NoopPrivateCall));
rules.push(Box::new(ConstMethodInline));
rules.push(Box::new(TwrRecover));
rules.push(Box::new(StringSwitchRecover));
rules
}
pub fn all_java_rules() -> Vec<Box<dyn Rule<JavaAst>>> {
let mut rules = default_java_rules();
let mut engine_all = cure_engine::rules::all_rules();
rules.append(&mut engine_all);
rules
}
pub fn simplify(ast: &mut JavaAst, root: JavaId, cfg: &Config) -> Report {
let rules = default_java_rules();
cure_engine::simplify(ast, root, &rules, cfg)
}
pub fn simplify_unit_with_facts(
ast: &mut JavaAst,
unit: &mut CompilationUnit,
cfg: &Config,
facts: &cure_java_ast::JavaFacts,
) -> Report {
ast.set_facts(facts.clone());
let r = simplify_unit(ast, unit, cfg);
ast.clear_facts();
r
}
pub fn simplify_unit(ast: &mut JavaAst, unit: &mut CompilationUnit, cfg: &Config) -> Report {
let mut total = Report::default();
for _cycle in 0..6 {
let mut round = Report::default();
collect_unit_consts(ast, unit);
if cfg.remove_dead_methods {
collect_noop_private_methods(ast, unit);
} else {
ast.noop_private_methods.clear();
}
for ty in &mut unit.types {
simplify_type(ast, ty, cfg, &mut round);
}
if cfg.remove_dead_methods {
round.edits += remove_dead_private_fields(ast, unit);
round.edits += remove_dead_private_methods(ast, unit);
round.edits += remove_unused_imports(ast, unit);
}
total.edits += round.edits;
total.iterations += round.iterations;
total.discarded += round.discarded;
for (k, v) in round.by_rule {
*total.by_rule.entry(k).or_insert(0) += v;
}
if round.edits == 0 {
break;
}
}
total
}
fn collect_anon_and_string_names(
ast: &JavaAst,
unit: &CompilationUnit,
) -> std::collections::HashSet<String> {
let mut out: std::collections::HashSet<String> = Default::default();
let mut bodies: Vec<JavaId> = Vec::new();
for_each_type(unit, &mut |ty| collect_member_roots(ty, &mut bodies));
for root in bodies {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
match ast.data(n) {
NodeData::New { anon_raw: Some(raw), .. } => {
for id in raw.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
out.insert(id.to_string());
}
}
}
NodeData::Literal(Lit::Str(text)) => {
for id in text.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
out.insert(id.to_string());
}
}
}
_ => {}
}
for &c in ast.children(n) {
stack.push(c);
}
}
}
out
}
fn collect_all_blind_refs(
ast: &JavaAst,
unit: &CompilationUnit,
) -> std::collections::HashSet<String> {
let mut out: std::collections::HashSet<String> = Default::default();
let split_into = |text: &str, out: &mut std::collections::HashSet<String>| {
for id in text.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
out.insert(id.to_string());
}
}
};
let mut bodies: Vec<JavaId> = Vec::new();
for_each_type(unit, &mut |ty| collect_member_roots(ty, &mut bodies));
for root in bodies {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
match ast.data(n) {
NodeData::Raw { text } => split_into(text, &mut out),
NodeData::New { anon_raw: Some(raw), .. } => split_into(raw, &mut out),
NodeData::Literal(Lit::Str(text)) => split_into(text, &mut out),
NodeData::ForEach { ty, .. } => {
collect_ty_idents(ty, &mut out);
}
NodeData::NewArray { ty, .. } => {
collect_ty_idents(ty, &mut out);
}
NodeData::Lambda { params_raw } => {
split_into(params_raw, &mut out);
}
_ => {}
}
for &c in ast.children(n) {
stack.push(c);
}
}
}
for r in &unit.raws {
split_into(r, &mut out);
}
for_each_type(unit, &mut |ty: &TypeDecl| {
collect_mods_idents_all(&ty.mods, &mut out);
for e in &ty.enum_constants {
split_into(e, &mut out);
}
split_into(&ty.ty_params, &mut out);
split_into(&ty.header, &mut out);
for m in &ty.members {
match m {
Member::Method { mods, name, ty_params, params, .. }
| Member::Constructor { mods, name, ty_params, params, .. } => {
collect_mods_idents_all(mods, &mut out);
split_into(ty_params, &mut out);
for p in params {
collect_mods_idents_all(&p.mods, &mut out);
collect_ty_idents(&p.ty, &mut out);
}
if has_bare_methodsource(mods) {
out.insert(name.clone());
}
}
Member::Field { mods, declarators, .. } => {
collect_mods_idents_all(mods, &mut out);
let _ = declarators;
}
Member::Type(t) => {
let _ = t; }
Member::Raw(text) => {
split_into(text, &mut out);
}
_ => {}
}
}
});
out
}
fn collect_mods_idents_all(mods: &str, out: &mut std::collections::HashSet<String>) {
for id in mods.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
out.insert(id.to_string());
}
}
}
fn has_bare_methodsource(mods: &str) -> bool {
let hay = mods.as_bytes();
const NEEDLE: &[u8] = b"MethodSource";
let mut i = 0usize;
while let Some(p) = mods[i..].find("MethodSource") {
let at = i + p;
let end = at + NEEDLE.len();
let prev_ok = at == 0
|| !(hay[at - 1].is_ascii_alphanumeric()
|| hay[at - 1] == b'_'
|| hay[at - 1] == b'$');
let next_ident =
end < hay.len() && (hay[end].is_ascii_alphanumeric() || hay[end] == b'_' || hay[end] == b'$');
if prev_ok && !next_ident {
let mut j = end;
while j < hay.len() && hay[j].is_ascii_whitespace() {
j += 1;
}
if j >= hay.len() || hay[j] != b'(' {
return true;
}
}
i = end;
}
false
}
const REFLECTIVE_MAGIC_METHODS: &[&str] = &[
"writeObject",
"readObject",
"readObjectNoData",
"writeReplace",
"readResolve",
"validateObject",
];
fn remove_dead_private_methods(ast: &JavaAst, unit: &mut CompilationUnit) -> usize {
use std::collections::HashSet;
let mut removed = 0usize;
let anon_names = collect_anon_and_string_names(ast, unit);
let blind = collect_all_blind_refs(ast, unit);
loop {
let mut referenced: HashSet<(String, usize)> = HashSet::new();
let mut veto: HashSet<String> = HashSet::new();
let mut roots: Vec<JavaId> = Vec::new();
for_each_type(unit, &mut |ty| collect_member_roots(ty, &mut roots));
for body in roots {
collect_call_arity_refs(ast, body, &mut referenced, &mut veto);
}
for_each_type(unit, &mut |ty| {
for m in &ty.members {
if let Member::Method { name, params, .. } = m {
if params.iter().any(|p| p.varargs) {
veto.insert(name.clone());
}
}
}
});
let mut changed = false;
for_each_type_mut(unit, &mut |ty: &mut TypeDecl| {
let before = ty.members.len();
ty.members.retain(|m| {
if let Member::Method { mods, name, params, .. } = m {
let is_private = mods.split_whitespace().any(|w| w == "private");
let annotated = mods.contains('@');
let keep = !is_private
|| annotated
|| params.iter().any(|p| p.varargs)
|| veto.contains(name)
|| REFLECTIVE_MAGIC_METHODS.contains(&name.as_str())
|| anon_names.contains(name)
|| blind.contains(name)
|| referenced.contains(&(name.clone(), params.len()));
keep
} else {
true
}
});
if ty.members.len() != before {
removed += before - ty.members.len();
changed = true;
}
});
if !changed {
return removed;
}
}
}
fn strip_ta_prefix(name: &str) -> &str {
if !name.starts_with('<') {
return name;
}
let b = name.as_bytes();
let mut depth = 0i32;
for (i, &c) in b.iter().enumerate() {
match c {
b'<' => depth += 1,
b'>' => {
depth -= 1;
if depth == 0 {
return &name[i + 1..];
}
}
_ => {}
}
}
name }
fn collect_call_arity_refs(
ast: &JavaAst,
root: JavaId,
out: &mut std::collections::HashSet<(String, usize)>,
veto: &mut std::collections::HashSet<String>,
) {
let mut stack = vec![root];
while let Some(id) = stack.pop() {
match ast.data(id) {
NodeData::Call => {
let ch = ast.children(id).to_vec();
if let Some(&callee) = ch.first() {
let name = match ast.data(callee) {
NodeData::Member { name } | NodeData::VarRef { name } => {
ast.sn(*name).to_string()
}
_ => String::new(),
};
if !name.is_empty() {
out.insert((strip_ta_prefix(&name).to_string(), ch.len() - 1));
}
}
}
NodeData::MethodRef { name } => {
if let Some(short) = ast.sn(*name).rsplit("::").next() {
veto.insert(strip_ta_prefix(short).to_string());
}
}
_ => {}
}
for &c in ast.children(id) {
stack.push(c);
}
}
}
fn for_each_type(unit: &CompilationUnit, f: &mut dyn FnMut(&TypeDecl)) {
for ty in &unit.types {
for_each_type_inner(ty, f);
}
}
fn for_each_type_inner(ty: &TypeDecl, f: &mut dyn FnMut(&TypeDecl)) {
f(ty);
for m in &ty.members {
if let Member::Type(t) = m {
for_each_type_inner(t, f);
}
}
}
fn for_each_type_mut(unit: &mut CompilationUnit, f: &mut dyn FnMut(&mut TypeDecl)) {
for ty in &mut unit.types {
for_each_type_mut_inner(ty, f);
}
}
fn for_each_type_mut_inner(ty: &mut TypeDecl, f: &mut dyn FnMut(&mut TypeDecl)) {
f(ty);
for m in &mut ty.members {
if let Member::Type(t) = m {
for_each_type_mut_inner(t, f);
}
}
}
fn collect_member_roots(ty: &TypeDecl, out: &mut Vec<JavaId>) {
for m in &ty.members {
match m {
Member::Method { body: Some(b), .. }
| Member::Constructor { body: Some(b), .. } => out.push(*b),
Member::Initializer { body, .. } => out.push(*body),
Member::Field { declarators, .. } => {
for d in declarators {
if let Some(init) = d.init {
out.push(init);
}
}
}
Member::Type(t) => collect_member_roots(t, out),
_ => {}
}
}
}
fn collect_noop_private_methods(ast: &mut JavaAst, unit: &CompilationUnit) {
use std::collections::{HashMap, HashSet};
ast.noop_private_methods.clear();
let mut by_name: HashMap<String, HashMap<usize, Vec<NoopCandidate>>> = HashMap::new();
let mut veto: HashSet<String> = HashSet::new();
for_each_type(unit, &mut |ty: &TypeDecl| {
for m in &ty.members {
if let Member::Method { mods, name, params, body, throws, .. } = m {
if mods.contains('@') || params.iter().any(|p| p.varargs) {
veto.insert(name.clone());
continue;
}
let is_private = mods.split_whitespace().any(|w| w == "private");
let empty = match body {
Some(b) => ast.kind(*b) == NodeKind::Block && ast.children(*b).is_empty(),
None => false,
};
let no_throws = throws.is_empty();
let cand = NoopCandidate {
params: params.iter().map(|p| p.ty.clone()).collect(),
noop: is_private && empty && no_throws,
};
by_name
.entry(name.clone())
.or_default()
.entry(params.len())
.or_default()
.push(cand);
}
}
});
let blind = collect_all_blind_refs(ast, unit);
let mut roots: Vec<JavaId> = Vec::new();
for_each_type(unit, &mut |ty| collect_member_roots(ty, &mut roots));
for root in roots {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
if let NodeData::MethodRef { name } = ast.data(n) {
if let Some(short) = ast.sn(*name).rsplit("::").next() {
veto.insert(strip_ta_prefix(short).to_string());
}
}
for &c in ast.children(n) {
stack.push(c);
}
}
}
for (name, arities) in by_name {
if veto.contains(&name) || blind.contains(&name) {
continue;
}
ast.noop_private_methods.insert(name, arities);
}
}
fn remove_unused_imports(ast: &JavaAst, unit: &mut CompilationUnit) -> usize {
use std::collections::HashSet;
let mut idents: HashSet<String> = HashSet::new();
let mut roots: Vec<JavaId> = Vec::new();
for_each_type(unit, &mut |ty| collect_member_roots(ty, &mut roots));
for root in roots {
let mut stack = vec![root];
while let Some(n) = stack.pop() {
match ast.data(n) {
NodeData::VarDecl { name, ty, .. } => {
idents.insert(name.clone());
collect_ty_idents(ty, &mut idents);
}
NodeData::VarRef { name } => {
let n = ast.sn(*name);
for id in n.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
NodeData::Member { name } => {
idents.insert(ast.sn(*name).to_string());
}
NodeData::MethodRef { name } => {
let n = ast.sn(*name);
for id in n.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
NodeData::Label { name } => {
idents.insert(name.clone());
}
NodeData::New { ty, anon_raw, .. } => {
collect_ty_idents(ty, &mut idents);
if let Some(raw) = anon_raw {
for id in
raw.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$')
{
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
}
NodeData::Cast { ty } => {
collect_ty_idents(ty, &mut idents);
}
NodeData::InstanceOf { ty, .. } => {
collect_ty_idents(ty, &mut idents);
}
NodeData::Catch { ty_raw, .. } => {
for id in
ty_raw.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$')
{
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
NodeData::Raw { text } => {
for id in text.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
_ => {}
}
for &c in ast.children(n) {
stack.push(c);
}
}
}
for name in collect_all_blind_refs(ast, unit) {
idents.insert(name);
}
for r in &unit.raws {
for id in r.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
for_each_type(unit, &mut |ty| {
collect_mods_idents(&ty.mods, &mut idents);
idents.insert(ty.name.clone());
for id in ty.ty_params.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
for id in ty.header.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
for e in ty.extends.iter().chain(ty.implements.iter()) {
for id in e.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
for m in &ty.members {
match m {
Member::Field { mods, ty, declarators } => {
collect_mods_idents(mods, &mut idents);
collect_ty_idents(ty, &mut idents);
for d in declarators {
idents.insert(d.name.clone());
}
}
Member::Method { mods, ret, name, params, throws, .. } => {
collect_mods_idents(mods, &mut idents);
collect_ty_idents(ret, &mut idents);
idents.insert(name.clone());
for p in params {
collect_mods_idents(&p.mods, &mut idents);
collect_ty_idents(&p.ty, &mut idents);
idents.insert(p.name.clone());
}
for t in throws {
for id in t.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
}
Member::Constructor { mods, name, params, throws, .. } => {
collect_mods_idents(mods, &mut idents);
idents.insert(name.clone());
for p in params {
collect_mods_idents(&p.mods, &mut idents);
collect_ty_idents(&p.ty, &mut idents);
idents.insert(p.name.clone());
}
for t in throws {
for id in t.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
idents.insert(id.to_string());
}
}
}
}
Member::Type(t) => {
let _ = t;
}
_ => {}
}
}
});
let before = unit.imports.len();
unit.imports.retain(|imp| {
if imp.contains('*') {
return true; }
let path = imp.trim_start_matches("static ").trim();
let simple = path.rsplit('.').next().unwrap_or(path).trim();
if simple.is_empty() || !simple.chars().next().is_some_and(|c| c.is_uppercase()) {
return true; }
idents.contains(simple)
});
before - unit.imports.len()
}
fn collect_ty_idents(t: &JType, out: &mut std::collections::HashSet<String>) {
match t {
JType::Ref(r) => {
for id in r.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
out.insert(id.to_string());
}
}
}
JType::Array(inner) => collect_ty_idents(inner, out),
_ => {}
}
}
fn collect_mods_idents(mods: &str, out: &mut std::collections::HashSet<String>) {
if mods.contains('@') {
for id in mods.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
if !id.is_empty() {
out.insert(id.to_string());
}
}
}
}
fn remove_dead_private_fields(ast: &JavaAst, unit: &mut CompilationUnit) -> usize {
use std::collections::HashSet;
let mut removed = 0usize;
let anon_names = collect_anon_and_string_names(ast, unit);
let blind = collect_all_blind_refs(ast, unit);
let mut referenced: HashSet<String> = HashSet::new();
let mut bodies: Vec<JavaId> = Vec::new();
for_each_type(unit, &mut |ty: &TypeDecl| {
for m in &ty.members {
match m {
Member::Method { body: Some(b), .. }
| Member::Constructor { body: Some(b), .. }
| Member::Initializer { body: b, .. } => bodies.push(*b),
Member::Field { declarators, .. } => {
for d in declarators {
if let Some(init) = d.init {
bodies.push(init);
}
}
}
_ => {}
}
}
});
for root in bodies {
let mut stack: Vec<(JavaId, bool)> = vec![(root, false)];
while let Some((n, is_callee)) = stack.pop() {
match ast.kind(n) {
NodeKind::VarRef if !is_callee => {
if let Some(nm) = ast.var_name(n) {
referenced.insert(nm.to_string());
}
}
NodeKind::Member => {
if let NodeData::Member { name: sym } = ast.data(n) {
referenced.insert(ast.sn(*sym).to_string());
}
}
_ => {}
}
if ast.kind(n) == NodeKind::Call {
let ch = ast.children(n).to_vec();
if let Some(&first) = ch.first() {
stack.push((first, true));
}
for &c in &ch[1..] {
stack.push((c, false));
}
continue;
}
for &c in ast.children(n) {
stack.push((c, false));
}
}
}
for_each_type_mut(unit, &mut |ty: &mut TypeDecl| {
for m in ty.members.iter_mut() {
if let Member::Field { mods, declarators, .. } = m {
let is_private = mods.split_whitespace().any(|w| w == "private");
let annotated = mods.contains('@');
if !is_private || annotated {
continue;
}
let dead: Vec<bool> = declarators
.iter()
.map(|d| {
let reflective =
d.name == "serialVersionUID" || d.name == "serialPersistentFields";
let init_ok =
d.init.map(|i| is_const_init(ast, i)).unwrap_or(true);
!reflective
&& init_ok
&& !referenced.contains(&d.name)
&& !anon_names.contains(&d.name)
&& !blind.contains(&d.name)
})
.collect();
let before = declarators.len();
let mut idx = 0usize;
declarators.retain(|_| {
let keep = !dead[idx];
idx += 1;
keep
});
removed += before - declarators.len();
}
}
let before = ty.members.len();
ty.members
.retain(|m| !matches!(m, Member::Field { declarators, .. } if declarators.is_empty()));
removed += before - ty.members.len();
});
removed
}
fn simplify_type(ast: &mut JavaAst, ty: &mut TypeDecl, cfg: &Config, total: &mut Report) {
for m in &mut ty.members {
simplify_member(ast, m, cfg, total);
}
}
fn simplify_member(ast: &mut JavaAst, m: &mut Member, cfg: &Config, total: &mut Report) {
let params: Vec<(String, JType)> = match m {
Member::Method { params, .. } | Member::Constructor { params, .. } => params
.iter()
.map(|p| (p.name.clone(), p.ty.clone()))
.collect(),
_ => Vec::new(),
};
ast.set_param_scope(¶ms);
match m {
Member::Method { body: Some(b), .. }
| Member::Constructor { body: Some(b), .. } => {
let r = simplify(ast, *b, cfg);
merge_report(total, r);
}
Member::Initializer { body, .. } => {
let r = simplify(ast, *body, cfg);
merge_report(total, r);
}
Member::Field { declarators, .. } => {
for d in declarators.iter_mut() {
if let Some(init) = d.init {
let r = simplify(ast, init, cfg);
merge_report(total, r);
let rules = default_java_rules();
let mut cur: JavaId = init;
for _ in 0..8 {
match cure_engine::fold_root(ast, cur, &rules, cfg) {
Some((new_root, name)) => {
d.init = Some(new_root);
cur = new_root;
*total.by_rule.entry(name).or_insert(0) += 1;
total.edits += 1;
let r = simplify(ast, new_root, cfg);
merge_report(total, r);
}
None => break,
}
}
}
}
}
Member::Type(t) => simplify_type(ast, t, cfg, total),
Member::Raw(_) => {}
_ => {}
}
}
fn merge_report(total: &mut Report, r: Report) {
total.edits += r.edits;
total.iterations += r.iterations;
for (k, v) in r.by_rule {
*total.by_rule.entry(k).or_insert(0) += v;
}
}
pub fn default_config() -> Config {
Config::default()
}