use std::collections::HashMap;
use serde::Serialize;
use crate::model::{ExtractedSymbol, SymbolKind, SymbolWithParent};
pub const BUDGET_GUARD_THRESHOLD: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ChangeType {
Added,
Modified,
Removed,
}
#[derive(Debug, Clone, Serialize)]
pub struct SymbolChange {
pub name: String,
pub kind: SymbolKind,
pub change_type: ChangeType,
pub file_path: String,
pub line: u32,
pub parent_name: Option<String>,
pub old_signature: Option<String>,
pub new_signature: Option<String>,
}
#[derive(Hash, Eq, PartialEq)]
pub(crate) struct SymbolKey {
pub name: String,
pub kind: SymbolKind,
pub parent_name: Option<String>,
}
pub(crate) fn resolve_parent_name(
symbols: &[ExtractedSymbol],
sym: &ExtractedSymbol,
) -> Option<String> {
sym.parent_index
.and_then(|idx| symbols.get(idx).map(|p| p.name.clone()))
}
pub(crate) fn symbol_change_from_current(
sym: SymbolWithParent,
change_type: ChangeType,
) -> SymbolChange {
SymbolChange {
name: sym.name,
kind: sym.kind,
change_type,
file_path: sym.file_path,
line: sym.start_line as u32,
parent_name: sym.parent_name,
old_signature: None,
new_signature: sym.signature,
}
}
pub(crate) fn symbol_change_from_old(
sym: &ExtractedSymbol,
all_old: &[ExtractedSymbol],
file_path: &str,
) -> SymbolChange {
SymbolChange {
name: sym.name.clone(),
kind: sym.kind,
change_type: ChangeType::Removed,
file_path: file_path.to_string(),
line: sym.start_line as u32,
parent_name: resolve_parent_name(all_old, sym),
old_signature: sym.signature.clone(),
new_signature: None,
}
}
pub fn compare_extracted_symbols(
old_symbols: &[ExtractedSymbol],
current_symbols: &[SymbolWithParent],
file_path: &str,
) -> (Vec<SymbolChange>, Vec<i64>) {
let mut old_map: HashMap<SymbolKey, Vec<&ExtractedSymbol>> = HashMap::new();
for sym in old_symbols {
let key = SymbolKey {
name: sym.name.clone(),
kind: sym.kind,
parent_name: resolve_parent_name(old_symbols, sym),
};
old_map.entry(key).or_default().push(sym);
}
let mut current_map: HashMap<SymbolKey, Vec<&SymbolWithParent>> = HashMap::new();
for sym in current_symbols {
let key = SymbolKey {
name: sym.name.clone(),
kind: sym.kind,
parent_name: sym.parent_name.clone(),
};
current_map.entry(key).or_default().push(sym);
}
let mut changes = Vec::new();
let mut modified_ids = Vec::new();
for (key, cur_syms) in ¤t_map {
if let Some(old_syms) = old_map.get(key) {
let paired = old_syms.len().min(cur_syms.len());
for i in 0..paired {
if old_syms[i].signature != cur_syms[i].signature {
changes.push(SymbolChange {
name: cur_syms[i].name.clone(),
kind: cur_syms[i].kind,
change_type: ChangeType::Modified,
file_path: file_path.to_string(),
line: cur_syms[i].start_line as u32,
parent_name: cur_syms[i].parent_name.clone(),
old_signature: old_syms[i].signature.clone(),
new_signature: cur_syms[i].signature.clone(),
});
modified_ids.push(cur_syms[i].id);
}
}
for cur in cur_syms.iter().skip(paired) {
changes.push(SymbolChange {
name: cur.name.clone(),
kind: cur.kind,
change_type: ChangeType::Added,
file_path: file_path.to_string(),
line: cur.start_line as u32,
parent_name: cur.parent_name.clone(),
old_signature: None,
new_signature: cur.signature.clone(),
});
}
} else {
for cur in cur_syms {
changes.push(SymbolChange {
name: cur.name.clone(),
kind: cur.kind,
change_type: ChangeType::Added,
file_path: file_path.to_string(),
line: cur.start_line as u32,
parent_name: cur.parent_name.clone(),
old_signature: None,
new_signature: cur.signature.clone(),
});
}
}
}
for (key, old_syms) in &old_map {
let paired = current_map
.get(key)
.map_or(0, |c| c.len().min(old_syms.len()));
for old in old_syms.iter().skip(paired) {
changes.push(symbol_change_from_old(old, old_symbols, file_path));
}
}
if changes.len() > BUDGET_GUARD_THRESHOLD {
modified_ids.clear();
}
(changes, modified_ids)
}
pub fn compare_db_symbols(
old_symbols: &[SymbolWithParent],
current_symbols: &[SymbolWithParent],
file_path: &str,
) -> (Vec<SymbolChange>, Vec<i64>) {
let mut old_map: HashMap<SymbolKey, Vec<&SymbolWithParent>> = HashMap::new();
for sym in old_symbols {
let key = SymbolKey {
name: sym.name.clone(),
kind: sym.kind,
parent_name: sym.parent_name.clone(),
};
old_map.entry(key).or_default().push(sym);
}
let mut current_map: HashMap<SymbolKey, Vec<&SymbolWithParent>> = HashMap::new();
for sym in current_symbols {
let key = SymbolKey {
name: sym.name.clone(),
kind: sym.kind,
parent_name: sym.parent_name.clone(),
};
current_map.entry(key).or_default().push(sym);
}
let mut changes = Vec::new();
let mut modified_ids = Vec::new();
for (key, cur_syms) in ¤t_map {
if let Some(old_syms) = old_map.get(key) {
let paired = old_syms.len().min(cur_syms.len());
for i in 0..paired {
if old_syms[i].signature != cur_syms[i].signature {
changes.push(SymbolChange {
name: cur_syms[i].name.clone(),
kind: cur_syms[i].kind,
change_type: ChangeType::Modified,
file_path: file_path.to_string(),
line: cur_syms[i].start_line as u32,
parent_name: cur_syms[i].parent_name.clone(),
old_signature: old_syms[i].signature.clone(),
new_signature: cur_syms[i].signature.clone(),
});
modified_ids.push(cur_syms[i].id);
}
}
for cur in cur_syms.iter().skip(paired) {
changes.push(symbol_change_from_current(
(*cur).clone(),
ChangeType::Added,
));
}
} else {
for cur in cur_syms {
changes.push(symbol_change_from_current(
(*cur).clone(),
ChangeType::Added,
));
}
}
}
for (key, old_syms) in &old_map {
let paired = current_map
.get(key)
.map_or(0, |c| c.len().min(old_syms.len()));
for old in old_syms.iter().skip(paired) {
changes.push(SymbolChange {
name: old.name.clone(),
kind: old.kind,
change_type: ChangeType::Removed,
file_path: file_path.to_string(),
line: old.start_line as u32,
parent_name: old.parent_name.clone(),
old_signature: old.signature.clone(),
new_signature: None,
});
}
}
if changes.len() > BUDGET_GUARD_THRESHOLD {
modified_ids.clear();
}
(changes, modified_ids)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_extracted(
name: &str,
kind: SymbolKind,
sig: Option<&str>,
parent_index: Option<usize>,
) -> ExtractedSymbol {
ExtractedSymbol {
name: name.to_string(),
kind,
signature: sig.map(|s| s.to_string()),
start_line: 1,
end_line: 5,
start_byte: 0,
end_byte: 100,
parent_index,
}
}
fn make_db_sym(
id: i64,
name: &str,
kind: SymbolKind,
sig: Option<&str>,
parent_name: Option<&str>,
) -> SymbolWithParent {
SymbolWithParent {
id,
name: name.to_string(),
kind,
file_path: "src/lib.rs".to_string(),
start_line: 1,
end_line: 5,
signature: sig.map(|s| s.to_string()),
parent_name: parent_name.map(|s| s.to_string()),
}
}
#[test]
fn extracted_added_new_key() {
let (changes, _) = compare_extracted_symbols(
&[],
&[make_db_sym(
1,
"new_fn",
SymbolKind::Function,
Some("fn new_fn()"),
None,
)],
"src/lib.rs",
);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].name, "new_fn");
assert_eq!(changes[0].change_type, ChangeType::Added);
}
#[test]
fn extracted_removed_key() {
let old = vec![make_extracted(
"old_fn",
SymbolKind::Function,
Some("fn old_fn()"),
None,
)];
let (changes, _) = compare_extracted_symbols(&old, &[], "src/lib.rs");
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].name, "old_fn");
assert_eq!(changes[0].change_type, ChangeType::Removed);
}
#[test]
fn extracted_modified_signature() {
let old = vec![make_extracted(
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let cur = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo(x: i32)"),
None,
)];
let (changes, mod_ids) = compare_extracted_symbols(&old, &cur, "src/lib.rs");
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].change_type, ChangeType::Modified);
assert_eq!(changes[0].old_signature.as_deref(), Some("fn foo()"));
assert_eq!(changes[0].new_signature.as_deref(), Some("fn foo(x: i32)"));
assert_eq!(mod_ids, vec![1]);
}
#[test]
fn extracted_unchanged() {
let old = vec![make_extracted(
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let cur = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let (changes, _) = compare_extracted_symbols(&old, &cur, "src/lib.rs");
assert!(changes.is_empty());
}
#[test]
fn extracted_budget_guard() {
let old: Vec<ExtractedSymbol> = (0..6)
.map(|i| {
make_extracted(
&format!("fn_{i}"),
SymbolKind::Function,
Some(&format!("fn fn_{i}()")),
None,
)
})
.collect();
let (changes, mod_ids) = compare_extracted_symbols(&old, &[], "src/lib.rs");
assert_eq!(changes.len(), 6);
assert!(mod_ids.is_empty(), "budget guard should clear modified_ids");
}
#[test]
fn db_added_new_key() {
let cur = vec![make_db_sym(
1,
"new_fn",
SymbolKind::Function,
Some("fn new_fn()"),
None,
)];
let (changes, _) = compare_db_symbols(&[], &cur, "src/lib.rs");
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].name, "new_fn");
assert_eq!(changes[0].change_type, ChangeType::Added);
}
#[test]
fn db_removed_key() {
let old = vec![make_db_sym(
1,
"old_fn",
SymbolKind::Function,
Some("fn old_fn()"),
None,
)];
let (changes, _) = compare_db_symbols(&old, &[], "src/lib.rs");
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].name, "old_fn");
assert_eq!(changes[0].change_type, ChangeType::Removed);
}
#[test]
fn db_modified_signature() {
let old = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let cur = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo(x: i32)"),
None,
)];
let (changes, mod_ids) = compare_db_symbols(&old, &cur, "src/lib.rs");
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].change_type, ChangeType::Modified);
assert_eq!(mod_ids, vec![1]);
}
#[test]
fn db_unchanged() {
let old = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let cur = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let (changes, _) = compare_db_symbols(&old, &cur, "src/lib.rs");
assert!(changes.is_empty());
}
#[test]
fn db_budget_guard() {
let old: Vec<SymbolWithParent> = (0..6)
.map(|i| {
make_db_sym(
i,
&format!("fn_{i}"),
SymbolKind::Function,
Some(&format!("fn fn_{i}()")),
None,
)
})
.collect();
let (changes, mod_ids) = compare_db_symbols(&old, &[], "src/lib.rs");
assert_eq!(changes.len(), 6);
assert!(mod_ids.is_empty(), "budget guard should clear modified_ids");
}
#[test]
fn db_extra_current_beyond_paired() {
let old = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let cur = vec![
make_db_sym(1, "foo", SymbolKind::Function, Some("fn foo()"), None),
make_db_sym(2, "foo", SymbolKind::Function, Some("fn foo(x: i32)"), None),
];
let (changes, _) = compare_db_symbols(&old, &cur, "src/lib.rs");
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].change_type, ChangeType::Added);
}
#[test]
fn db_extra_old_beyond_paired() {
let old = vec![
make_db_sym(1, "foo", SymbolKind::Function, Some("fn foo()"), None),
make_db_sym(2, "foo", SymbolKind::Function, Some("fn foo(x: i32)"), None),
];
let cur = vec![make_db_sym(
1,
"foo",
SymbolKind::Function,
Some("fn foo()"),
None,
)];
let (changes, _) = compare_db_symbols(&old, &cur, "src/lib.rs");
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].change_type, ChangeType::Removed);
}
#[test]
fn change_type_equality() {
assert_eq!(ChangeType::Added, ChangeType::Added);
assert_eq!(ChangeType::Modified, ChangeType::Modified);
assert_eq!(ChangeType::Removed, ChangeType::Removed);
assert_ne!(ChangeType::Added, ChangeType::Removed);
}
#[test]
fn resolve_parent_name_with_parent() {
let symbols = vec![
make_extracted("MyClass", SymbolKind::Class, None, None),
make_extracted("method", SymbolKind::Method, None, Some(0)),
];
assert_eq!(
resolve_parent_name(&symbols, &symbols[1]),
Some("MyClass".to_string())
);
}
#[test]
fn resolve_parent_name_without_parent() {
let symbols = vec![make_extracted("top_fn", SymbolKind::Function, None, None)];
assert_eq!(resolve_parent_name(&symbols, &symbols[0]), None);
}
#[test]
fn resolve_parent_name_out_of_bounds() {
let symbols = vec![make_extracted("orphan", SymbolKind::Method, None, Some(99))];
assert_eq!(resolve_parent_name(&symbols, &symbols[0]), None);
}
}