use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProjectType {
Rust,
NodeJs,
Python,
Go,
Java,
Unknown,
}
impl Default for ProjectType {
fn default() -> Self {
Self::Unknown
}
}
impl ProjectType {
pub fn test_command(&self) -> Option<&'static str> {
match self {
ProjectType::Rust => Some("cargo test"),
ProjectType::NodeJs => Some("npm test"),
ProjectType::Python => Some("pytest"),
ProjectType::Go => Some("go test ./..."),
ProjectType::Java => Some("mvn test"),
ProjectType::Unknown => None,
}
}
pub fn build_command(&self) -> Option<&'static str> {
match self {
ProjectType::Rust => Some("cargo build"),
ProjectType::NodeJs => Some("npm run build"),
ProjectType::Python => None, ProjectType::Go => Some("go build"),
ProjectType::Java => Some("mvn compile"),
ProjectType::Unknown => None,
}
}
pub fn typecheck_command(&self) -> Option<&'static str> {
match self {
ProjectType::Rust => Some("cargo check"),
ProjectType::NodeJs => Some("npx tsc --noEmit"),
ProjectType::Python => Some("mypy ."),
ProjectType::Go => Some("go vet ./..."),
ProjectType::Java => None,
ProjectType::Unknown => None,
}
}
pub fn lint_command(&self) -> Option<&'static str> {
match self {
ProjectType::Rust => Some("cargo clippy"),
ProjectType::NodeJs => Some("npm run lint"),
ProjectType::Python => Some("ruff check ."),
ProjectType::Go => Some("golint ./..."),
ProjectType::Java => Some("mvn checkstyle:check"),
ProjectType::Unknown => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifySuggestion {
pub modified_file: String,
pub project_type: ProjectType,
pub related_tests: Vec<String>,
pub commands: Vec<VerifyCommand>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifyCommand {
pub kind: VerifyKind,
pub command: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerifyKind {
Test,
Build,
TypeCheck,
Lint,
}
pub struct VerifyTool {
project_root: PathBuf,
project_type: ProjectType,
}
impl VerifyTool {
pub fn new(project_root: PathBuf) -> Self {
let project_type = Self::detect_project_type(&project_root);
Self {
project_root,
project_type,
}
}
pub fn detect_project_type(root: &Path) -> ProjectType {
if root.join("Cargo.toml").exists() {
return ProjectType::Rust;
}
if root.join("package.json").exists() {
return ProjectType::NodeJs;
}
if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
return ProjectType::Python;
}
if root.join("go.mod").exists() {
return ProjectType::Go;
}
if root.join("pom.xml").exists() || root.join("build.gradle").exists() {
return ProjectType::Java;
}
ProjectType::Unknown
}
pub fn project_type(&self) -> ProjectType {
self.project_type
}
pub fn infer_related_tests(&self, modified_file: &str) -> Vec<String> {
let path = PathBuf::from(modified_file);
let mut related_tests = Vec::new();
match self.project_type {
ProjectType::Rust => {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let integration_test = format!("tests/{}_test.rs", stem);
let module_test = path.parent()
.map(|p| p.join(format!("{}_test.rs", stem)))
.map(|p| p.to_string_lossy().to_string());
if self.project_root.join(&integration_test).exists() {
related_tests.push(integration_test);
}
if let Some(test) = module_test {
if self.project_root.join(&test).exists() {
related_tests.push(test);
}
}
let module_test_dir = format!("src/{}/tests.rs", stem);
if self.project_root.join(&module_test_dir).exists() {
related_tests.push(module_test_dir);
}
}
}
ProjectType::NodeJs => {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let test_patterns = vec![
format!("test/{}.spec.ts", stem),
format!("test/{}.test.ts", stem),
format!("tests/{}.spec.ts", stem),
format!("tests/{}.test.ts", stem),
format!("__tests__/{}.test.ts", stem),
format!("__tests__/{}.test.js", stem),
format!("{}.spec.ts", stem),
format!("{}.test.ts", stem),
];
for test_path in test_patterns {
let test_path_js = test_path.replace(".ts", ".js");
if self.project_root.join(&test_path).exists() {
related_tests.push(test_path);
} else if self.project_root.join(&test_path_js).exists() {
related_tests.push(test_path_js);
}
}
}
}
ProjectType::Python => {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let test_file = format!("test_{}.py", stem);
let tests_dir_file = format!("tests/test_{}.py", stem);
if self.project_root.join(&test_file).exists() {
related_tests.push(test_file);
}
if self.project_root.join(&tests_dir_file).exists() {
related_tests.push(tests_dir_file);
}
}
}
ProjectType::Go => {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if ext == "go" {
let test_file = format!("{}_test.go",
path.with_extension("").to_string_lossy());
if self.project_root.join(&test_file).exists() {
related_tests.push(test_file);
}
}
}
}
ProjectType::Java => {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let test_file = format!("src/test/java/{}Test.java", stem);
if self.project_root.join(&test_file).exists() {
related_tests.push(test_file);
}
}
}
ProjectType::Unknown => {}
}
related_tests
}
pub fn generate_suggestion(&self, modified_file: &str) -> VerifySuggestion {
let related_tests = self.infer_related_tests(modified_file);
let mut commands = Vec::new();
if let Some(cmd) = self.project_type.typecheck_command() {
commands.push(VerifyCommand {
kind: VerifyKind::TypeCheck,
command: cmd.to_string(),
description: Some("Type check the project".to_string()),
});
}
if let Some(cmd) = self.project_type.lint_command() {
commands.push(VerifyCommand {
kind: VerifyKind::Lint,
command: cmd.to_string(),
description: Some("Run linter".to_string()),
});
}
if !related_tests.is_empty() {
if let Some(test_cmd) = self.project_type.test_command() {
let specific_cmd = match self.project_type {
ProjectType::Rust => {
format!("cargo test --test {}",
related_tests[0].trim_end_matches(".rs"))
}
_ => test_cmd.to_string(),
};
commands.push(VerifyCommand {
kind: VerifyKind::Test,
command: specific_cmd,
description: Some(format!("Run related tests: {}",
related_tests.join(", "))),
});
}
} else if let Some(cmd) = self.project_type.test_command() {
commands.push(VerifyCommand {
kind: VerifyKind::Test,
command: cmd.to_string(),
description: Some("Run all tests".to_string()),
});
}
if let Some(cmd) = self.project_type.build_command() {
commands.push(VerifyCommand {
kind: VerifyKind::Build,
command: cmd.to_string(),
description: Some("Build the project".to_string()),
});
}
VerifySuggestion {
modified_file: modified_file.to_string(),
project_type: self.project_type,
related_tests,
commands,
}
}
pub fn get_all_commands(&self) -> Vec<VerifyCommand> {
let mut commands = Vec::new();
if let Some(cmd) = self.project_type.typecheck_command() {
commands.push(VerifyCommand {
kind: VerifyKind::TypeCheck,
command: cmd.to_string(),
description: Some("Type check the project".to_string()),
});
}
if let Some(cmd) = self.project_type.lint_command() {
commands.push(VerifyCommand {
kind: VerifyKind::Lint,
command: cmd.to_string(),
description: Some("Run linter".to_string()),
});
}
if let Some(cmd) = self.project_type.test_command() {
commands.push(VerifyCommand {
kind: VerifyKind::Test,
command: cmd.to_string(),
description: Some("Run all tests".to_string()),
});
}
if let Some(cmd) = self.project_type.build_command() {
commands.push(VerifyCommand {
kind: VerifyKind::Build,
command: cmd.to_string(),
description: Some("Build the project".to_string()),
});
}
commands
}
}
pub fn detect_project_type(root: &Path) -> ProjectType {
VerifyTool::detect_project_type(root)
}
pub fn infer_related_tests(root: &Path, modified_file: &str) -> Vec<String> {
let tool = VerifyTool::new(root.to_path_buf());
tool.infer_related_tests(modified_file)
}
pub fn generate_verify_suggestion(root: &Path, modified_file: &str) -> VerifySuggestion {
let tool = VerifyTool::new(root.to_path_buf());
tool.generate_suggestion(modified_file)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_detect_rust_project() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::Rust);
}
#[test]
fn test_detect_nodejs_project() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("package.json"), "{}").unwrap();
assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::NodeJs);
}
#[test]
fn test_detect_python_project() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("pyproject.toml"), "[project]\nname = \"test\"").unwrap();
assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::Python);
}
#[test]
fn test_detect_unknown_project() {
let temp_dir = TempDir::new().unwrap();
assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::Unknown);
}
#[test]
fn test_rust_test_inference() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
fs::create_dir(temp_dir.path().join("tests")).unwrap();
fs::write(temp_dir.path().join("tests/utils_test.rs"), "").unwrap();
let tool = VerifyTool::new(temp_dir.path().to_path_buf());
let tests = tool.infer_related_tests("src/utils.rs");
assert!(tests.contains(&"tests/utils_test.rs".to_string()));
}
#[test]
fn test_project_type_commands() {
assert_eq!(ProjectType::Rust.test_command(), Some("cargo test"));
assert_eq!(ProjectType::Rust.build_command(), Some("cargo build"));
assert_eq!(ProjectType::Rust.typecheck_command(), Some("cargo check"));
assert_eq!(ProjectType::NodeJs.test_command(), Some("npm test"));
assert_eq!(ProjectType::NodeJs.build_command(), Some("npm run build"));
assert_eq!(ProjectType::Python.test_command(), Some("pytest"));
assert_eq!(ProjectType::Python.build_command(), None);
}
#[test]
fn test_generate_suggestion() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
fs::create_dir(temp_dir.path().join("tests")).unwrap();
let tool = VerifyTool::new(temp_dir.path().to_path_buf());
let suggestion = tool.generate_suggestion("src/main.rs");
assert_eq!(suggestion.project_type, ProjectType::Rust);
assert_eq!(suggestion.modified_file, "src/main.rs");
assert!(!suggestion.commands.is_empty());
assert_eq!(suggestion.commands[0].kind, VerifyKind::TypeCheck);
}
}