use std::collections::{HashMap, HashSet};
use crate::parser::{Language, ParsedFile};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SimilarityMethod {
Lines,
Tokens,
Ast,
}
pub fn compute_similarity(a: &str, b: &str, method: SimilarityMethod) -> f64 {
match method {
SimilarityMethod::Lines => {
let lines_a: HashSet<&str> = a.lines().filter(|l| !l.trim().is_empty()).collect();
let lines_b: HashSet<&str> = b.lines().filter(|l| !l.trim().is_empty()).collect();
if lines_a.is_empty() && lines_b.is_empty() {
return 1.0;
}
if lines_a.is_empty() || lines_b.is_empty() {
return 0.0;
}
let intersection: HashSet<_> = lines_a.intersection(&lines_b).collect();
let union: HashSet<_> = lines_a.union(&lines_b).collect();
let line_similarity = intersection.len() as f64 / union.len() as f64;
if line_similarity == 0.0 {
return compute_similarity(a, b, SimilarityMethod::Tokens);
}
line_similarity
}
SimilarityMethod::Tokens => {
let tokens_a: HashSet<&str> = a.split_whitespace().collect();
let tokens_b: HashSet<&str> = b.split_whitespace().collect();
if tokens_a.is_empty() && tokens_b.is_empty() {
return 1.0;
}
if tokens_a.is_empty() || tokens_b.is_empty() {
return 0.0;
}
let intersection: HashSet<_> = tokens_a.intersection(&tokens_b).collect();
let union: HashSet<_> = tokens_a.union(&tokens_b).collect();
intersection.len() as f64 / union.len() as f64
}
SimilarityMethod::Ast => {
compute_similarity_with_language(a, b, SimilarityMethod::Ast, Language::Unknown)
}
}
}
pub fn compute_similarity_with_language(
a: &str,
b: &str,
method: SimilarityMethod,
language: Language,
) -> f64 {
match method {
SimilarityMethod::Ast => {
if let Some(score) = compute_ast_similarity(a, b, language) {
return score;
}
compute_similarity(a, b, SimilarityMethod::Tokens)
}
_ => compute_similarity(a, b, method),
}
}
pub fn try_compute_ast_similarity(a: &str, b: &str, language: Language) -> Option<f64> {
try_compute_ast_similarity_for_languages(a, language, b, language)
}
pub fn try_compute_ast_similarity_for_languages(
a: &str,
a_language: Language,
b: &str,
b_language: Language,
) -> Option<f64> {
if a_language == Language::Unknown || b_language == Language::Unknown {
return None;
}
let counts_a = ast_node_counts(a, a_language)?;
let counts_b = ast_node_counts(b, b_language)?;
Some(count_similarity(&counts_a, &counts_b))
}
pub(super) struct PreparedSimilarity {
method: SimilarityMethod,
lines: Option<HashSet<String>>,
tokens: HashSet<String>,
ast_counts: HashMap<Language, Option<HashMap<String, usize>>>,
}
impl PreparedSimilarity {
pub(super) fn new(
content: &str,
method: SimilarityMethod,
languages: impl IntoIterator<Item = Language>,
) -> Self {
let tokens = content
.split_whitespace()
.map(String::from)
.collect::<HashSet<_>>();
let lines = (method == SimilarityMethod::Lines).then(|| {
content
.lines()
.filter(|line| !line.trim().is_empty())
.map(String::from)
.collect()
});
let ast_counts = if method == SimilarityMethod::Ast {
languages
.into_iter()
.map(|language| (language, ast_node_counts(content, language)))
.collect()
} else {
HashMap::new()
};
Self {
method,
lines,
tokens,
ast_counts,
}
}
pub(super) fn similarity(&self, other: &Self, language: Language) -> f64 {
debug_assert_eq!(self.method, other.method);
match self.method {
SimilarityMethod::Lines => {
let score = set_similarity(
self.lines.as_ref().expect("lines prepared"),
other.lines.as_ref().expect("lines prepared"),
);
if score == 0.0 {
set_similarity(&self.tokens, &other.tokens)
} else {
score
}
}
SimilarityMethod::Tokens => set_similarity(&self.tokens, &other.tokens),
SimilarityMethod::Ast => match (
self.ast_counts.get(&language).and_then(Option::as_ref),
other.ast_counts.get(&language).and_then(Option::as_ref),
) {
(Some(left), Some(right)) => count_similarity(left, right),
_ => set_similarity(&self.tokens, &other.tokens),
},
}
}
}
fn set_similarity(left: &HashSet<String>, right: &HashSet<String>) -> f64 {
if left.is_empty() && right.is_empty() {
return 1.0;
}
if left.is_empty() || right.is_empty() {
return 0.0;
}
left.intersection(right).count() as f64 / left.union(right).count() as f64
}
fn count_similarity(left: &HashMap<String, usize>, right: &HashMap<String, usize>) -> f64 {
if left.is_empty() && right.is_empty() {
return 1.0;
}
if left.is_empty() || right.is_empty() {
return 0.0;
}
let keys = left.keys().chain(right.keys()).collect::<HashSet<_>>();
let (intersection, union) = keys.into_iter().fold((0usize, 0usize), |totals, key| {
let left_count = left.get(key).copied().unwrap_or(0);
let right_count = right.get(key).copied().unwrap_or(0);
(
totals.0 + left_count.min(right_count),
totals.1 + left_count.max(right_count),
)
});
if union == 0 {
0.0
} else {
intersection as f64 / union as f64
}
}
fn ast_node_counts(content: &str, language: Language) -> Option<HashMap<String, usize>> {
let parsed = ParsedFile::parse(content, language)?;
let mut counts = HashMap::new();
collect_node_kinds(parsed.root_node(), &mut counts);
Some(counts)
}
fn compute_ast_similarity(a: &str, b: &str, language: Language) -> Option<f64> {
let counts_a = ast_node_counts(a, language)?;
let counts_b = ast_node_counts(b, language)?;
Some(count_similarity(&counts_a, &counts_b))
}
fn collect_node_kinds(node: tree_sitter::Node<'_>, counts: &mut HashMap<String, usize>) {
let mut stack = vec![node];
while let Some(current) = stack.pop() {
let kind = current.kind();
let entry = counts.entry(kind.to_string()).or_insert(0);
*entry += 1;
let child_count = current.child_count();
for index in (0..child_count).rev() {
if let Some(child) = current.child(index as u32) {
stack.push(child);
}
}
}
}