use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::Serialize;
use walkdir::WalkDir;
use crate::analyses::query::query_map_collect;
use crate::clones::{CloneLanguage, extract_functions, group_clones};
use crate::facts::FactsDb;
use crate::options::Options;
use crate::{CodeLoreError, Result};
#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct ClonesRow {
pub clone_group_id: u32,
pub fingerprint: String,
pub entity: String,
pub function: String,
pub start_line: u32,
pub end_line: u32,
pub node_count: u32,
pub similarity: f64,
pub family_size: u32,
}
#[tracing::instrument(name = "clones", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_clones(opts: &Options) -> Result<Vec<ClonesRow>> {
use rayon::iter::{IntoParallelIterator, ParallelIterator};
let filter = crate::paths_filter::PathsFilter::from_opts(opts)?;
let candidates: Vec<(PathBuf, String, CloneLanguage)> = WalkDir::new(&opts.repo_path)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| entry.file_type().is_file())
.filter_map(|entry| {
let path = entry.path();
let lang = CloneLanguage::from_path(path)?;
let rel = relative(&opts.repo_path, path);
let rel_path = std::path::Path::new(&rel);
if crate::paths_filter::is_git_metadata(rel_path) {
return None;
}
if filter.is_excluded(rel_path, false) {
return None;
}
Some((path.to_path_buf(), rel, lang))
})
.collect();
let all_fns: Vec<_> = candidates
.into_par_iter()
.filter_map(|(path, rel, lang)| -> Option<Result<Vec<_>>> {
let code = fs::read(&path).ok()?;
if code.len() > crate::constants::DEFAULT_MAX_AST_FILE_BYTES {
tracing::debug!(
"clones: skipping {rel} ({size} bytes > {cap}-byte AST cap)",
size = code.len(),
cap = crate::constants::DEFAULT_MAX_AST_FILE_BYTES,
);
return None;
}
Some(
extract_functions(&rel, &code, lang)
.map_err(|e| CodeLoreError::Analysis(format!("clones: extract {rel}: {e}"))),
)
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect();
let groups = group_clones(all_fns, opts.min_clone_node_count);
let mut rows = Vec::new();
for group in groups {
let family_size = u32::try_from(group.members.len()).unwrap_or(u32::MAX);
for member in &group.members {
rows.push(ClonesRow {
clone_group_id: group.clone_group_id,
fingerprint: member.fingerprint.hex(),
entity: member.path.clone(),
function: member.function_name.clone(),
start_line: member.start_line,
end_line: member.end_line,
node_count: member.fingerprint.node_count,
similarity: 1.0, family_size,
});
}
}
Ok(rows)
}
pub(crate) fn run_clones_memoised(
db: &FactsDb,
opts: &Options,
) -> Result<std::rc::Rc<Vec<ClonesRow>>> {
let memo = db.analysis_memo::<crate::analyses::memo::ClonesMemo>();
if let Some(cached) = memo.get() {
return Ok(cached);
}
let rows = std::rc::Rc::new(run_clones(opts)?);
memo.put(rows.clone());
Ok(rows)
}
pub(crate) fn head_clone_counts(db: &FactsDb) -> Result<HashMap<String, u32>> {
let rows = query_map_collect(
db,
"SELECT path, COUNT(*) FROM clones GROUP BY path",
[],
"head clone counts",
|r| {
let path = r.get::<_, String>(0)?;
let count = u32::try_from(r.get::<_, i64>(1)?).unwrap_or(u32::MAX);
Ok((path, count))
},
)?;
Ok(rows.into_iter().collect())
}
fn relative(root: &Path, abs: &Path) -> String {
abs.strip_prefix(root)
.map_or_else(|_| crate::paths::to_posix(abs), crate::paths::to_posix)
}
#[cfg(all(test, feature = "test-support"))]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn finds_type2_clone_pair_in_a_tempdir() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a.rs");
let b = dir.path().join("b.rs");
let mut fa = std::fs::File::create(&a).unwrap();
writeln!(
fa,
"fn add(a: i32, b: i32) -> i32 {{ let x = 1; let y = 2; a + b + x + y }}"
)
.unwrap();
let mut fb = std::fs::File::create(&b).unwrap();
writeln!(
fb,
"fn mul(p: u64, q: u64) -> u64 {{ let s = 9; let t = 7; p + q + s + t }}"
)
.unwrap();
let opts = Options {
repo_path: dir.path().to_path_buf(),
min_clone_node_count: 0, ..Options::default()
};
let rows = run_clones(&opts).unwrap();
assert_eq!(rows.len(), 2, "Type 2 pair → 2 rows in 1 family");
assert_eq!(rows[0].clone_group_id, rows[1].clone_group_id);
assert_eq!(rows[0].family_size, 2);
let entities: Vec<_> = rows.iter().map(|r| r.entity.as_str()).collect();
assert!(entities.iter().any(|e| e.ends_with("a.rs")));
assert!(entities.iter().any(|e| e.ends_with("b.rs")));
}
#[test]
fn run_clones_memoised_serves_second_call_from_the_memo() {
let fx = crate::test_support::differential_repo::build();
let repo = crate::repo::GixRepo::open(fx.dir.path()).expect("open");
let db = crate::facts::FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: fx.dir.path().to_path_buf(),
min_clone_node_count: 0,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
assert!(
db.analysis_memo::<crate::analyses::memo::ClonesMemo>()
.get()
.is_none(),
"memo starts empty"
);
let first = run_clones_memoised(&db, &opts).expect("first walk");
assert!(
db.analysis_memo::<crate::analyses::memo::ClonesMemo>()
.get()
.is_some(),
"first call must populate the memo",
);
let second = run_clones_memoised(&db, &opts).expect("second walk");
assert!(
std::rc::Rc::ptr_eq(&first, &second),
"second call must be served from the memo (same Rc allocation)",
);
}
}