use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use syn::visit::{self, Visit};
use syn::{Block, File, ImplItemFn, ItemFn, ItemMod, Signature};
use super::fingerprint::{Fingerprint, fingerprints};
use super::normalize::normalize;
use crate::complexity::{has_attr, is_cfg_test};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Location {
pub file: PathBuf,
pub start_line: usize,
pub end_line: usize,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FunctionPrint {
pub location: Location,
pub node_count: usize,
pub prints: BTreeSet<Fingerprint>,
}
#[must_use]
pub fn functions_in_file(
file: &File,
path: &Path,
) -> Vec<FunctionPrint> {
let mut collector = Collector {
file: path.to_path_buf(),
out: Vec::new(),
};
collector.visit_file(file);
collector.out
}
struct Collector {
file: PathBuf,
out: Vec<FunctionPrint>,
}
impl Collector {
fn record(
&mut self,
sig: &Signature,
block: &Block,
) {
let tree = normalize(sig, block);
self.out.push(FunctionPrint {
location: Location {
file: self.file.clone(),
start_line: sig.fn_token.span.start().line,
end_line: block.brace_token.span.close().end().line,
name: sig.ident.to_string(),
},
node_count: tree.node_count(),
prints: fingerprints(&tree),
});
}
}
impl<'ast> Visit<'ast> for Collector {
fn visit_item_fn(
&mut self,
node: &'ast ItemFn,
) {
if has_attr(&node.attrs, "test") {
return;
}
self.record(&node.sig, &node.block);
visit::visit_item_fn(self, node);
}
fn visit_impl_item_fn(
&mut self,
node: &'ast ImplItemFn,
) {
if has_attr(&node.attrs, "test") {
return;
}
self.record(&node.sig, &node.block);
visit::visit_impl_item_fn(self, node);
}
fn visit_item_mod(
&mut self,
node: &'ast ItemMod,
) {
if !is_cfg_test(&node.attrs) {
visit::visit_item_mod(self, node);
}
}
}
pub fn functions_in_source(
src: &str,
path: &Path,
) -> Result<Vec<FunctionPrint>, syn::Error> {
let file: File = syn::parse_file(src)?;
Ok(functions_in_file(&file, path))
}
#[cfg(test)]
mod tests {
use super::*;
const MIXED: &str = "
fn real_a(xs: &[i32]) -> i32 { let mut n = 0; for x in xs { n += x; } n }
fn real_b(ys: &[i32]) -> i32 { let mut m = 0; for y in ys { m += y; } m }
#[test]
fn t_one() { assert_eq!(real_a(&[1]), 1); }
#[test]
fn t_two() { assert_eq!(real_b(&[2]), 2); }
#[cfg(test)]
mod tests {
fn helper_a(v: &[i32]) -> i32 { let mut n = 0; for x in v { n += x; } n }
fn helper_b(w: &[i32]) -> i32 { let mut m = 0; for y in w { m += y; } m }
}
";
fn names(src: &str) -> Vec<String> {
functions_in_source(src, Path::new("a.rs"))
.expect("test source must parse")
.into_iter()
.map(|f| f.location.name)
.collect()
}
#[test]
fn test_functions_are_not_extracted() {
let found = names(MIXED);
assert!(
!found.contains(&"t_one".to_string()),
"#[test] fn: {found:?}"
);
assert!(
!found.contains(&"t_two".to_string()),
"#[test] fn: {found:?}"
);
}
#[test]
fn cfg_test_modules_are_not_extracted() {
let found = names(MIXED);
assert!(
!found.contains(&"helper_a".to_string()),
"#[cfg(test)] mod: {found:?}"
);
assert!(
!found.contains(&"helper_b".to_string()),
"#[cfg(test)] mod: {found:?}"
);
}
#[test]
fn non_test_functions_are_still_extracted() {
let found = names(MIXED);
assert!(
found.contains(&"real_a".to_string()),
"production fn: {found:?}"
);
assert!(
found.contains(&"real_b".to_string()),
"production fn: {found:?}"
);
assert_eq!(
found.len(),
2,
"exactly the production functions: {found:?}"
);
}
#[test]
fn methods_in_a_test_module_are_not_extracted() {
let found =
names("#[cfg(test)] mod tests { struct S; impl S { fn m(&self) -> i32 { 1 } } }");
assert!(
found.is_empty(),
"nothing inside a test module counts: {found:?}"
);
}
#[test]
fn functions_inside_a_plain_module_are_extracted() {
let found = names("mod inner { fn buried(x: i32) -> i32 { x + 1 } }");
assert_eq!(
found,
vec!["buried".to_string()],
"a plain mod is descended into"
);
}
#[test]
fn nested_functions_are_extracted_in_their_own_right() {
let found = names("fn outer() { fn inner(x: i32) -> i32 { x + 1 } inner(1); }");
assert!(found.contains(&"inner".to_string()), "nested fn: {found:?}");
assert!(
found.contains(&"outer".to_string()),
"and its parent: {found:?}"
);
}
}