use crate::audits::context::model::LanguageKind;
use std::path::Path;
pub fn push_unique<T: PartialEq>(values: &mut Vec<T>, value: T) {
if !values.contains(&value) {
values.push(value);
}
}
pub fn normalize(value: &str) -> String {
value.trim().to_lowercase()
}
pub fn path_contains_component(path: &Path, targets: &[&str]) -> bool {
path.to_string_lossy().split(['/', '\\']).any(|component| {
let normalized = normalize(component);
targets.iter().any(|target| normalized == *target)
})
}
pub fn is_pascal_case(value: &str) -> bool {
value
.chars()
.next()
.map(|character| character.is_uppercase())
.unwrap_or(false)
}
pub fn is_js_or_ts(language: LanguageKind) -> bool {
matches!(
language,
LanguageKind::TypeScript | LanguageKind::JavaScript
)
}
pub fn is_test_file(path: &Path) -> bool {
let path_text = path.to_string_lossy().to_lowercase();
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_lowercase())
.unwrap_or_default();
path_text.starts_with("tests/")
|| path_text.starts_with("tests\\")
|| path_text.contains("/tests/")
|| path_text.contains("\\tests\\")
|| path_text.starts_with("fixtures/")
|| path_text.starts_with("fixtures\\")
|| path_text.contains("/fixtures/")
|| path_text.contains("\\fixtures\\")
|| path_text.contains("/__tests__/")
|| path_text.contains("\\__tests__\\")
|| file_name.ends_with(".test.ts")
|| file_name.ends_with(".test.tsx")
|| file_name.ends_with(".test.js")
|| file_name.ends_with(".test.jsx")
|| file_name.ends_with(".spec.ts")
|| file_name.ends_with(".spec.tsx")
|| file_name.ends_with(".spec.js")
|| file_name.ends_with(".spec.jsx")
|| file_name.ends_with("_test.go")
|| file_name.ends_with("_test.py")
|| file_name.ends_with("test.java")
|| file_name.ends_with("tests.java")
|| file_name.ends_with("test.kt")
|| file_name.ends_with("tests.kt")
|| file_name.ends_with("test.cs")
|| file_name.ends_with("tests.cs")
|| (file_name.starts_with("test_") && !file_name.ends_with(".rs"))
|| file_name == "tests.rs"
|| file_name.ends_with("_tests.rs")
|| file_name.starts_with("tests_")
}
pub fn is_config_file(path: &Path) -> bool {
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.map(normalize)
.unwrap_or_default();
matches!(
file_name.as_str(),
"package.json"
| "tsconfig.json"
| "vite.config.ts"
| "vite.config.js"
| "next.config.js"
| "next.config.mjs"
| "cargo.toml"
| "cargo.lock"
| "projectsettings.asset"
| "dockerfile"
| "containerfile"
| "go.mod"
| "go.sum"
| "pyproject.toml"
| "requirements.txt"
| "build.gradle"
| "settings.gradle"
| "pom.xml"
) || (file_name.starts_with("appsettings") && file_name.ends_with(".json"))
}
pub fn is_generated_file(path: &Path, content: &str) -> bool {
path_contains_component(
path,
&[
"generated",
"__generated__",
"gen",
"codegen",
"target",
"build",
],
) || content.contains("@generated")
|| content.contains("code generated")
|| content.contains("Code generated")
|| content.contains("Code Generated")
|| content.contains("generated by")
|| content.contains("Generated by")
|| content.contains("Generated By")
}
pub fn is_app_entrypoint(path: &Path, content: &str, language: LanguageKind) -> bool {
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.map(normalize)
.unwrap_or_default();
matches!(
file_name.as_str(),
"main.rs"
| "build.rs"
| "main.go"
| "main.py"
| "app.py"
| "program.cs"
| "main.java"
| "main.kt"
| "index.ts"
| "index.js"
| "index.tsx"
| "index.jsx"
| "main.ts"
| "main.js"
| "main.tsx"
| "main.jsx"
) || (language == LanguageKind::Python && content.contains("if __name__ == \"__main__\""))
|| (language == LanguageKind::Go && content.contains("func main("))
|| (language == LanguageKind::Rust && content.contains("fn main("))
}
#[cfg(test)]
mod tests {
use super::{is_app_entrypoint, is_test_file, path_contains_component};
use crate::audits::context::model::LanguageKind;
use std::path::Path;
#[test]
fn entrypoints_recognized_by_filename_without_content() {
assert!(is_app_entrypoint(
Path::new("build.rs"),
"",
LanguageKind::Rust
));
assert!(is_app_entrypoint(
Path::new("src/main.tsx"),
"",
LanguageKind::TypeScript
));
assert!(is_app_entrypoint(
Path::new("src/index.tsx"),
"",
LanguageKind::TypeScript
));
assert!(!is_app_entrypoint(
Path::new("src/state.rs"),
"",
LanguageKind::Rust
));
}
#[test]
fn path_component_matching_handles_windows_separators() {
assert!(path_contains_component(
Path::new(r"tools\scripts\check.js"),
&["scripts"],
));
assert!(path_contains_component(
Path::new(r"src\domain\model.rs"),
&["domain"],
));
}
#[test]
fn test_classification_covers_rust_test_modules_and_fixtures() {
assert!(is_test_file(Path::new("src/behavioral_tests.rs")));
assert!(is_test_file(Path::new("tests/fixtures/runtime/client.rs")));
assert!(is_test_file(Path::new(r"fixtures\runtime\client.rs")));
assert!(is_test_file(Path::new("src/audits/foo/tests.rs")));
assert!(is_test_file(Path::new(
"src/audits/code_quality/rust_panic_risk/tests_render.rs"
)));
}
#[test]
fn inline_tests_do_not_make_a_production_file_a_test_file() {
assert!(!is_test_file(Path::new(
"src/audits/code_quality/complexity.rs"
)));
assert!(!is_test_file(Path::new("src/scan/cache.rs")));
}
}