use super::changes::{ChangeKind, ChangeMap};
use super::syntax::Syntax;
use crate::commands::remaining::types::{
ASTChange, ChangeType, DiffGranularity, DiffReport, DiffSummary, Location, NodeKind,
};
pub fn changemap_to_l1_report<'a>(
lhs_nodes: &[&'a Syntax<'a>],
rhs_nodes: &[&'a Syntax<'a>],
change_map: &ChangeMap<'a>,
file_a: &str,
file_b: &str,
) -> DiffReport {
let mut changes: Vec<ASTChange> = Vec::new();
walk_lhs_nodes(lhs_nodes, change_map, file_a, file_b, &mut changes);
walk_rhs_nodes(rhs_nodes, change_map, file_b, &mut changes);
let mut summary = DiffSummary::default();
for change in &changes {
summary.total_changes += 1;
summary.semantic_changes += 1;
match change.change_type {
ChangeType::Insert => summary.inserts += 1,
ChangeType::Delete => summary.deletes += 1,
ChangeType::Update => summary.updates += 1,
_ => {}
}
}
DiffReport {
file_a: file_a.to_string(),
file_b: file_b.to_string(),
identical: changes.is_empty(),
changes,
summary: Some(summary),
granularity: DiffGranularity::Token,
file_changes: None,
module_changes: None,
import_graph_summary: None,
arch_changes: None,
arch_summary: None,
}
}
pub fn changemap_to_l2_report<'a>(
lhs_nodes: &[&'a Syntax<'a>],
rhs_nodes: &[&'a Syntax<'a>],
change_map: &ChangeMap<'a>,
file_a: &str,
file_b: &str,
) -> DiffReport {
let mut changes: Vec<ASTChange> = Vec::new();
l2_walk_lhs_nodes(lhs_nodes, change_map, file_a, file_b, &mut changes);
l2_walk_rhs_nodes(rhs_nodes, change_map, file_b, &mut changes);
let mut summary = DiffSummary::default();
for change in &changes {
summary.total_changes += 1;
summary.semantic_changes += 1;
match change.change_type {
ChangeType::Insert => summary.inserts += 1,
ChangeType::Delete => summary.deletes += 1,
ChangeType::Update => summary.updates += 1,
_ => {}
}
}
DiffReport {
file_a: file_a.to_string(),
file_b: file_b.to_string(),
identical: changes.is_empty(),
changes,
summary: Some(summary),
granularity: DiffGranularity::Expression,
file_changes: None,
module_changes: None,
import_graph_summary: None,
arch_changes: None,
arch_summary: None,
}
}
fn l2_walk_lhs_nodes<'a>(
nodes: &[&'a Syntax<'a>],
change_map: &ChangeMap<'a>,
file_a: &str,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
for node in nodes {
l2_walk_lhs_node(node, change_map, file_a, file_b, changes);
}
}
fn l2_walk_lhs_node<'a>(
node: &'a Syntax<'a>,
change_map: &ChangeMap<'a>,
file_a: &str,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
let change_kind = change_map.get(node);
match node {
Syntax::Atom {
content, position, ..
} => match change_kind {
Some(ChangeKind::Novel) => {
changes.push(ASTChange {
change_type: ChangeType::Delete,
node_kind: NodeKind::Expression,
name: Some(truncate_name(content)),
old_location: Some(span_to_location(position, file_a)),
new_location: None,
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
Some(ChangeKind::ReplacedComment(_, rhs_node))
| Some(ChangeKind::ReplacedString(_, rhs_node)) => {
let (rhs_content, rhs_position) = atom_content_and_position(rhs_node);
let sim = strsim::normalized_levenshtein(content, rhs_content);
changes.push(ASTChange {
change_type: ChangeType::Update,
node_kind: NodeKind::Expression,
name: Some(truncate_name(content)),
old_location: Some(span_to_location(position, file_a)),
new_location: Some(span_to_location(rhs_position, file_b)),
old_text: Some(content.clone()),
new_text: Some(rhs_content.to_string()),
similarity: Some(sim),
children: None,
base_changes: None,
});
}
Some(ChangeKind::Unchanged(_)) | None => {
}
},
Syntax::List {
open_content,
open_position,
children,
..
} => match change_kind {
Some(ChangeKind::Novel) => {
let name = if !open_content.is_empty() {
truncate_name(open_content)
} else if let Some(first_child) = children.first() {
first_atom_name(first_child)
} else {
"<list>".to_string()
};
changes.push(ASTChange {
change_type: ChangeType::Delete,
node_kind: NodeKind::Expression,
name: Some(name),
old_location: Some(span_to_location(open_position, file_a)),
new_location: None,
old_text: None,
new_text: None,
similarity: None,
children: None, base_changes: None,
});
}
Some(ChangeKind::Unchanged(opposite)) => {
let lhs_has_changes = has_any_changed_child(children, change_map);
let rhs_has_changes = if let Syntax::List {
children: opp_children,
..
} = opposite
{
has_any_changed_child(opp_children, change_map)
} else {
false
};
if lhs_has_changes || rhs_has_changes {
let mut child_changes: Vec<ASTChange> = Vec::new();
walk_lhs_nodes(children, change_map, file_a, file_b, &mut child_changes);
if let Syntax::List {
children: opp_children,
..
} = opposite
{
walk_rhs_nodes(opp_children, change_map, file_b, &mut child_changes);
}
if !child_changes.is_empty() {
let name = if !open_content.is_empty() {
truncate_name(open_content)
} else if let Some(first_child) = children.first() {
first_atom_name(first_child)
} else {
"<list>".to_string()
};
let new_loc = match opposite {
Syntax::List {
open_position: opp_pos,
..
} => Some(span_to_location(opp_pos, file_b)),
_ => None,
};
changes.push(ASTChange {
change_type: ChangeType::Update,
node_kind: NodeKind::Expression,
name: Some(name),
old_location: Some(span_to_location(open_position, file_a)),
new_location: new_loc,
old_text: None,
new_text: None,
similarity: None,
children: Some(child_changes),
base_changes: None,
});
}
} else {
l2_walk_lhs_nodes(children, change_map, file_a, file_b, changes);
}
}
None => {
l2_walk_lhs_nodes(children, change_map, file_a, file_b, changes);
}
Some(ChangeKind::ReplacedComment(_, _)) | Some(ChangeKind::ReplacedString(_, _)) => {
l2_walk_lhs_nodes(children, change_map, file_a, file_b, changes);
}
},
}
}
fn l2_walk_rhs_nodes<'a>(
nodes: &[&'a Syntax<'a>],
change_map: &ChangeMap<'a>,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
for node in nodes {
l2_walk_rhs_node(node, change_map, file_b, changes);
}
}
fn l2_walk_rhs_node<'a>(
node: &'a Syntax<'a>,
change_map: &ChangeMap<'a>,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
let change_kind = change_map.get(node);
match node {
Syntax::Atom {
content, position, ..
} => match change_kind {
Some(ChangeKind::Novel) => {
changes.push(ASTChange {
change_type: ChangeType::Insert,
node_kind: NodeKind::Expression,
name: Some(truncate_name(content)),
old_location: None,
new_location: Some(span_to_location(position, file_b)),
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
Some(ChangeKind::ReplacedComment(_, _)) | Some(ChangeKind::ReplacedString(_, _)) => {
}
Some(ChangeKind::Unchanged(_)) | None => {
}
},
Syntax::List {
open_content,
open_position,
children,
..
} => match change_kind {
Some(ChangeKind::Novel) => {
let name = if !open_content.is_empty() {
truncate_name(open_content)
} else if let Some(first_child) = children.first() {
first_atom_name(first_child)
} else {
"<list>".to_string()
};
changes.push(ASTChange {
change_type: ChangeType::Insert,
node_kind: NodeKind::Expression,
name: Some(name),
old_location: None,
new_location: Some(span_to_location(open_position, file_b)),
old_text: None,
new_text: None,
similarity: None,
children: None, base_changes: None,
});
}
Some(ChangeKind::Unchanged(opposite)) => {
let this_rhs_has_changes = has_any_changed_child(children, change_map);
if !this_rhs_has_changes {
l2_walk_rhs_nodes(children, change_map, file_b, changes);
} else {
let lhs_already_captured = if let Syntax::List { .. } = opposite {
match change_map.get(opposite) {
Some(ChangeKind::Unchanged(lhs_opposite)) => {
std::ptr::eq(lhs_opposite, node)
}
_ => false,
}
} else {
false
};
if lhs_already_captured {
} else {
l2_walk_rhs_nodes(children, change_map, file_b, changes);
}
}
}
None => {
l2_walk_rhs_nodes(children, change_map, file_b, changes);
}
Some(ChangeKind::ReplacedComment(_, _)) | Some(ChangeKind::ReplacedString(_, _)) => {
}
},
}
}
fn has_any_changed_child<'a>(children: &[&'a Syntax<'a>], change_map: &ChangeMap<'a>) -> bool {
children
.iter()
.any(|c| !matches!(change_map.get(c), Some(ChangeKind::Unchanged(_))))
}
fn first_atom_name(node: &Syntax) -> String {
match node {
Syntax::Atom { content, .. } => truncate_name(content),
Syntax::List {
children,
open_content,
..
} => {
if !open_content.is_empty() {
truncate_name(open_content)
} else if let Some(child) = children.first() {
first_atom_name(child)
} else {
"<list>".to_string()
}
}
}
}
fn walk_lhs_nodes<'a>(
nodes: &[&'a Syntax<'a>],
change_map: &ChangeMap<'a>,
file_a: &str,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
for node in nodes {
walk_lhs_node(node, change_map, file_a, file_b, changes);
}
}
fn walk_lhs_node<'a>(
node: &'a Syntax<'a>,
change_map: &ChangeMap<'a>,
file_a: &str,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
let change_kind = change_map.get(node);
match node {
Syntax::Atom {
content, position, ..
} => match change_kind {
Some(ChangeKind::Novel) => {
changes.push(ASTChange {
change_type: ChangeType::Delete,
node_kind: NodeKind::Expression,
name: Some(truncate_name(content)),
old_location: Some(span_to_location(position, file_a)),
new_location: None,
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
Some(ChangeKind::ReplacedComment(_, rhs_node))
| Some(ChangeKind::ReplacedString(_, rhs_node)) => {
let (rhs_content, rhs_position) = atom_content_and_position(rhs_node);
let sim = strsim::normalized_levenshtein(content, rhs_content);
changes.push(ASTChange {
change_type: ChangeType::Update,
node_kind: NodeKind::Expression,
name: Some(truncate_name(content)),
old_location: Some(span_to_location(position, file_a)),
new_location: Some(span_to_location(rhs_position, file_b)),
old_text: Some(content.clone()),
new_text: Some(rhs_content.to_string()),
similarity: Some(sim),
children: None,
base_changes: None,
});
}
Some(ChangeKind::Unchanged(_)) | None => {
}
},
Syntax::List {
open_content,
open_position,
children,
close_content,
close_position,
..
} => match change_kind {
Some(ChangeKind::Novel) => {
if !open_content.is_empty() {
changes.push(ASTChange {
change_type: ChangeType::Delete,
node_kind: NodeKind::Expression,
name: Some(truncate_name(open_content)),
old_location: Some(span_to_location(open_position, file_a)),
new_location: None,
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
walk_lhs_nodes(children, change_map, file_a, file_b, changes);
if !close_content.is_empty() {
changes.push(ASTChange {
change_type: ChangeType::Delete,
node_kind: NodeKind::Expression,
name: Some(truncate_name(close_content)),
old_location: Some(span_to_location(close_position, file_a)),
new_location: None,
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
}
Some(ChangeKind::Unchanged(_)) | None => {
walk_lhs_nodes(children, change_map, file_a, file_b, changes);
}
Some(ChangeKind::ReplacedComment(_, _)) | Some(ChangeKind::ReplacedString(_, _)) => {
walk_lhs_nodes(children, change_map, file_a, file_b, changes);
}
},
}
}
fn walk_rhs_nodes<'a>(
nodes: &[&'a Syntax<'a>],
change_map: &ChangeMap<'a>,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
for node in nodes {
walk_rhs_node(node, change_map, file_b, changes);
}
}
fn walk_rhs_node<'a>(
node: &'a Syntax<'a>,
change_map: &ChangeMap<'a>,
file_b: &str,
changes: &mut Vec<ASTChange>,
) {
let change_kind = change_map.get(node);
match node {
Syntax::Atom {
content, position, ..
} => match change_kind {
Some(ChangeKind::Novel) => {
changes.push(ASTChange {
change_type: ChangeType::Insert,
node_kind: NodeKind::Expression,
name: Some(truncate_name(content)),
old_location: None,
new_location: Some(span_to_location(position, file_b)),
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
Some(ChangeKind::ReplacedComment(_, _)) | Some(ChangeKind::ReplacedString(_, _)) => {
}
Some(ChangeKind::Unchanged(_)) | None => {
}
},
Syntax::List {
open_content,
open_position,
children,
close_content,
close_position,
..
} => match change_kind {
Some(ChangeKind::Novel) => {
if !open_content.is_empty() {
changes.push(ASTChange {
change_type: ChangeType::Insert,
node_kind: NodeKind::Expression,
name: Some(truncate_name(open_content)),
old_location: None,
new_location: Some(span_to_location(open_position, file_b)),
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
walk_rhs_nodes(children, change_map, file_b, changes);
if !close_content.is_empty() {
changes.push(ASTChange {
change_type: ChangeType::Insert,
node_kind: NodeKind::Expression,
name: Some(truncate_name(close_content)),
old_location: None,
new_location: Some(span_to_location(close_position, file_b)),
old_text: None,
new_text: None,
similarity: None,
children: None,
base_changes: None,
});
}
}
Some(ChangeKind::Unchanged(_)) | None => {
walk_rhs_nodes(children, change_map, file_b, changes);
}
Some(ChangeKind::ReplacedComment(_, _)) | Some(ChangeKind::ReplacedString(_, _)) => {
walk_rhs_nodes(children, change_map, file_b, changes);
}
},
}
}
fn atom_content_and_position<'a>(
node: &'a Syntax<'a>,
) -> (&'a str, &'a [line_numbers::SingleLineSpan]) {
match node {
Syntax::Atom {
content, position, ..
} => (content.as_str(), position.as_slice()),
Syntax::List { .. } => {
("", &[])
}
}
}
fn span_to_location(spans: &[line_numbers::SingleLineSpan], file: &str) -> Location {
match spans {
[] => Location::new(file, 1),
[first, ..] => {
let line = first.line.0 + 1; let col = first.start_col;
let last = spans.last().unwrap();
let end_line = last.line.0 + 1;
let end_col = last.end_col;
Location {
file: file.to_string(),
line,
column: col,
end_line: Some(end_line),
end_column: Some(end_col),
}
}
}
}
fn truncate_name(content: &str) -> String {
let cleaned: String = content
.chars()
.map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
.collect();
if cleaned.len() <= 80 {
cleaned
} else {
let mut s = cleaned[..80].to_string();
s.push_str("...");
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_name_short() {
assert_eq!(truncate_name("hello"), "hello");
}
#[test]
fn test_truncate_name_long() {
let long = "a".repeat(100);
let result = truncate_name(&long);
assert_eq!(result.len(), 83); assert!(result.ends_with("..."));
}
#[test]
fn test_truncate_name_newlines() {
assert_eq!(truncate_name("foo\nbar"), "foo bar");
}
}