use crate::diff::PassCtx;
use crate::diff::{ASTDiff, ASTMapping, ASTMappingOperation, ASTMappingReason};
fn after_child_indices(
after_metadata: &crate::code::ASTMetadata,
) -> rustc_hash::FxHashMap<usize, usize> {
let mut indices = rustc_hash::FxHashMap::default();
for info in after_metadata.node_info.values() {
for (index, &child) in info.children.iter().enumerate() {
indices.insert(child, index);
}
}
indices
}
fn between(
previous: usize,
next: usize,
after_metadata: &crate::code::ASTMetadata,
after_child_indices: &rustc_hash::FxHashMap<usize, usize>,
) -> Option<usize> {
let parent = after_metadata.node_to_parent.get(&previous)?;
if after_metadata.node_to_parent.get(&next)? != parent {
return None;
}
let siblings = &after_metadata.node_info.get(parent)?.children;
let previous_index = *after_child_indices.get(&previous)?;
if siblings.get(previous_index + 2) != Some(&next) {
return None;
}
siblings.get(previous_index + 1).copied()
}
pub fn solve(ctx: &PassCtx, diff: &mut ASTDiff) {
let before_metadata = ctx.before_metadata();
let after_metadata = ctx.after_metadata();
let mut parents: Vec<usize> = before_metadata
.node_info
.iter()
.filter_map(|(&id, info)| (info.children.len() >= 3).then_some(id))
.collect();
parents.sort_unstable_by_key(|id| {
before_metadata
.node_info
.get(id)
.map(|info| info.preorder_index)
.unwrap_or(usize::MAX)
});
let mut after_indices = None;
for parent in parents {
let Some(siblings) = before_metadata
.node_info
.get(&parent)
.map(|info| &info.children)
else {
continue;
};
for index in 1..siblings.len().saturating_sub(1) {
let leaf = siblings[index];
if before_metadata
.node_info
.get(&leaf)
.is_none_or(|info| !info.children.is_empty())
{
continue;
}
let partner_of = |id: usize| match diff.before_node_map.get(&id) {
Some(&partner) if partner != 0 => Some(partner),
_ => None,
};
let (Some(partner), Some(previous), Some(next)) = (
partner_of(leaf),
partner_of(siblings[index - 1]),
partner_of(siblings[index + 1]),
) else {
continue;
};
if !diff
.mapping
.get(&(leaf, partner))
.is_some_and(|entry| entry.operation == ASTMappingOperation::Identical)
{
continue;
}
let after_indices =
after_indices.get_or_insert_with(|| after_child_indices(after_metadata));
let Some(target) = between(previous, next, after_metadata, after_indices) else {
continue;
};
if target == partner {
continue;
}
if diff
.after_node_map
.get(&target)
.is_some_and(|&claimed| claimed != 0)
{
continue;
}
let (Some(leaf_info), Some(target_info)) = (
before_metadata.node_info.get(&leaf),
after_metadata.node_info.get(&target),
) else {
continue;
};
if leaf_info.kind != target_info.kind || leaf_info.text != target_info.text {
continue;
}
diff.remove_match_mapping(leaf, partner);
diff.remove_insert_mapping(target);
diff.add_mapping(
leaf,
target,
ASTMapping {
cost: 0,
operation: ASTMappingOperation::Identical,
reason: ASTMappingReason::LeafBetweenMatchedNeighbours,
},
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code::{Code, Language};
use crate::diff::{NodeCache, diff_code};
fn nth_leaf(code: &Code, text: &str, n: usize) -> usize {
let mut found = Vec::new();
let mut stack = vec![code.ast.as_ref().unwrap().root_node()];
while let Some(node) = stack.pop() {
if node.child_count() == 0 && code.contents.get(node.byte_range()) == Some(text) {
found.push((node.start_byte(), node.id()));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push(child);
}
}
found.sort_unstable();
found[n].1
}
#[test]
fn an_operator_chain_that_grew_keeps_each_existing_token_where_it_was() {
let before = Code::from_string(
"class C { boolean f() { return a == 1 || a == 2 || a == 3; } }",
&Language::Java,
);
let after = Code::from_string(
"class C { boolean f() { return a == 1 || a == 2 || a == 3 || a == 4; } }",
&Language::Java,
);
let diff = diff_code(&before, &after);
let ast = diff.ast.as_ref().unwrap();
for index in 0..2 {
assert_eq!(
ast.before_node_map.get(&nth_leaf(&before, "||", index)),
Some(&nth_leaf(&after, "||", index)),
"the `||` at position {index} is the same `||` on both sides",
);
}
assert_eq!(ast.after_node_map.get(&nth_leaf(&after, "||", 2)), Some(&0),);
}
#[test]
fn an_invocation_chain_that_grew_keeps_its_existing_dot() {
let before = Code::from_string("class C { void f() { x.f(); } }", &Language::Java);
let after = Code::from_string("class C { void f() { x.f().g(); } }", &Language::Java);
let diff = diff_code(&before, &after);
let ast = diff.ast.as_ref().unwrap();
assert_eq!(
ast.before_node_map.get(&nth_leaf(&before, ".", 0)),
Some(&nth_leaf(&after, ".", 0)),
);
assert_eq!(ast.after_node_map.get(&nth_leaf(&after, ".", 1)), Some(&0));
}
fn solve_over(before: &Code, after: &Code, pairs: &[(usize, usize)]) -> ASTDiff {
let node_cache = NodeCache::build(before, after);
let mut diff = ASTDiff::default();
for &(before_id, after_id) in pairs {
diff.add_mapping(
before_id,
after_id,
ASTMapping::identical(ASTMappingReason::IdenticalHash),
);
}
solve(
&crate::diff::PassCtx::new(before, after, &node_cache),
&mut diff,
);
diff
}
#[test]
fn a_comma_is_not_re_pointed_when_an_argument_was_inserted_between_its_neighbours() {
let before = Code::from_string("class C { void f() { g(a, b); } }", &Language::Java);
let after = Code::from_string("class C { void f() { g(a, x, b); } }", &Language::Java);
let comma = nth_leaf(&before, ",", 0);
let diff = solve_over(
&before,
&after,
&[
(nth_leaf(&before, "a", 0), nth_leaf(&after, "a", 0)),
(comma, nth_leaf(&after, ",", 0)),
(nth_leaf(&before, "b", 0), nth_leaf(&after, "b", 0)),
],
);
assert_eq!(
diff.before_node_map.get(&comma),
Some(&nth_leaf(&after, ",", 0)),
"the first `,` is where it always was",
);
}
#[test]
fn a_leaf_with_only_one_matched_neighbour_is_left_alone() {
let before = Code::from_string("class C { void f() { g(a, b); } }", &Language::Java);
let after = Code::from_string("class C { void f() { g(a, c); } }", &Language::Java);
let comma = nth_leaf(&before, ",", 0);
let diff = solve_over(
&before,
&after,
&[(nth_leaf(&before, "a", 0), nth_leaf(&after, "a", 0))],
);
assert_eq!(diff.before_node_map.get(&comma), None);
}
#[test]
fn a_claimed_target_is_never_taken() {
let before = Code::from_string(
"class C { boolean f() { return a == 1 || a == 2 || a == 3; } }",
&Language::Java,
);
let after = Code::from_string(
"class C { boolean f() { return a == 1 || a == 2 || a == 3 || a == 4; } }",
&Language::Java,
);
let outer_before = nth_leaf(&before, "||", 1);
let outer_after = nth_leaf(&after, "||", 2);
let claimed = nth_leaf(&after, "||", 1);
let diff = solve_over(
&before,
&after,
&[
(outer_before, outer_after),
(nth_leaf(&before, "||", 0), claimed),
],
);
assert_eq!(diff.before_node_map.get(&outer_before), Some(&outer_after));
}
fn nth_node(code: &Code, kind: &str, text: &str, n: usize) -> usize {
let mut found = Vec::new();
let mut stack = vec![code.ast.as_ref().unwrap().root_node()];
while let Some(node) = stack.pop() {
if node.kind() == kind && code.contents.get(node.byte_range()) == Some(text) {
found.push((node.start_byte(), node.id()));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push(child);
}
}
found.sort_unstable();
found[n].1
}
fn grown_chain_paired_by_depth(operation: ASTMappingOperation) -> (ASTDiff, usize, usize) {
let before = Code::from_string(
"class C { boolean f() { return a == 1 || a == 2 || a == 3; } }",
&Language::Java,
);
let after = Code::from_string(
"class C { boolean f() { return a == 1 || a == 2 || a == 3 || a == 4; } }",
&Language::Java,
);
let node_cache = NodeCache::build(&before, &after);
let mut diff = ASTDiff::default();
let inner = "a == 1 || a == 2";
diff.add_mapping(
nth_node(&before, "binary_expression", inner, 0),
nth_node(&after, "binary_expression", inner, 0),
ASTMapping::identical(ASTMappingReason::IdenticalHash),
);
diff.add_mapping(
nth_node(&before, "binary_expression", "a == 3", 0),
nth_node(&after, "binary_expression", "a == 3", 0),
ASTMapping::identical(ASTMappingReason::IdenticalHash),
);
let outer_before = nth_leaf(&before, "||", 1);
diff.add_mapping(
outer_before,
nth_leaf(&after, "||", 2),
ASTMapping {
cost: 0,
operation,
reason: ASTMappingReason::IdenticalHash,
},
);
solve(
&crate::diff::PassCtx::new(&before, &after, &node_cache),
&mut diff,
);
(diff, outer_before, nth_leaf(&after, "||", 1))
}
#[test]
fn an_identical_leaf_between_matched_neighbours_is_re_pointed_to_their_middle() {
let (diff, leaf, between) = grown_chain_paired_by_depth(ASTMappingOperation::Identical);
assert_eq!(diff.before_node_map.get(&leaf), Some(&between));
}
#[test]
fn a_non_identical_leaf_pairing_is_never_re_pointed() {
let (diff, leaf, between) =
grown_chain_paired_by_depth(ASTMappingOperation::MatchButNotIdentical);
assert_ne!(diff.before_node_map.get(&leaf), Some(&between));
}
}