use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
use dashmap::DashMap;
use glob::Pattern;
use crate::discovery::PreFilter;
use crate::error::{Result, SecretraceError};
pub type AstCache = Arc<DashMap<PathBuf, (u64, syn::File)>>;
pub fn new_ast_cache() -> AstCache {
Arc::new(DashMap::new())
}
pub struct FileWalker {
root: PathBuf,
ignore_patterns: Vec<Pattern>,
only_patterns: Vec<Pattern>,
include_tests: bool,
include_examples: bool,
follow_symlinks: bool,
pre_filter: PreFilter,
}
impl FileWalker {
pub fn new(root: impl AsRef<Path>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
ignore_patterns: Vec::new(),
only_patterns: Vec::new(),
include_tests: false,
include_examples: false,
follow_symlinks: false,
pre_filter: PreFilter::new(),
}
}
pub fn with_ignore_patterns(mut self, patterns: &[String]) -> Self {
for pattern in patterns {
if let Ok(p) = Pattern::new(pattern) {
self.ignore_patterns.push(p);
}
}
self
}
pub fn with_only_patterns(mut self, patterns: &[String]) -> Self {
for pattern in patterns {
if let Ok(p) = Pattern::new(pattern) {
self.only_patterns.push(p);
}
}
self
}
pub fn with_include_tests(mut self, include: bool) -> Self {
self.include_tests = include;
self
}
pub fn with_include_examples(mut self, include: bool) -> Self {
self.include_examples = include;
self
}
pub fn with_follow_symlinks(mut self, follow: bool) -> Self {
self.follow_symlinks = follow;
self
}
pub fn with_pre_filter(mut self, filter: PreFilter) -> Self {
self.pre_filter = filter;
self
}
pub fn walk(&self) -> Result<Vec<PathBuf>> {
let root = self.root.canonicalize().map_err(|e| SecretraceError::FileRead {
path: self.root.clone(),
source: e,
})?;
let files: Vec<PathBuf> = jwalk::WalkDir::new(&root)
.follow_links(self.follow_symlinks)
.into_iter()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().is_file())
.map(|entry| entry.path())
.filter(|path| self.should_include(path))
.collect();
Ok(files)
}
fn should_include(&self, path: &Path) -> bool {
if path.extension().map(|e| e != "rs").unwrap_or(true) {
return false;
}
if self.pre_filter.should_skip_path(path) {
return false;
}
let path_str = path.to_string_lossy();
for pattern in &self.ignore_patterns {
if pattern.matches(&path_str) {
return false;
}
}
if !self.only_patterns.is_empty() {
let matches_any = self.only_patterns.iter().any(|p| p.matches(&path_str));
if !matches_any {
return false;
}
}
if !self.include_tests && self.is_test_path(path) {
return false;
}
if !self.include_examples && self.is_example_path(path) {
return false;
}
true
}
fn is_test_path(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
path_str.contains("/tests/")
|| path_str.contains("/test/")
|| path_str.ends_with("_test.rs")
|| path_str.ends_with("_tests.rs")
}
fn is_example_path(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
path_str.contains("/examples/")
|| path_str.contains("/example/")
}
}
pub fn get_or_parse(
cache: &AstCache,
path: &Path,
) -> Result<syn::File> {
let modified = std::fs::metadata(path)
.and_then(|m| m.modified())
.and_then(|t| Ok(t.duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)))
.unwrap_or(0);
if let Some(entry) = cache.get(path) {
let (cached_time, ref ast) = *entry;
if cached_time == modified {
return Ok(ast.clone());
}
}
let content = std::fs::read_to_string(path).map_err(|e| SecretraceError::FileRead {
path: path.to_path_buf(),
source: e,
})?;
let ast = syn::parse_file(&content).map_err(|e| SecretraceError::Parse {
path: path.to_path_buf(),
source: e,
})?;
cache.insert(path.to_path_buf(), (modified, ast.clone()));
Ok(ast)
}
pub fn clear_expired(cache: &AstCache) {
let mut to_remove = Vec::new();
for entry in cache.iter() {
let path = entry.key();
let (cached_time, _) = entry.value();
let current_time = std::fs::metadata(path)
.and_then(|m| m.modified())
.and_then(|t| Ok(t.duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)))
.unwrap_or(0);
if current_time != *cached_time {
to_remove.push(path.clone());
}
}
for path in to_remove {
cache.remove(&path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn create_test_tree() -> TempDir {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}").unwrap();
std::fs::write(dir.path().join("src/lib.rs"), "pub fn foo() {}").unwrap();
std::fs::create_dir_all(dir.path().join("tests")).unwrap();
std::fs::write(dir.path().join("tests/integration.rs"), "#[test] fn t() {}").unwrap();
std::fs::create_dir_all(dir.path().join("examples")).unwrap();
std::fs::write(dir.path().join("examples/basic.rs"), "fn main() {}").unwrap();
std::fs::create_dir_all(dir.path().join("target/debug")).unwrap();
std::fs::write(dir.path().join("target/debug/build.rs"), "// generated").unwrap();
dir
}
#[test]
fn test_walk_basic() {
let dir = create_test_tree();
let walker = FileWalker::new(dir.path());
let files = walker.walk().unwrap();
assert!(files.iter().any(|p| p.ends_with("main.rs")));
assert!(files.iter().any(|p| p.ends_with("lib.rs")));
assert!(!files.iter().any(|p| p.to_string_lossy().contains("tests")));
assert!(!files.iter().any(|p| p.to_string_lossy().contains("examples")));
assert!(!files.iter().any(|p| p.to_string_lossy().contains("target")));
}
#[test]
fn test_walk_with_tests() {
let dir = create_test_tree();
let walker = FileWalker::new(dir.path()).with_include_tests(true);
let files = walker.walk().unwrap();
assert!(files.iter().any(|p| p.to_string_lossy().contains("tests")));
}
#[test]
fn test_walk_with_examples() {
let dir = create_test_tree();
let walker = FileWalker::new(dir.path()).with_include_examples(true);
let files = walker.walk().unwrap();
assert!(files.iter().any(|p| p.to_string_lossy().contains("examples")));
}
#[test]
fn test_ignore_patterns() {
let dir = create_test_tree();
let walker = FileWalker::new(dir.path())
.with_ignore_patterns(&["**/lib.rs".to_string()]);
let files = walker.walk().unwrap();
assert!(files.iter().any(|p| p.ends_with("main.rs")));
assert!(!files.iter().any(|p| p.ends_with("lib.rs")));
}
#[test]
fn test_only_patterns() {
let dir = create_test_tree();
let walker = FileWalker::new(dir.path())
.with_only_patterns(&["**/main.rs".to_string()]);
let files = walker.walk().unwrap();
assert!(files.iter().any(|p| p.ends_with("main.rs")));
assert!(!files.iter().any(|p| p.ends_with("lib.rs")));
}
#[test]
fn test_ast_cache() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.rs");
std::fs::write(&file_path, "fn foo() {}").unwrap();
let cache = new_ast_cache();
let ast1 = get_or_parse(&cache, &file_path).unwrap();
assert_eq!(cache.len(), 1);
let ast2 = get_or_parse(&cache, &file_path).unwrap();
assert_eq!(cache.len(), 1);
assert_eq!(ast1.items.len(), ast2.items.len());
}
}