use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use super::normalize::NormNode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Fingerprint(pub u64);
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
struct Fnv1a {
state: u64,
}
impl Fnv1a {
fn new() -> Self {
Self { state: FNV_OFFSET }
}
}
impl Hasher for Fnv1a {
fn write(
&mut self,
bytes: &[u8],
) {
for b in bytes {
self.state ^= u64::from(*b);
self.state = self.state.wrapping_mul(FNV_PRIME);
}
}
fn finish(&self) -> u64 {
self.state
}
}
#[must_use]
pub fn fingerprint(node: &NormNode) -> Fingerprint {
collect(node, &mut BTreeSet::new())
}
#[must_use]
pub fn fingerprints(root: &NormNode) -> BTreeSet<Fingerprint> {
let mut out = BTreeSet::new();
collect(root, &mut out);
out
}
fn collect(
node: &NormNode,
out: &mut BTreeSet<Fingerprint>,
) -> Fingerprint {
let mut hasher = Fnv1a::new();
node.kind.hash(&mut hasher);
for child in &node.children {
hasher.write_u64(collect(child, out).0);
}
let print = Fingerprint(hasher.finish());
out.insert(print);
print
}
#[cfg(test)]
mod tests {
use super::*;
use crate::duplicates::normalize::{NodeKind, normalize};
use proptest::prelude::*;
use syn::ItemFn;
fn norm(src: &str) -> NormNode {
let item: ItemFn = syn::parse_str(src).expect("test source must parse");
normalize(&item.sig, &item.block)
}
fn prints(src: &str) -> BTreeSet<Fingerprint> {
fingerprints(&norm(src))
}
#[test]
fn equal_subtrees_have_equal_fingerprints() {
let a = norm("fn f(x: i32) -> i32 { x + 1 }");
let b = norm("fn g(y: i32) -> i32 { y + 2 }");
assert_eq!(fingerprint(&a), fingerprint(&b));
}
#[test]
fn different_shapes_have_different_fingerprints() {
let a = norm("fn f(x: i32) -> i32 { x + 1 }");
let b = norm("fn f(x: i32) -> i32 { x * 1 }");
assert_ne!(fingerprint(&a), fingerprint(&b));
}
#[test]
fn the_set_contains_the_whole_function_fingerprint() {
let f = norm("fn f(x: i32) -> i32 { x + 1 }");
assert!(fingerprints(&f).contains(&fingerprint(&f)));
}
#[test]
fn the_set_contains_one_entry_per_distinct_shape() {
let f = norm("fn f(a: i32, b: i32) { g(a); g(b); }");
assert!(
fingerprints(&f).len() < f.node_count(),
"identical subtrees must share a fingerprint"
);
}
#[test]
fn nested_control_flow_order_is_structural() {
let a = prints("fn f(v: V) { for x in v { if p(x) { g(x); } } }");
let b = prints("fn f(v: V) { if p(v) { for x in v { g(x); } } }");
assert_ne!(a, b, "the same constructs nested differently are different");
}
#[test]
fn statement_order_is_structural_but_the_statements_are_shared() {
let a = prints("fn f(v: V) { g(v); if p(v) { h(v); } }");
let b = prints("fn f(v: V) { if p(v) { h(v); } g(v); }");
assert_ne!(a, b, "reordering changes the block's shape");
let shared: Vec<_> = a.intersection(&b).collect();
assert!(
shared.len() >= 2,
"the reordered statements themselves still share fingerprints"
);
}
#[test]
fn fingerprinting_is_deterministic() {
let src = "fn f(v: V) -> V { for x in v { g(x); } v }";
assert_eq!(prints(src), prints(src));
}
#[test]
fn a_leaf_fingerprint_is_stable_for_a_known_shape() {
let leaf = NormNode::leaf(NodeKind::Path);
assert_eq!(
fingerprint(&leaf),
fingerprint(&NormNode::leaf(NodeKind::Path))
);
}
proptest! {
#[test]
fn set_size_is_bounded_by_node_count(n in 1usize..12) {
let body = "g(a); ".repeat(n);
let f = norm(&format!("fn f(a: A) {{ {body} }}"));
prop_assert!(fingerprints(&f).len() <= f.node_count());
}
#[test]
fn renaming_does_not_change_the_set(name in "x_[a-z]{1,5}") {
let one = prints(&format!("fn f({name}: i32) -> i32 {{ {name} + 1 }}"));
let two = prints("fn f(v: i32) -> i32 { v + 1 }");
prop_assert_eq!(one, two);
}
}
}