use super::*;
pub(crate) const FORBIDDEN_RENAME_COST: u64 = COST_DELETE + COST_INSERT + 1;
pub(crate) fn before_match_target(
id: usize,
before_decision: &HashMap<usize, BeforeDecision>,
diff: &ASTDiff,
) -> Option<usize> {
match_target(id, before_decision, &diff.before_node_map)
}
pub(crate) fn after_match_target(
id: usize,
after_decision: &HashMap<usize, AfterDecision>,
diff: &ASTDiff,
) -> Option<usize> {
match_target(id, after_decision, &diff.after_node_map)
}
fn match_target<D: SideDecision>(
id: usize,
decisions: &HashMap<usize, D>,
node_map: &rustc_hash::FxHashMap<usize, usize>,
) -> Option<usize> {
match decisions.get(&id) {
Some(decision) => decision.match_target(),
None => node_map.get(&id).copied().filter(|&t| t != 0),
}
}
pub(crate) fn ancestor_child_of(
node: usize,
ancestor: usize,
parents: &rustc_hash::FxHashMap<usize, usize>,
) -> Option<usize> {
let mut cur = node;
while let Some(&p) = parents.get(&cur) {
if p == ancestor {
return Some(cur);
}
cur = p;
}
None
}
pub(crate) enum SubtreeTargetOutcome {
MatchAndRecurse(usize),
PruneRecurse,
Leaf(Option<usize>),
}
pub(crate) fn collect_subtree_targets(
root: usize,
meta: &ASTMetadata,
out: &mut Vec<usize>,
classify: &impl Fn(usize) -> SubtreeTargetOutcome,
) {
let Some(info) = meta.node_info.get(&root) else {
return;
};
for &child in &info.children {
match classify(child) {
SubtreeTargetOutcome::MatchAndRecurse(t) => {
out.push(t);
collect_subtree_targets(child, meta, out, classify);
}
SubtreeTargetOutcome::PruneRecurse => {
collect_subtree_targets(child, meta, out, classify);
}
SubtreeTargetOutcome::Leaf(target) => {
if let Some(t) = target {
out.push(t);
}
}
}
}
}
pub(crate) fn collect_before_subtree_targets(
root: usize,
before_meta: &ASTMetadata,
before_decision: &HashMap<usize, BeforeDecision>,
diff: &ASTDiff,
out: &mut Vec<usize>,
) {
collect_side_subtree_targets(
root,
before_meta,
before_decision,
&diff.before_node_map,
out,
);
}
pub(crate) fn collect_after_subtree_targets(
root: usize,
after_meta: &ASTMetadata,
after_decision: &HashMap<usize, AfterDecision>,
diff: &ASTDiff,
out: &mut Vec<usize>,
) {
collect_side_subtree_targets(root, after_meta, after_decision, &diff.after_node_map, out);
}
fn collect_side_subtree_targets<D: SideDecision>(
root: usize,
meta: &ASTMetadata,
decisions: &HashMap<usize, D>,
node_map: &rustc_hash::FxHashMap<usize, usize>,
out: &mut Vec<usize>,
) {
collect_subtree_targets(root, meta, out, &|child| match decisions.get(&child) {
Some(decision) => match decision.match_target() {
Some(t) => SubtreeTargetOutcome::MatchAndRecurse(t),
None => SubtreeTargetOutcome::PruneRecurse,
},
None => SubtreeTargetOutcome::Leaf(node_map.get(&child).copied().filter(|&t| t != 0)),
});
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn improve_slot_alignment(
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
diff: &ASTDiff,
before_root_ids: &[usize],
before_parents: &rustc_hash::FxHashMap<usize, usize>,
after_parents: &rustc_hash::FxHashMap<usize, usize>,
before_decision: &mut HashMap<usize, BeforeDecision>,
after_decision: &mut HashMap<usize, AfterDecision>,
) {
let before_forest_roots: std::collections::HashSet<usize> =
before_root_ids.iter().copied().collect();
let ctx = SlotCtx {
before_meta,
after_meta,
diff,
before_parents,
after_parents,
before_forest_roots: &before_forest_roots,
};
validate_fresh_matches(&ctx, before_decision, after_decision);
pull_up_wrapped_matches(
before_meta,
after_meta,
diff,
before_parents,
after_parents,
before_decision,
after_decision,
);
validate_fresh_matches(&ctx, before_decision, after_decision);
reclaim_slot_level_twins(
before_meta,
after_meta,
diff,
before_parents,
after_parents,
before_decision,
after_decision,
);
promote_same_slot_pairs(
before_meta,
after_meta,
diff,
before_parents,
after_parents,
before_decision,
after_decision,
);
}
pub(crate) fn subtree_has_content(root: usize, meta: &ASTMetadata) -> bool {
let Some(info) = meta.node_info.get(&root) else {
return false;
};
if info.children.is_empty() {
return !nodes::is_generic_token_kind(&info.kind);
}
info.children
.iter()
.any(|&child| subtree_has_content(child, meta))
}
pub(crate) fn validate_fresh_matches(
ctx: &SlotCtx,
before_decision: &mut HashMap<usize, BeforeDecision>,
after_decision: &mut HashMap<usize, AfterDecision>,
) {
let depth_of = |mut node: usize| -> usize {
let mut depth = 0;
while let Some(&p) = ctx.before_parents.get(&node) {
depth += 1;
node = p;
}
depth
};
let mut pairs: Vec<(usize, usize, usize, usize, usize, usize, usize)> = before_decision
.iter()
.filter_map(|(&b, d)| match d {
BeforeDecision::Match(a) => {
let before_info = ctx.before_meta.node_info.get(&b)?;
let after_info = ctx.after_meta.node_info.get(a)?;
Some((
depth_of(b),
before_info.start_byte,
after_info.start_byte,
before_info.preorder_index,
after_info.preorder_index,
b,
*a,
))
}
BeforeDecision::Delete => None,
})
.collect();
pairs.sort_unstable();
for (_, _, _, _, _, b, a) in pairs {
if before_decision.get(&b) != Some(&BeforeDecision::Match(a)) {
continue;
}
if ctx.before_forest_roots.contains(&b) {
continue;
}
let (Some(b_info), Some(a_info)) = (
ctx.before_meta.node_info.get(&b),
ctx.after_meta.node_info.get(&a),
) else {
continue;
};
let keep = if b_info.children.is_empty() && a_info.children.is_empty() {
leaf_match_supported(b, a, b_info, a_info, ctx, before_decision)
} else {
island_match_supported(b, a, ctx, before_decision, after_decision)
};
if !keep {
before_decision.insert(b, BeforeDecision::Delete);
after_decision.insert(a, AfterDecision::Insert);
}
}
}
pub(crate) fn island_match_supported(
b: usize,
a: usize,
ctx: &SlotCtx,
before_decision: &HashMap<usize, BeforeDecision>,
after_decision: &HashMap<usize, AfterDecision>,
) -> bool {
let (Some(&pb), Some(&pa)) = (ctx.before_parents.get(&b), ctx.after_parents.get(&a)) else {
return true;
};
if before_match_target(pb, before_decision, ctx.diff).is_some()
|| after_match_target(pa, after_decision, ctx.diff).is_some()
{
return true;
}
if has_nearby_matched_ancestor(b, MAX_CONTEXT_ANCESTOR_DEPTH, ctx, before_decision) {
return true;
}
let hashes_match = ctx
.before_meta
.node_to_full_hash
.get(&b)
.zip(ctx.after_meta.node_to_full_hash.get(&a))
.is_some_and(|(bh, ah)| bh == ah);
hashes_match && subtree_has_content(b, ctx.before_meta)
}
pub(crate) fn leaf_match_supported(
b: usize,
a: usize,
b_info: &ASTNodeMetadata,
a_info: &ASTNodeMetadata,
ctx: &SlotCtx,
before_decision: &HashMap<usize, BeforeDecision>,
) -> bool {
if !nodes::matching_allowed(
&b_info.kind,
&a_info.kind,
&ctx.before_meta.language,
|| update_context_supported(b, a, ctx, before_decision),
) {
return false;
}
if nodes::is_generic_token_kind(&b_info.kind) || b_info.kind != a_info.kind {
return true;
}
if b_info.text == a_info.text {
return has_nearby_matched_ancestor(
b,
MAX_UPDATE_CONTEXT_ANCESTOR_DEPTH,
ctx,
before_decision,
);
}
update_context_supported(b, a, ctx, before_decision)
|| nodes::leaf_texts_similar(&b_info.text, &a_info.text)
}
pub(crate) fn pull_up_wrapped_matches(
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
diff: &ASTDiff,
before_parents: &rustc_hash::FxHashMap<usize, usize>,
after_parents: &rustc_hash::FxHashMap<usize, usize>,
before_decision: &mut HashMap<usize, BeforeDecision>,
after_decision: &mut HashMap<usize, AfterDecision>,
) {
let mut pairs: Vec<(usize, usize, usize, usize, usize, usize)> = before_decision
.iter()
.filter_map(|(&b, d)| match d {
BeforeDecision::Match(a) => {
let before_info = before_meta.node_info.get(&b)?;
let after_info = after_meta.node_info.get(a)?;
Some((
before_info.start_byte,
after_info.start_byte,
before_info.preorder_index,
after_info.preorder_index,
b,
*a,
))
}
BeforeDecision::Delete => None,
})
.collect();
pairs.sort_unstable();
for (_, _, _, _, b, a) in pairs {
if before_decision.get(&b) != Some(&BeforeDecision::Match(a)) {
continue;
}
if let Some(&pb) = before_parents.get(&b)
&& let Some(pa_target) = before_match_target(pb, before_decision, diff)
&& after_parents.get(&a) != Some(&pa_target)
&& let Some(c) = ancestor_child_of(a, pa_target, after_parents)
&& c != a
&& after_meta.node_info.get(&c).map(|i| i.kind.as_str())
== before_meta.node_info.get(&b).map(|i| i.kind.as_str())
&& after_decision.get(&c) == Some(&AfterDecision::Insert)
{
let mut targets = Vec::new();
collect_after_subtree_targets(c, after_meta, after_decision, diff, &mut targets);
if targets
.iter()
.all(|&t| is_ancestor_or_self(b, t, before_parents))
{
after_decision.insert(a, AfterDecision::Insert);
before_decision.insert(b, BeforeDecision::Match(c));
after_decision.insert(c, AfterDecision::Match(b));
continue;
}
}
if let Some(&pa) = after_parents.get(&a)
&& let Some(pb_target) = after_match_target(pa, after_decision, diff)
&& before_parents.get(&b) != Some(&pb_target)
&& let Some(c) = ancestor_child_of(b, pb_target, before_parents)
&& c != b
&& before_meta.node_info.get(&c).map(|i| i.kind.as_str())
== after_meta.node_info.get(&a).map(|i| i.kind.as_str())
&& before_decision.get(&c) == Some(&BeforeDecision::Delete)
{
let mut targets = Vec::new();
collect_before_subtree_targets(c, before_meta, before_decision, diff, &mut targets);
if targets
.iter()
.all(|&t| is_ancestor_or_self(a, t, after_parents))
{
before_decision.insert(b, BeforeDecision::Delete);
before_decision.insert(c, BeforeDecision::Match(a));
after_decision.insert(a, AfterDecision::Match(c));
}
}
}
}
fn slot_level_twin<D: SideDecision>(
slot_parent: usize,
node: &crate::code::ASTNodeMetadata,
meta: &ASTMetadata,
decisions: &HashMap<usize, D>,
) -> Option<usize> {
let complements = nodes::delimiter_complement_kinds(&node.kind)?;
let parent_info = meta.node_info.get(&slot_parent)?;
let holds_complement = parent_info.children.iter().any(|child| {
meta.node_info
.get(child)
.is_some_and(|info| complements.contains(&info.kind.as_str()))
});
if !holds_complement {
return None;
}
parent_info
.children
.iter()
.filter(|&&child| {
decisions
.get(&child)
.is_some_and(|decision| decision.match_target().is_none())
})
.filter_map(|&child| meta.node_info.get(&child).map(|info| (child, info)))
.filter(|(_, info)| {
info.children.is_empty() && info.kind == node.kind && info.text == node.text
})
.min_by_key(|(_, info)| (info.start_byte, info.preorder_index))
.map(|(child, _)| child)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn reclaim_slot_level_twins(
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
diff: &ASTDiff,
before_parents: &rustc_hash::FxHashMap<usize, usize>,
after_parents: &rustc_hash::FxHashMap<usize, usize>,
before_decision: &mut HashMap<usize, BeforeDecision>,
after_decision: &mut HashMap<usize, AfterDecision>,
) {
let mut pairs: Vec<(usize, usize, usize, usize, usize, usize)> = before_decision
.iter()
.filter_map(|(&b, decision)| match decision {
BeforeDecision::Match(a) => {
let before_info = before_meta.node_info.get(&b)?;
let after_info = after_meta.node_info.get(a)?;
Some((
before_info.start_byte,
after_info.start_byte,
before_info.preorder_index,
after_info.preorder_index,
b,
*a,
))
}
BeforeDecision::Delete => None,
})
.collect();
pairs.sort_unstable();
for (_, _, _, _, b, a) in pairs {
if before_decision.get(&b) != Some(&BeforeDecision::Match(a)) {
continue;
}
if let Some(&pa) = after_parents.get(&a)
&& let Some(pb_target) = after_match_target(pa, after_decision, diff)
&& before_parents.get(&b) != Some(&pb_target)
&& let Some(before_info) = before_meta.node_info.get(&b)
&& before_info.children.is_empty()
&& let Some(c) = slot_level_twin(pb_target, before_info, before_meta, before_decision)
{
before_decision.insert(b, BeforeDecision::Delete);
before_decision.insert(c, BeforeDecision::Match(a));
after_decision.insert(a, AfterDecision::Match(c));
continue;
}
if let Some(&pb) = before_parents.get(&b)
&& let Some(pa_target) = before_match_target(pb, before_decision, diff)
&& after_parents.get(&a) != Some(&pa_target)
&& let Some(after_info) = after_meta.node_info.get(&a)
&& after_info.children.is_empty()
&& let Some(c) = slot_level_twin(pa_target, after_info, after_meta, after_decision)
{
after_decision.insert(a, AfterDecision::Insert);
after_decision.insert(c, AfterDecision::Match(b));
before_decision.insert(b, BeforeDecision::Match(c));
}
}
}
pub(crate) const LARGE_SLOT_SUBTREE: usize = 20;
#[allow(clippy::too_many_arguments)]
pub(crate) fn slot_promotion_allowed(
b: usize,
a: usize,
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
diff: &ASTDiff,
before_parents: &rustc_hash::FxHashMap<usize, usize>,
after_parents: &rustc_hash::FxHashMap<usize, usize>,
before_decision: &HashMap<usize, BeforeDecision>,
after_decision: &HashMap<usize, AfterDecision>,
) -> bool {
let mut b_targets = Vec::new();
collect_before_subtree_targets(b, before_meta, before_decision, diff, &mut b_targets);
if !b_targets
.iter()
.all(|&t| is_ancestor_or_self(a, t, after_parents))
{
return false;
}
let mut a_targets = Vec::new();
collect_after_subtree_targets(a, after_meta, after_decision, diff, &mut a_targets);
if !a_targets
.iter()
.all(|&t| is_ancestor_or_self(b, t, before_parents))
{
return false;
}
if b_targets.is_empty() && a_targets.is_empty() {
let size_b = before_meta
.node_to_subtree_size
.get(&b)
.copied()
.unwrap_or(1);
let size_a = after_meta
.node_to_subtree_size
.get(&a)
.copied()
.unwrap_or(1);
if size_b > LARGE_SLOT_SUBTREE
&& size_a > LARGE_SLOT_SUBTREE
&& !share_descendant_hash(b, a, before_meta, after_meta)
{
return false;
}
}
true
}
pub(crate) fn share_descendant_hash(
b: usize,
a: usize,
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
) -> bool {
fn collect_hashes(root: usize, meta: &ASTMetadata, out: &mut std::collections::HashSet<u64>) {
let Some(info) = meta.node_info.get(&root) else {
return;
};
for &child in &info.children {
if let Some(&h) = meta.node_to_full_hash.get(&child) {
out.insert(h);
}
collect_hashes(child, meta, out);
}
}
let mut before_hashes = std::collections::HashSet::new();
collect_hashes(b, before_meta, &mut before_hashes);
fn any_shared(
root: usize,
meta: &ASTMetadata,
before_hashes: &std::collections::HashSet<u64>,
) -> bool {
let Some(info) = meta.node_info.get(&root) else {
return false;
};
info.children.iter().any(|&child| {
meta.node_to_full_hash
.get(&child)
.is_some_and(|h| before_hashes.contains(h))
|| any_shared(child, meta, before_hashes)
})
}
any_shared(a, after_meta, &before_hashes)
}
pub(crate) fn weighted_lcs_pairs(
n: usize,
m: usize,
weight: impl Fn(usize, usize) -> u64,
) -> Vec<(usize, usize)> {
let mut dp = vec![vec![0u64; m + 1]; n + 1];
for i in (0..n).rev() {
for j in (0..m).rev() {
let mut best = dp[i + 1][j].max(dp[i][j + 1]);
let w = weight(i, j);
if w > 0 {
best = best.max(dp[i + 1][j + 1] + w);
}
dp[i][j] = best;
}
}
let mut pairs = Vec::new();
let (mut i, mut j) = (0, 0);
while i < n && j < m {
let w = weight(i, j);
if w > 0 && dp[i][j] == dp[i + 1][j + 1] + w {
pairs.push((i, j));
i += 1;
j += 1;
} else if dp[i + 1][j] >= dp[i][j + 1] {
i += 1;
} else {
j += 1;
}
}
pairs
}
pub(crate) const SLOT_LCS_ANCHOR_WEIGHT: u64 = 10_000;
pub(crate) fn promote_same_slot_pairs(
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
diff: &ASTDiff,
before_parents: &rustc_hash::FxHashMap<usize, usize>,
after_parents: &rustc_hash::FxHashMap<usize, usize>,
before_decision: &mut HashMap<usize, BeforeDecision>,
after_decision: &mut HashMap<usize, AfterDecision>,
) {
use std::collections::HashSet;
let mut queue: Vec<(usize, usize)> = Vec::new();
for (&b, d) in before_decision.iter() {
if let BeforeDecision::Match(a) = d {
queue.push((b, *a));
}
}
queue.sort_unstable_by_key(|&(b, a)| {
let before_info = before_meta.node_info.get(&b);
let after_info = after_meta.node_info.get(&a);
(
before_info.map(|i| i.start_byte).unwrap_or(usize::MAX),
after_info.map(|i| i.start_byte).unwrap_or(usize::MAX),
before_info.map(|i| i.preorder_index).unwrap_or(usize::MAX),
after_info.map(|i| i.preorder_index).unwrap_or(usize::MAX),
)
});
queue.dedup();
let mut seen: HashSet<(usize, usize)> = HashSet::new();
while let Some((pb, pa)) = queue.pop() {
if !seen.insert((pb, pa)) {
continue;
}
let (Some(b_info), Some(a_info)) = (
before_meta.node_info.get(&pb),
after_meta.node_info.get(&pa),
) else {
continue;
};
let b_children = b_info.children.clone();
let a_children = a_info.children.clone();
let promoted = {
let weight = |i: usize, j: usize| -> u64 {
let (b, a) = (b_children[i], a_children[j]);
let b_target = before_match_target(b, before_decision, diff);
if let Some(t) = b_target {
return if t == a { SLOT_LCS_ANCHOR_WEIGHT } else { 0 };
}
if after_match_target(a, after_decision, diff).is_some() {
return 0;
}
let deletable = before_decision.get(&b) == Some(&BeforeDecision::Delete);
let insertable = after_decision.get(&a) == Some(&AfterDecision::Insert);
if !deletable || !insertable {
return 0;
}
let (Some(b_info), Some(a_info)) =
(before_meta.node_info.get(&b), after_meta.node_info.get(&a))
else {
return 0;
};
if b_info.kind != a_info.kind {
return 0;
}
if b_info.children.is_empty()
&& a_info.children.is_empty()
&& b_info.text != a_info.text
&& !nodes::leaf_texts_similar(&b_info.text, &a_info.text)
{
return 0;
}
1
};
weighted_lcs_pairs(b_children.len(), a_children.len(), weight)
};
for (i, j) in promoted {
let (b, a) = (b_children[i], a_children[j]);
if before_decision.get(&b) != Some(&BeforeDecision::Delete)
|| after_decision.get(&a) != Some(&AfterDecision::Insert)
{
continue;
}
if !slot_promotion_allowed(
b,
a,
before_meta,
after_meta,
diff,
before_parents,
after_parents,
before_decision,
after_decision,
) {
continue;
}
before_decision.insert(b, BeforeDecision::Match(a));
after_decision.insert(a, AfterDecision::Match(b));
queue.push((b, a));
}
repair_leaf_slots(
&b_children,
&a_children,
before_meta,
after_meta,
before_decision,
after_decision,
);
}
}
pub(crate) fn repair_leaf_slots(
b_children: &[usize],
a_children: &[usize],
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
before_decision: &mut HashMap<usize, BeforeDecision>,
after_decision: &mut HashMap<usize, AfterDecision>,
) {
for &x in b_children {
let Some(x_info) = before_meta.node_info.get(&x) else {
continue;
};
if !x_info.children.is_empty() {
continue;
}
let Some(&BeforeDecision::Match(t)) = before_decision.get(&x) else {
continue;
};
if a_children.contains(&t) {
continue;
}
let mut candidates = a_children.iter().copied().filter(|&y| {
after_decision.get(&y) == Some(&AfterDecision::Insert)
&& after_meta.node_info.get(&y).is_some_and(|y_info| {
y_info.children.is_empty()
&& y_info.kind == x_info.kind
&& y_info.text == x_info.text
})
});
let (Some(y), None) = (candidates.next(), candidates.next()) else {
continue;
};
after_decision.insert(t, AfterDecision::Insert);
before_decision.insert(x, BeforeDecision::Match(y));
after_decision.insert(y, AfterDecision::Match(x));
}
}
pub(crate) fn is_ancestor_or_self(
ancestor: usize,
mut node: usize,
parents: &rustc_hash::FxHashMap<usize, usize>,
) -> bool {
loop {
if node == ancestor {
return true;
}
match parents.get(&node) {
Some(&parent) => node = parent,
None => return false,
}
}
}
pub(crate) fn compute_pruned_targets(
root_ids: &[usize],
meta: &ASTMetadata,
node_map: &rustc_hash::FxHashMap<usize, usize>,
) -> rustc_hash::FxHashMap<usize, Vec<usize>> {
fn visit(
node_id: usize,
meta: &ASTMetadata,
node_map: &rustc_hash::FxHashMap<usize, usize>,
memo: &mut rustc_hash::FxHashMap<usize, Vec<usize>>,
) -> Vec<usize> {
if let Some(cached) = memo.get(&node_id) {
return cached.clone();
}
let result = if let Some(&target) = node_map.get(&node_id) {
if target == 0 {
Vec::new()
} else {
vec![target]
}
} else if let Some(info) = meta.node_info.get(&node_id) {
info.children
.iter()
.flat_map(|&child| visit(child, meta, node_map, memo))
.collect()
} else {
Vec::new()
};
memo.insert(node_id, result.clone());
result
}
let mut memo = rustc_hash::FxHashMap::default();
for &root_id in root_ids {
visit(root_id, meta, node_map, &mut memo);
}
memo.retain(|_, targets| !targets.is_empty());
memo
}
pub(crate) fn collect_pruned_chunk_pairs(
before_root_ids: &[usize],
after_root_ids: &[usize],
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
diff: &ASTDiff,
) -> Vec<(usize, usize)> {
fn visit(
node_id: usize,
meta: &ASTMetadata,
node_map: &rustc_hash::FxHashMap<usize, usize>,
out: &mut Vec<usize>,
) {
if node_map.contains_key(&node_id) {
out.push(node_id);
return;
}
if let Some(info) = meta.node_info.get(&node_id) {
for &child_id in &info.children {
visit(child_id, meta, node_map, out);
}
}
}
let mut pairs: rustc_hash::FxHashMap<usize, usize> = rustc_hash::FxHashMap::default();
let mut before_roots = Vec::new();
for &root_id in before_root_ids {
visit(
root_id,
before_meta,
&diff.before_node_map,
&mut before_roots,
);
}
for id in before_roots {
if let Some(&after_id) = diff.before_node_map.get(&id) {
pairs.insert(id, after_id);
}
}
let mut after_roots = Vec::new();
for &root_id in after_root_ids {
visit(root_id, after_meta, &diff.after_node_map, &mut after_roots);
}
for id in after_roots {
if let Some(&before_id) = diff.after_node_map.get(&id) {
pairs.entry(before_id).or_insert(id);
}
}
pairs.into_iter().collect()
}
pub(crate) fn longest_increasing_by_second(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
if pairs.is_empty() {
return Vec::new();
}
let mut tails: Vec<usize> = Vec::new();
let mut parent: Vec<Option<usize>> = vec![None; pairs.len()];
for i in 0..pairs.len() {
let val = pairs[i].1;
let pos = tails.partition_point(|&t| pairs[t].1 < val);
if pos > 0 {
parent[i] = Some(tails[pos - 1]);
}
if pos == tails.len() {
tails.push(i);
} else {
tails[pos] = i;
}
}
let mut result = Vec::with_capacity(tails.len());
let mut cur = tails.last().copied();
while let Some(i) = cur {
result.push(pairs[i]);
cur = parent[i];
}
result.reverse();
result
}
#[cfg(test)]
mod reclaim_tests {
use crate::code::{Code, Language};
fn leaf_targets(
before_src: &str,
after_src: &str,
language: &Language,
text: &str,
) -> Vec<Option<usize>> {
let before = Code::from_string(before_src, language);
let after = Code::from_string(after_src, language);
let diff = crate::diff::diff_code(&before, &after);
let ast = diff.ast.as_ref().expect("an AST diff");
let root = before
.ast
.as_ref()
.expect("a parsed before tree")
.root_node();
let mut leaves = Vec::new();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if node.child_count() == 0 && &before_src[node.byte_range()] == text {
leaves.push(node);
}
for index in 0..node.child_count() {
stack.push(node.child(index).expect("child in range"));
}
}
leaves.sort_by_key(|node| node.start_byte());
leaves
.into_iter()
.map(|node| {
ast.before_node_map
.get(&node.id())
.copied()
.filter(|&target| target != 0)
})
.collect()
}
#[test]
fn a_surviving_call_keeps_its_own_closing_paren_when_a_nested_call_is_removed() {
let targets = leaf_targets(
"fn m() {\n self.f(&mut w, common.prim_rect.size());\n}\n",
"fn m() {\n self.f(&mut w, common.prim_size);\n}\n",
&Language::Rust,
")",
);
assert_eq!(targets.len(), 3, "expected three `)` in the before tree");
assert!(targets[0].is_some(), "the signature's `)` is untouched");
assert_eq!(
targets[1], None,
"the `)` of the removed `.size()` call must go with it"
);
assert!(
targets[2].is_some(),
"the surviving call's own `)` must keep the pairing"
);
}
#[test]
fn a_separator_is_left_where_the_dp_put_it() {
let targets = leaf_targets(
"class C {\n int x = Build.VERSION_CODES.R;\n}\n",
"class C {\n int x = AndroidVersions.API_30;\n}\n",
&Language::Java,
".",
);
assert_eq!(targets.len(), 2, "expected two `.` in the before tree");
assert!(
targets[0].is_some() && targets[1].is_none(),
"the first `.` should keep the pairing and the second should go, got {targets:?}"
);
}
}