Skip to main content

matrixcode_core/tools/
verify.rs

1//! Verification suggestion system for code changes.
2//!
3//! This module provides automatic detection of project types and
4//! suggests relevant verification commands (tests, builds, type checks)
5//! after file modifications.
6
7use std::path::{Path, PathBuf};
8
9/// Supported project types for verification
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ProjectType {
12    /// Rust project (Cargo.toml)
13    Rust,
14    /// Node.js project (package.json)
15    NodeJs,
16    /// Python project (pyproject.toml or requirements.txt)
17    Python,
18    /// Go project (go.mod)
19    Go,
20    /// Java/Kotlin project (pom.xml or build.gradle)
21    Java,
22    /// Unknown or unsupported project type
23    Unknown,
24}
25
26impl Default for ProjectType {
27    fn default() -> Self {
28        Self::Unknown
29    }
30}
31
32impl ProjectType {
33    /// Returns the test command for this project type
34    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    /// Returns the build command for this project type
46    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, // Python doesn't have a standard build command
51            ProjectType::Go => Some("go build"),
52            ProjectType::Java => Some("mvn compile"),
53            ProjectType::Unknown => None,
54        }
55    }
56
57    /// Returns the type check command for this project type
58    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    /// Returns the lint command for this project type
70    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/// Verification suggestion generated after file modification
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct VerifySuggestion {
85    /// The modified file that triggered this suggestion
86    pub modified_file: String,
87    /// Detected project type
88    pub project_type: ProjectType,
89    /// Related test files that might be affected
90    pub related_tests: Vec<String>,
91    /// Suggested verification commands
92    pub commands: Vec<VerifyCommand>,
93}
94
95/// A single verification command with its type
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct VerifyCommand {
98    /// Type of verification
99    pub kind: VerifyKind,
100    /// The command to execute
101    pub command: String,
102    /// Optional description for the command
103    pub description: Option<String>,
104}
105
106/// Types of verification
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum VerifyKind {
109    /// Run tests
110    Test,
111    /// Build the project
112    Build,
113    /// Type checking
114    TypeCheck,
115    /// Linting
116    Lint,
117}
118
119/// Main verification tool for project detection and test inference
120pub struct VerifyTool {
121    /// Root directory of the project
122    project_root: PathBuf,
123    /// Detected project type
124    project_type: ProjectType,
125}
126
127impl VerifyTool {
128    /// Create a new VerifyTool with the given project root
129    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    /// Detect project type by checking for config files
138    pub fn detect_project_type(root: &Path) -> ProjectType {
139        // Check in order of specificity
140        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    /// Get the detected project type
159    pub fn project_type(&self) -> ProjectType {
160        self.project_type
161    }
162
163    /// Infer related test files for a given modified file
164    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                // Rust: src/xxx.rs -> tests/xxx_test.rs or src/xxx/test.rs
171                if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
172                    // Check for integration tests
173                    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                    // Also check for module tests directory
188                    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                // Node.js: lib/xxx.ts -> test/xxx.spec.ts or __tests__/xxx.test.ts
196                if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
197                    // Common test patterns
198                    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                        // Also check with .js extension
211                        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                // Python: xxx.py -> test_xxx.py
222                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                // Go: xxx.go -> xxx_test.go
236                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                // Java: Xxx.java -> src/test/java/XxxTest.java
248                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    /// Generate verification suggestion for a modified file
262    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        // Add type check command (fastest, run first)
267        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        // Add lint command
276        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        // Add test command
285        if !related_tests.is_empty() {
286            // If we found specific tests, suggest running those
287            if let Some(test_cmd) = self.project_type.test_command() {
288                let specific_cmd = match self.project_type {
289                    ProjectType::Rust => {
290                        // For Rust, we can run specific test file
291                        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            // No specific tests found, suggest running all tests
305            commands.push(VerifyCommand {
306                kind: VerifyKind::Test,
307                command: cmd.to_string(),
308                description: Some("Run all tests".to_string()),
309            });
310        }
311
312        // Add build command
313        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    /// Get all available verification commands for the project
330    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
369/// Quick detection function for external use
370pub fn detect_project_type(root: &Path) -> ProjectType {
371    VerifyTool::detect_project_type(root)
372}
373
374/// Quick test inference function for external use
375pub 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
380/// Generate a verification suggestion for a file modification
381pub 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        // Check that typecheck is first (fastest)
458        assert_eq!(suggestion.commands[0].kind, VerifyKind::TypeCheck);
459    }
460}