use std::path::{Path, PathBuf};
use walkdir::{DirEntry, WalkDir};
const VALID_EXTENSIONS: &[&str] = &["js", "jsx", "ts", "tsx"];
const IGNORED_DIRS: &[&str] = &[
"node_modules",
"dist",
"build",
"coverage",
".git",
"artifacts",
"storybook-static",
];
const TEST_DIRS: &[&str] = &["__tests__", "__mocks__", "__fixtures__", "e2e", "cypress"];
const TEST_SUFFIXES: &[&str] = &["test", "spec", "stories", "story"];
pub fn collect_files(root: &Path, include_tests: bool) -> Vec<PathBuf> {
let root: std::borrow::Cow<Path> = match root.canonicalize() {
Ok(abs) => std::borrow::Cow::Owned(abs),
Err(_) => std::borrow::Cow::Borrowed(root),
};
let root = root.as_ref();
if root.is_file() {
return if has_valid_extension(root) {
vec![root.to_path_buf()]
} else {
eprintln!(
"Warning: {} is not a JS/TS file — skipping.",
root.display()
);
vec![]
};
}
if !root.exists() {
eprintln!("Error: path '{}' does not exist.", root.display());
return vec![];
}
WalkDir::new(root)
.follow_links(false) .into_iter()
.filter_entry(|entry| !is_ignored_dir(entry, include_tests))
.filter_map(|result| match result {
Ok(entry) => {
if entry.file_type().is_file()
&& has_valid_extension(entry.path())
&& (include_tests || !is_test_file(entry.path()))
{
Some(entry.into_path())
} else {
None
}
}
Err(err) => {
eprintln!("Warning: could not read entry — {err}");
None
}
})
.collect()
}
fn is_ignored_dir(entry: &DirEntry, include_tests: bool) -> bool {
if !entry.file_type().is_dir() {
return false;
}
let dir_name = entry.file_name().to_str().unwrap_or("");
if dir_name.starts_with('.') {
return true;
}
if IGNORED_DIRS.contains(&dir_name) {
return true;
}
if !include_tests && TEST_DIRS.contains(&dir_name) {
return true;
}
false
}
fn is_test_file(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
return false;
};
let parts: Vec<&str> = name.split('.').collect();
if parts.len() <= 2 {
return false;
}
parts[1..parts.len() - 1]
.iter()
.any(|seg| TEST_SUFFIXES.contains(seg))
}
fn has_valid_extension(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| VALID_EXTENSIONS.contains(&ext))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_valid_extension() {
assert!(has_valid_extension(Path::new("App.tsx")));
assert!(has_valid_extension(Path::new("index.js")));
assert!(has_valid_extension(Path::new("Component.jsx")));
assert!(has_valid_extension(Path::new("types.ts")));
assert!(!has_valid_extension(Path::new("style.css")));
assert!(!has_valid_extension(Path::new("README.md")));
assert!(!has_valid_extension(Path::new("config.json")));
assert!(!has_valid_extension(Path::new("no_extension")));
}
#[test]
fn test_is_test_file() {
assert!(is_test_file(Path::new("Button.test.tsx")));
assert!(is_test_file(Path::new("Button.spec.tsx")));
assert!(is_test_file(Path::new("Button.stories.tsx")));
assert!(is_test_file(Path::new("Button.story.tsx")));
assert!(is_test_file(Path::new("useHook.test.ts")));
assert!(is_test_file(Path::new("utils.spec.js")));
assert!(is_test_file(Path::new("Card.stories.jsx")));
assert!(!is_test_file(Path::new("Button.tsx")));
assert!(!is_test_file(Path::new("useHook.ts")));
assert!(!is_test_file(Path::new("index.js")));
assert!(!is_test_file(Path::new("TestUtils.tsx"))); }
}