use crate::models::{AnalysisContext, FactorCategory, ScoreFactor, Scope};
use syn::{Attribute, Item, ItemFn, ItemMod};
#[derive(Debug, Clone)]
pub struct ScopeFilterConfig {
pub ignore_test_functions: bool,
pub ignore_test_modules: bool,
pub ignore_tests_dir: bool,
pub ignore_examples_dir: bool,
pub ignore_benches_dir: bool,
pub additional_ignore_paths: Vec<String>,
pub test_context_score_mod: i32,
}
impl Default for ScopeFilterConfig {
fn default() -> Self {
Self {
ignore_test_functions: true,
ignore_test_modules: true,
ignore_tests_dir: true,
ignore_examples_dir: true,
ignore_benches_dir: true,
additional_ignore_paths: vec![],
test_context_score_mod: -100,
}
}
}
pub struct ScopeFilter {
config: ScopeFilterConfig,
path_patterns: Vec<glob::Pattern>,
}
impl ScopeFilter {
pub fn new(config: ScopeFilterConfig) -> Result<Self, glob::PatternError> {
let path_patterns = config
.additional_ignore_paths
.iter()
.map(|p| glob::Pattern::new(p))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
config,
path_patterns,
})
}
pub fn should_skip(&self, context: &AnalysisContext) -> bool {
if self.config.ignore_tests_dir && context.in_test_context {
return true;
}
if self.config.ignore_examples_dir && context.in_example {
return true;
}
if self.config.ignore_benches_dir && context.in_benchmark {
return true;
}
let path_str = context.file_path.to_string_lossy();
for pattern in &self.path_patterns {
if pattern.matches(&path_str) {
return true;
}
}
false
}
pub fn analyze(&self, context: &AnalysisContext) -> Vec<ScoreFactor> {
let mut factors = Vec::new();
if context.in_test_context {
factors.push(
ScoreFactor::new(
"test_context",
FactorCategory::Context,
self.config.test_context_score_mod,
"Code is in a test context (mock data expected)",
)
.with_evidence(format!("File: {}", context.file_path.display())),
);
}
if context.in_example {
factors.push(ScoreFactor::new(
"example_context",
FactorCategory::Context,
-80,
"Code is in an example (demonstration data expected)",
));
}
if context.in_benchmark {
factors.push(ScoreFactor::new(
"benchmark_context",
FactorCategory::Context,
-80,
"Code is in a benchmark (test data expected)",
));
}
factors
}
pub fn is_test_attribute(attr: &Attribute) -> bool {
if attr.path().is_ident("test") {
return true;
}
if let Some(last) = attr.path().segments.last() {
if last.ident == "test" {
return true;
}
}
if attr.path().is_ident("cfg") {
if let Ok(meta) = attr.meta.require_list() {
let tokens = meta.tokens.to_string();
if tokens.contains("test") {
return true;
}
}
}
false
}
pub fn is_test_function(func: &ItemFn) -> bool {
func.attrs.iter().any(Self::is_test_attribute)
}
pub fn is_test_module(module: &ItemMod) -> bool {
module.attrs.iter().any(Self::is_test_attribute)
}
pub fn enter_scope(context: &mut AnalysisContext, item: &Item) {
match item {
Item::Mod(m) => {
let name = m.ident.to_string();
if Self::is_test_module(m) || name == "tests" || name == "test" {
context.push_scope(Scope::TestModule);
} else {
context.push_scope(Scope::Module(name));
}
}
Item::Fn(f) => {
let name = f.sig.ident.to_string();
if Self::is_test_function(f) {
context.push_scope(Scope::TestFunction);
} else {
context.current_function = Some(name.clone());
context.push_scope(Scope::Function(name));
}
}
Item::Impl(i) => {
let type_name = quote::quote!(#i.self_ty).to_string();
let trait_name = i
.trait_
.as_ref()
.map(|(_, path, _)| quote::quote!(#path).to_string());
context.push_scope(Scope::Impl(crate::models::context::ImplContext {
type_name,
trait_name,
}));
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_detects_test_attribute() {
let func: ItemFn = parse_quote! {
#[test]
fn test_something() {}
};
assert!(ScopeFilter::is_test_function(&func));
}
#[test]
fn test_detects_tokio_test() {
let func: ItemFn = parse_quote! {
#[tokio::test]
async fn test_async() {}
};
assert!(ScopeFilter::is_test_function(&func));
}
#[test]
fn test_detects_cfg_test_module() {
let module: ItemMod = parse_quote! {
#[cfg(test)]
mod tests {
fn helper() {}
}
};
assert!(ScopeFilter::is_test_module(&module));
}
}