use crate::diff::PassCtx;
use crate::diff::nodes::{is_comment, is_leading_modifier, map_identical_descendants};
use crate::diff::{ASTDiff, ASTMapping, ASTMappingReason};
pub fn solve(ctx: &PassCtx, diff: &mut ASTDiff) {
let (before, after, node_cache) = (ctx.before, ctx.after, ctx.node_cache);
let before_src = before.contents.as_bytes();
let after_src = after.contents.as_bytes();
let language = ctx.language();
let current_mappings: Vec<(usize, usize)> =
diff.before_node_map.iter().map(|(&k, &v)| (k, v)).collect();
let candidate_anchors: rustc_hash::FxHashSet<usize> = node_cache
.before
.values()
.filter(|node| {
(is_comment(node.kind()) || is_leading_modifier(node.kind(), &language))
&& !diff.before_node_map.contains_key(&node.id())
})
.filter_map(|node| node.next_sibling().map(|next| next.id()))
.collect();
for (before_id, after_id) in current_mappings {
if before_id == 0 || after_id == 0 {
continue;
}
if !candidate_anchors.contains(&before_id) {
continue;
}
let Some(&before_node) = node_cache.before.get(&before_id) else {
continue;
};
let Some(&after_node) = node_cache.after.get(&after_id) else {
continue;
};
let mut before_anchor = before_node;
let mut after_anchor = after_node;
while let (Some(before_prev), Some(after_prev)) =
(before_anchor.prev_sibling(), after_anchor.prev_sibling())
{
if diff.before_node_map.contains_key(&before_prev.id())
|| diff.after_node_map.contains_key(&after_prev.id())
{
break;
}
if before_prev.kind() != after_prev.kind() {
break;
}
if !(is_comment(before_prev.kind())
|| is_leading_modifier(before_prev.kind(), &language))
{
break;
}
let before_text = before_prev.utf8_text(before_src).unwrap_or("");
let after_text = after_prev.utf8_text(after_src).unwrap_or("");
if before_text != after_text {
break;
}
diff.add_mapping(
before_prev.id(),
after_prev.id(),
ASTMapping::identical(ASTMappingReason::LeadingSibling),
);
map_identical_descendants(before_prev, after_prev, diff);
before_anchor = before_prev;
after_anchor = after_prev;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code::{Code, Language};
use crate::diff::ASTMappingOperation;
use crate::test::helper::find_first_of_kind;
#[test]
fn test_is_comment_function() {
assert!(is_comment("comment"));
assert!(is_comment("line_comment"));
assert!(is_comment("block_comment"));
assert!(is_comment("js_comment"));
assert!(!is_comment("function_item"));
assert!(!is_comment("identifier"));
assert!(!is_comment("string"));
}
#[test]
fn matches_leading_comment_of_an_unchanged_sibling_function() {
let before = Code::from_string(
"// This is a comment\nfn hello() {\n println!(\"Hello\");\n}\nfn other() {\n 1;\n}\n",
&Language::Rust,
);
let after = Code::from_string(
"// This is a comment\nfn hello() {\n println!(\"Hello\");\n}\nfn other() {\n if true {\n 2;\n }\n}\n",
&Language::Rust,
);
let diff = crate::diff::diff_code(&before, &after)
.ast
.expect("ast diff");
let comment_mapping = diff
.mapping
.values()
.find(|m| m.reason == ASTMappingReason::LeadingSibling)
.expect("leading comment should be matched via LeadingSibling");
assert_eq!(comment_mapping.operation, ASTMappingOperation::Identical);
let before_root = before.ast.as_ref().unwrap().root_node();
let before_marker =
find_first_of_kind(before_root, "//").expect("before `//` token should exist");
assert!(
diff.before_node_map.contains_key(&before_marker.id()),
"the comment's `//` marker token should also be mapped, not just the comment node"
);
}
#[test]
fn matches_leading_attribute_of_an_unchanged_sibling_mod_item() {
let before = Code::from_string(
"#[cfg(test)]\nmod alpha;\n#[cfg(test)]\nmod beta;\n",
&Language::Rust,
);
let after = Code::from_string(
"#[cfg(test)]\nmod alpha;\n#[cfg(test)]\nmod gamma;\n#[cfg(test)]\nmod beta;\n",
&Language::Rust,
);
let diff = crate::diff::diff_code(&before, &after)
.ast
.expect("ast diff");
let leading_sibling_matches = diff
.mapping
.values()
.filter(|m| m.reason == ASTMappingReason::LeadingSibling)
.count();
assert_eq!(
leading_sibling_matches, 2,
"both alpha's and beta's #[cfg(test)] should match via LeadingSibling, \
independent of the newly-inserted gamma's own identical-looking attribute"
);
}
#[test]
fn matches_a_chain_of_two_leading_attributes() {
let before = Code::from_string(
"#[cfg(test)]\n#[allow(dead_code)]\nmod alpha;\nfn other() {}\n",
&Language::Rust,
);
let after = Code::from_string(
"#[cfg(test)]\n#[allow(dead_code)]\nmod alpha;\nfn other() {\n 1;\n}\n",
&Language::Rust,
);
let diff = crate::diff::diff_code(&before, &after)
.ast
.expect("ast diff");
let leading_sibling_matches = diff
.mapping
.values()
.filter(|m| m.reason == ASTMappingReason::LeadingSibling)
.count();
assert_eq!(
leading_sibling_matches, 2,
"both stacked attributes above `mod alpha` should match, via two chain hops"
);
}
#[test]
fn does_not_match_a_changed_leading_attribute() {
let before = Code::from_string("#[cfg(test)]\nmod alpha;\n", &Language::Rust);
let after = Code::from_string("#[cfg(not(test))]\nmod alpha;\n", &Language::Rust);
let diff = crate::diff::diff_code(&before, &after)
.ast
.expect("ast diff");
assert!(
diff.mapping
.values()
.all(|m| m.reason != ASTMappingReason::LeadingSibling),
"a changed attribute must not be matched as an identical leading sibling"
);
}
#[test]
fn leading_sibling_chain_stops_at_first_text_mismatch_and_keeps_earlier_hops() {
let before = Code::from_string(
"#[cfg(test)]\n#[allow(dead_code)]\nmod alpha;\nfn other() {}\n",
&Language::Rust,
);
let after = Code::from_string(
"#[cfg(not(test))]\n#[allow(dead_code)]\nmod alpha;\nfn other() {\n 1;\n}\n",
&Language::Rust,
);
let diff = crate::diff::diff_code(&before, &after)
.ast
.expect("ast diff");
let leading_sibling_matches = diff
.mapping
.values()
.filter(|m| m.reason == ASTMappingReason::LeadingSibling)
.count();
assert_eq!(leading_sibling_matches, 1);
}
}