vsec 0.0.1

Detect secrets and in Rust codebases
Documentation
// src/filters/layer2_scope.rs

use crate::models::{AnalysisContext, FactorCategory, ScoreFactor, Scope};
use syn::{Attribute, Item, ItemFn, ItemMod};

/// Configuration for scope-based filtering
#[derive(Debug, Clone)]
pub struct ScopeFilterConfig {
    /// Ignore #[test] functions
    pub ignore_test_functions: bool,

    /// Ignore #[cfg(test)] modules
    pub ignore_test_modules: bool,

    /// Ignore files in /tests/ directory
    pub ignore_tests_dir: bool,

    /// Ignore files in /examples/ directory
    pub ignore_examples_dir: bool,

    /// Ignore files in /benches/ directory
    pub ignore_benches_dir: bool,

    /// Additional paths to ignore (glob patterns)
    pub additional_ignore_paths: Vec<String>,

    /// Score modifier for test context (if not fully ignored)
    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,
        }
    }
}

/// Scope-based filter
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,
        })
    }

    /// Check if we should skip analysis for the current context
    pub fn should_skip(&self, context: &AnalysisContext) -> bool {
        // Check directory-based rules
        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;
        }

        // Check additional paths
        let path_str = context.file_path.to_string_lossy();
        for pattern in &self.path_patterns {
            if pattern.matches(&path_str) {
                return true;
            }
        }

        false
    }

    /// Analyze context and return scoring factors
    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
    }

    /// Check if attributes indicate a test context
    pub fn is_test_attribute(attr: &Attribute) -> bool {
        // Check for #[test]
        if attr.path().is_ident("test") {
            return true;
        }

        // Check for #[tokio::test], #[async_std::test], etc.
        if let Some(last) = attr.path().segments.last() {
            if last.ident == "test" {
                return true;
            }
        }

        // Check for #[cfg(test)]
        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
    }

    /// Check if a function is a test function
    pub fn is_test_function(func: &ItemFn) -> bool {
        func.attrs.iter().any(Self::is_test_attribute)
    }

    /// Check if a module is a test module
    pub fn is_test_module(module: &ItemMod) -> bool {
        module.attrs.iter().any(Self::is_test_attribute)
    }

    /// Update context when entering a scope
    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));
    }
}