1use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ProjectType {
12 Rust,
14 NodeJs,
16 Python,
18 Go,
20 Java,
22 Unknown,
24}
25
26impl Default for ProjectType {
27 fn default() -> Self {
28 Self::Unknown
29 }
30}
31
32impl ProjectType {
33 pub fn test_command(&self) -> Option<&'static str> {
35 match self {
36 ProjectType::Rust => Some("cargo test"),
37 ProjectType::NodeJs => Some("npm test"),
38 ProjectType::Python => Some("pytest"),
39 ProjectType::Go => Some("go test ./..."),
40 ProjectType::Java => Some("mvn test"),
41 ProjectType::Unknown => None,
42 }
43 }
44
45 pub fn build_command(&self) -> Option<&'static str> {
47 match self {
48 ProjectType::Rust => Some("cargo build"),
49 ProjectType::NodeJs => Some("npm run build"),
50 ProjectType::Python => None, ProjectType::Go => Some("go build"),
52 ProjectType::Java => Some("mvn compile"),
53 ProjectType::Unknown => None,
54 }
55 }
56
57 pub fn typecheck_command(&self) -> Option<&'static str> {
59 match self {
60 ProjectType::Rust => Some("cargo check"),
61 ProjectType::NodeJs => Some("npx tsc --noEmit"),
62 ProjectType::Python => Some("mypy ."),
63 ProjectType::Go => Some("go vet ./..."),
64 ProjectType::Java => None,
65 ProjectType::Unknown => None,
66 }
67 }
68
69 pub fn lint_command(&self) -> Option<&'static str> {
71 match self {
72 ProjectType::Rust => Some("cargo clippy"),
73 ProjectType::NodeJs => Some("npm run lint"),
74 ProjectType::Python => Some("ruff check ."),
75 ProjectType::Go => Some("golint ./..."),
76 ProjectType::Java => Some("mvn checkstyle:check"),
77 ProjectType::Unknown => None,
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct VerifySuggestion {
85 pub modified_file: String,
87 pub project_type: ProjectType,
89 pub related_tests: Vec<String>,
91 pub commands: Vec<VerifyCommand>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct VerifyCommand {
98 pub kind: VerifyKind,
100 pub command: String,
102 pub description: Option<String>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum VerifyKind {
109 Test,
111 Build,
113 TypeCheck,
115 Lint,
117}
118
119pub struct VerifyTool {
121 project_root: PathBuf,
123 project_type: ProjectType,
125}
126
127impl VerifyTool {
128 pub fn new(project_root: PathBuf) -> Self {
130 let project_type = Self::detect_project_type(&project_root);
131 Self {
132 project_root,
133 project_type,
134 }
135 }
136
137 pub fn detect_project_type(root: &Path) -> ProjectType {
139 if root.join("Cargo.toml").exists() {
141 return ProjectType::Rust;
142 }
143 if root.join("package.json").exists() {
144 return ProjectType::NodeJs;
145 }
146 if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
147 return ProjectType::Python;
148 }
149 if root.join("go.mod").exists() {
150 return ProjectType::Go;
151 }
152 if root.join("pom.xml").exists() || root.join("build.gradle").exists() {
153 return ProjectType::Java;
154 }
155 ProjectType::Unknown
156 }
157
158 pub fn project_type(&self) -> ProjectType {
160 self.project_type
161 }
162
163 pub fn infer_related_tests(&self, modified_file: &str) -> Vec<String> {
165 let path = PathBuf::from(modified_file);
166 let mut related_tests = Vec::new();
167
168 match self.project_type {
169 ProjectType::Rust => {
170 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
172 let integration_test = format!("tests/{}_test.rs", stem);
174 let module_test = path.parent()
175 .map(|p| p.join(format!("{}_test.rs", stem)))
176 .map(|p| p.to_string_lossy().to_string());
177
178 if self.project_root.join(&integration_test).exists() {
179 related_tests.push(integration_test);
180 }
181 if let Some(test) = module_test {
182 if self.project_root.join(&test).exists() {
183 related_tests.push(test);
184 }
185 }
186
187 let module_test_dir = format!("src/{}/tests.rs", stem);
189 if self.project_root.join(&module_test_dir).exists() {
190 related_tests.push(module_test_dir);
191 }
192 }
193 }
194 ProjectType::NodeJs => {
195 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
197 let test_patterns = vec![
199 format!("test/{}.spec.ts", stem),
200 format!("test/{}.test.ts", stem),
201 format!("tests/{}.spec.ts", stem),
202 format!("tests/{}.test.ts", stem),
203 format!("__tests__/{}.test.ts", stem),
204 format!("__tests__/{}.test.js", stem),
205 format!("{}.spec.ts", stem),
206 format!("{}.test.ts", stem),
207 ];
208
209 for test_path in test_patterns {
210 let test_path_js = test_path.replace(".ts", ".js");
212 if self.project_root.join(&test_path).exists() {
213 related_tests.push(test_path);
214 } else if self.project_root.join(&test_path_js).exists() {
215 related_tests.push(test_path_js);
216 }
217 }
218 }
219 }
220 ProjectType::Python => {
221 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
223 let test_file = format!("test_{}.py", stem);
224 let tests_dir_file = format!("tests/test_{}.py", stem);
225
226 if self.project_root.join(&test_file).exists() {
227 related_tests.push(test_file);
228 }
229 if self.project_root.join(&tests_dir_file).exists() {
230 related_tests.push(tests_dir_file);
231 }
232 }
233 }
234 ProjectType::Go => {
235 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
237 if ext == "go" {
238 let test_file = format!("{}_test.go",
239 path.with_extension("").to_string_lossy());
240 if self.project_root.join(&test_file).exists() {
241 related_tests.push(test_file);
242 }
243 }
244 }
245 }
246 ProjectType::Java => {
247 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
249 let test_file = format!("src/test/java/{}Test.java", stem);
250 if self.project_root.join(&test_file).exists() {
251 related_tests.push(test_file);
252 }
253 }
254 }
255 ProjectType::Unknown => {}
256 }
257
258 related_tests
259 }
260
261 pub fn generate_suggestion(&self, modified_file: &str) -> VerifySuggestion {
263 let related_tests = self.infer_related_tests(modified_file);
264 let mut commands = Vec::new();
265
266 if let Some(cmd) = self.project_type.typecheck_command() {
268 commands.push(VerifyCommand {
269 kind: VerifyKind::TypeCheck,
270 command: cmd.to_string(),
271 description: Some("Type check the project".to_string()),
272 });
273 }
274
275 if let Some(cmd) = self.project_type.lint_command() {
277 commands.push(VerifyCommand {
278 kind: VerifyKind::Lint,
279 command: cmd.to_string(),
280 description: Some("Run linter".to_string()),
281 });
282 }
283
284 if !related_tests.is_empty() {
286 if let Some(test_cmd) = self.project_type.test_command() {
288 let specific_cmd = match self.project_type {
289 ProjectType::Rust => {
290 format!("cargo test --test {}",
292 related_tests[0].trim_end_matches(".rs"))
293 }
294 _ => test_cmd.to_string(),
295 };
296 commands.push(VerifyCommand {
297 kind: VerifyKind::Test,
298 command: specific_cmd,
299 description: Some(format!("Run related tests: {}",
300 related_tests.join(", "))),
301 });
302 }
303 } else if let Some(cmd) = self.project_type.test_command() {
304 commands.push(VerifyCommand {
306 kind: VerifyKind::Test,
307 command: cmd.to_string(),
308 description: Some("Run all tests".to_string()),
309 });
310 }
311
312 if let Some(cmd) = self.project_type.build_command() {
314 commands.push(VerifyCommand {
315 kind: VerifyKind::Build,
316 command: cmd.to_string(),
317 description: Some("Build the project".to_string()),
318 });
319 }
320
321 VerifySuggestion {
322 modified_file: modified_file.to_string(),
323 project_type: self.project_type,
324 related_tests,
325 commands,
326 }
327 }
328
329 pub fn get_all_commands(&self) -> Vec<VerifyCommand> {
331 let mut commands = Vec::new();
332
333 if let Some(cmd) = self.project_type.typecheck_command() {
334 commands.push(VerifyCommand {
335 kind: VerifyKind::TypeCheck,
336 command: cmd.to_string(),
337 description: Some("Type check the project".to_string()),
338 });
339 }
340
341 if let Some(cmd) = self.project_type.lint_command() {
342 commands.push(VerifyCommand {
343 kind: VerifyKind::Lint,
344 command: cmd.to_string(),
345 description: Some("Run linter".to_string()),
346 });
347 }
348
349 if let Some(cmd) = self.project_type.test_command() {
350 commands.push(VerifyCommand {
351 kind: VerifyKind::Test,
352 command: cmd.to_string(),
353 description: Some("Run all tests".to_string()),
354 });
355 }
356
357 if let Some(cmd) = self.project_type.build_command() {
358 commands.push(VerifyCommand {
359 kind: VerifyKind::Build,
360 command: cmd.to_string(),
361 description: Some("Build the project".to_string()),
362 });
363 }
364
365 commands
366 }
367}
368
369pub fn detect_project_type(root: &Path) -> ProjectType {
371 VerifyTool::detect_project_type(root)
372}
373
374pub fn infer_related_tests(root: &Path, modified_file: &str) -> Vec<String> {
376 let tool = VerifyTool::new(root.to_path_buf());
377 tool.infer_related_tests(modified_file)
378}
379
380pub fn generate_verify_suggestion(root: &Path, modified_file: &str) -> VerifySuggestion {
382 let tool = VerifyTool::new(root.to_path_buf());
383 tool.generate_suggestion(modified_file)
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use std::fs;
390 use tempfile::TempDir;
391
392 #[test]
393 fn test_detect_rust_project() {
394 let temp_dir = TempDir::new().unwrap();
395 fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
396 assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::Rust);
397 }
398
399 #[test]
400 fn test_detect_nodejs_project() {
401 let temp_dir = TempDir::new().unwrap();
402 fs::write(temp_dir.path().join("package.json"), "{}").unwrap();
403 assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::NodeJs);
404 }
405
406 #[test]
407 fn test_detect_python_project() {
408 let temp_dir = TempDir::new().unwrap();
409 fs::write(temp_dir.path().join("pyproject.toml"), "[project]\nname = \"test\"").unwrap();
410 assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::Python);
411 }
412
413 #[test]
414 fn test_detect_unknown_project() {
415 let temp_dir = TempDir::new().unwrap();
416 assert_eq!(VerifyTool::detect_project_type(temp_dir.path()), ProjectType::Unknown);
417 }
418
419 #[test]
420 fn test_rust_test_inference() {
421 let temp_dir = TempDir::new().unwrap();
422 fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
423 fs::create_dir(temp_dir.path().join("tests")).unwrap();
424 fs::write(temp_dir.path().join("tests/utils_test.rs"), "").unwrap();
425
426 let tool = VerifyTool::new(temp_dir.path().to_path_buf());
427 let tests = tool.infer_related_tests("src/utils.rs");
428 assert!(tests.contains(&"tests/utils_test.rs".to_string()));
429 }
430
431 #[test]
432 fn test_project_type_commands() {
433 assert_eq!(ProjectType::Rust.test_command(), Some("cargo test"));
434 assert_eq!(ProjectType::Rust.build_command(), Some("cargo build"));
435 assert_eq!(ProjectType::Rust.typecheck_command(), Some("cargo check"));
436
437 assert_eq!(ProjectType::NodeJs.test_command(), Some("npm test"));
438 assert_eq!(ProjectType::NodeJs.build_command(), Some("npm run build"));
439
440 assert_eq!(ProjectType::Python.test_command(), Some("pytest"));
441 assert_eq!(ProjectType::Python.build_command(), None);
442 }
443
444 #[test]
445 fn test_generate_suggestion() {
446 let temp_dir = TempDir::new().unwrap();
447 fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
448 fs::create_dir(temp_dir.path().join("tests")).unwrap();
449
450 let tool = VerifyTool::new(temp_dir.path().to_path_buf());
451 let suggestion = tool.generate_suggestion("src/main.rs");
452
453 assert_eq!(suggestion.project_type, ProjectType::Rust);
454 assert_eq!(suggestion.modified_file, "src/main.rs");
455 assert!(!suggestion.commands.is_empty());
456
457 assert_eq!(suggestion.commands[0].kind, VerifyKind::TypeCheck);
459 }
460}