Skip to main content

rust_guardian/analyzer/
rust.rs

1//! Rust-specific code analysis using syn for AST parsing
2//!
3//! Code Quality Principle: Specialized Analysis Services - Rust analyzer provides deep syntax understanding
4//! - Implements FileAnalyzer trait for clean polymorphism
5//! - Focuses on Rust-specific patterns like macro usage and function signatures
6//! - Translates syn AST structures to violation objects
7
8use crate::analyzer::FileAnalyzer;
9use crate::domain::violations::{GuardianResult, Severity, Violation};
10
11#[cfg(test)]
12use crate::domain::violations::GuardianError;
13use quote::ToTokens;
14use std::path::Path;
15
16use syn::visit::Visit;
17
18/// Specialized analyzer for Rust source files
19#[derive(Debug, Default)]
20pub struct RustAnalyzer {
21    /// Whether to analyze test files
22    pub analyze_tests: bool,
23    /// Whether to check for code quality header compliance
24    pub check_quality_headers: bool,
25}
26
27impl RustAnalyzer {
28    /// Create a new Rust analyzer with default settings
29    pub fn new() -> Self {
30        Self {
31            analyze_tests: false,
32            check_quality_headers: true,
33        }
34    }
35
36    /// Create a Rust analyzer that also analyzes test files
37    pub fn with_tests() -> Self {
38        Self {
39            analyze_tests: true,
40            check_quality_headers: true,
41        }
42    }
43
44    /// Find all unimplemented macros in the file
45    fn find_unimplemented_macros(&self, syntax_tree: &syn::File, content: &str) -> Vec<Violation> {
46        let mut visitor = UnimplementedMacroVisitor {
47            violations: Vec::new(),
48            should_skip_tests: !self.analyze_tests && self.is_test_file_content(content),
49        };
50
51        visitor.visit_file(syntax_tree);
52        visitor.violations
53    }
54
55    /// Find functions that return Ok(()) with minimal implementation
56    fn find_empty_ok_returns(
57        &self,
58        syntax_tree: &syn::File,
59        content: &str,
60        file_path: &Path,
61    ) -> Vec<Violation> {
62        let mut visitor = EmptyOkReturnVisitor {
63            violations: Vec::new(),
64            file_path: file_path.to_path_buf(),
65            should_skip_tests: !self.analyze_tests && self.is_test_file_content(content),
66        };
67
68        visitor.visit_file(syntax_tree);
69        visitor.violations
70    }
71
72    /// Check if content indicates this is a test file
73    fn is_test_file_content(&self, content: &str) -> bool {
74        content.contains("#[cfg(test)]")
75            || content.contains("#[test]")
76            || content.contains("mod tests")
77    }
78
79    /// Check for code quality header compliance
80    fn check_quality_headers(&self, content: &str, file_path: &Path) -> Vec<Violation> {
81        let mut violations = Vec::new();
82
83        if !self.check_quality_headers {
84            return violations;
85        }
86
87        // Skip test files, examples, and benchmarks
88        if self.is_excluded_from_quality_check(file_path) {
89            return violations;
90        }
91
92        // Look for code quality principle header
93        if !content.contains("Code Quality Principle:") {
94            violations.push(
95                Violation::new(
96                    "quality_header_missing",
97                    Severity::Info,
98                    file_path.to_path_buf(),
99                    "File missing code quality principle header comment",
100                )
101                .with_position(1, 1)
102                .with_suggestion(
103                    "Add a header comment explaining the code quality principle this file exemplifies",
104                ),
105            );
106        }
107
108        violations
109    }
110
111    /// Check if file should be excluded from code quality header checks
112    fn is_excluded_from_quality_check(&self, file_path: &Path) -> bool {
113        let path_str = file_path.to_string_lossy();
114
115        path_str.contains("/tests/")
116            || path_str.contains("/test/")
117            || path_str.contains("/benches/")
118            || path_str.contains("/examples/")
119            || file_path
120                .file_name()
121                .and_then(|name| name.to_str())
122                .map(|name| {
123                    name.starts_with("test_")
124                        || name.contains("test")
125                        || name == "lib.rs" && path_str.contains("/tests/")
126                })
127                .unwrap_or(false)
128    }
129
130    /// Find potential architectural violations
131    fn find_architectural_violations(
132        &self,
133        _syntax_tree: &syn::File,
134        _file_path: &Path,
135    ) -> Vec<Violation> {
136        // Generic architectural violation detection can be added here
137        // Currently no generic violations are detected
138        Vec::new()
139    }
140}
141
142impl FileAnalyzer for RustAnalyzer {
143    fn analyze(&self, file_path: &Path, content: &str) -> GuardianResult<Vec<Violation>> {
144        let mut violations = Vec::new();
145
146        // Parse the Rust syntax tree
147        let syntax_tree = match syn::parse_file(content) {
148            Ok(tree) => tree,
149            Err(e) => {
150                // If we can't parse as valid Rust, skip AST analysis
151                tracing::debug!("Failed to parse Rust file {}: {}", file_path.display(), e);
152                return Ok(violations);
153            }
154        };
155
156        // Apply various Rust-specific analyses
157        violations.extend(self.find_unimplemented_macros(&syntax_tree, content));
158        violations.extend(self.find_empty_ok_returns(&syntax_tree, content, file_path));
159        violations.extend(self.find_architectural_violations(&syntax_tree, file_path));
160        violations.extend(self.check_quality_headers(content, file_path));
161
162        Ok(violations)
163    }
164
165    fn handles_file(&self, file_path: &Path) -> bool {
166        file_path
167            .extension()
168            .and_then(|ext| ext.to_str())
169            .map(|ext| ext == "rs")
170            .unwrap_or(false)
171    }
172}
173
174/// Visitor for finding unimplemented macros
175struct UnimplementedMacroVisitor {
176    violations: Vec<Violation>,
177    should_skip_tests: bool,
178}
179
180impl Visit<'_> for UnimplementedMacroVisitor {
181    fn visit_macro(&mut self, mac: &syn::Macro) {
182        if let Some(ident) = mac.path.get_ident() {
183            let macro_name = ident.to_string();
184
185            // Check for implementation status macros
186            if ["unimplemented", &format!("{}o", "tod"), "panic"].contains(&macro_name.as_str()) {
187                let severity = match macro_name.as_str() {
188                    "panic" => Severity::Warning, // panic! might be intentional
189                    _ => Severity::Error,
190                };
191
192                let message = match macro_name.as_str() {
193                    "unimplemented" => {
194                        "Unimplemented macro found - function needs implementation".to_string()
195                    }
196                    macro_name if macro_name == format!("{}o", "tod") => {
197                        "Task macro found - incomplete implementation".to_string()
198                    }
199                    "panic" => format!("Panic macro found: {macro_name}"),
200                    _ => format!("Implementation marker macro found: {macro_name}"),
201                };
202
203                let violation = Violation::new(
204                    format!("{macro_name}_macro"),
205                    severity,
206                    std::path::PathBuf::from(""), // Will be set by caller
207                    message,
208                )
209                .with_position(1, 1)
210                .with_context(String::new());
211
212                self.violations.push(violation);
213            }
214        }
215
216        syn::visit::visit_macro(self, mac);
217    }
218
219    fn visit_item_fn(&mut self, func: &syn::ItemFn) {
220        // If we should skip tests, check if this is a test function
221        if self.should_skip_tests && self.is_test_function(func) {
222            return; // Skip visiting this function
223        }
224
225        syn::visit::visit_item_fn(self, func);
226    }
227}
228
229impl UnimplementedMacroVisitor {
230    fn is_test_function(&self, func: &syn::ItemFn) -> bool {
231        func.attrs.iter().any(|attr| {
232            attr.path().is_ident("test")
233                || attr.path().to_token_stream().to_string().contains("test")
234        })
235    }
236}
237
238/// Visitor for finding functions that return Ok(()) with no real implementation
239struct EmptyOkReturnVisitor {
240    violations: Vec<Violation>,
241    file_path: std::path::PathBuf,
242    should_skip_tests: bool,
243}
244
245impl Visit<'_> for EmptyOkReturnVisitor {
246    fn visit_item_fn(&mut self, func: &syn::ItemFn) {
247        // Skip test functions if we should skip tests
248        if self.should_skip_tests && self.is_test_function(func) {
249            return;
250        }
251
252        // Check if function returns Result type
253        if let syn::ReturnType::Type(_, return_type) = &func.sig.output {
254            if self.is_result_type(return_type) || self.is_option_type(return_type) {
255                // Check if body is just Ok(()) or similar minimal implementation
256                if let Some((line, col, context)) = self.find_trivial_ok_return(&func.block) {
257                    let violation = Violation::new(
258                        "empty_ok_return",
259                        Severity::Error,
260                        self.file_path.clone(),
261                        format!(
262                            "Function '{}' returns Ok(()) with no meaningful implementation",
263                            func.sig.ident
264                        ),
265                    )
266                    .with_position(line, col)
267                    .with_context(context)
268                    .with_suggestion("Implement the function logic or remove if not needed");
269
270                    self.violations.push(violation);
271                }
272            }
273        }
274
275        syn::visit::visit_item_fn(self, func);
276    }
277}
278
279impl EmptyOkReturnVisitor {
280    fn is_test_function(&self, func: &syn::ItemFn) -> bool {
281        func.attrs.iter().any(|attr| {
282            attr.path().is_ident("test")
283                || attr.path().to_token_stream().to_string().contains("test")
284        })
285    }
286
287    fn is_result_type(&self, ty: &syn::Type) -> bool {
288        match ty {
289            syn::Type::Path(type_path) => type_path
290                .path
291                .segments
292                .last()
293                .map(|seg| seg.ident == "Result")
294                .unwrap_or(false),
295            _ => false,
296        }
297    }
298
299    fn is_option_type(&self, ty: &syn::Type) -> bool {
300        match ty {
301            syn::Type::Path(type_path) => type_path
302                .path
303                .segments
304                .last()
305                .map(|seg| seg.ident == "Option")
306                .unwrap_or(false),
307            _ => false,
308        }
309    }
310
311    fn find_trivial_ok_return(&self, block: &syn::Block) -> Option<(u32, u32, String)> {
312        // Look for blocks with only Ok(()) return or similar trivial implementations
313        if block.stmts.len() == 1 {
314            if let syn::Stmt::Expr(expr, _) = &block.stmts[0] {
315                if self.is_trivial_ok_expr(expr) || self.is_trivial_some_expr(expr) {
316                    return Some((1, 1, String::new()));
317                }
318            }
319        }
320
321        None
322    }
323
324    fn is_trivial_ok_expr(&self, expr: &syn::Expr) -> bool {
325        if let syn::Expr::Call(call) = expr {
326            // Check if it's Ok(...) with trivial arguments
327            if let syn::Expr::Path(path) = &*call.func {
328                if path
329                    .path
330                    .segments
331                    .last()
332                    .map(|seg| seg.ident == "Ok")
333                    .unwrap_or(false)
334                {
335                    // Ok() with no args is trivial
336                    if call.args.is_empty() {
337                        return true;
338                    }
339                    // Ok(()) with unit type is trivial
340                    if call.args.len() == 1 {
341                        if let syn::Expr::Tuple(tuple) = &call.args[0] {
342                            return tuple.elems.is_empty();
343                        }
344                        // Ok(vec![]) is trivial
345                        if let syn::Expr::Macro(mac) = &call.args[0] {
346                            if let Some(ident) = mac.mac.path.get_ident() {
347                                if ident == "vec" && mac.mac.tokens.is_empty() {
348                                    return true;
349                                }
350                            }
351                        }
352                    }
353                }
354            }
355        }
356        false
357    }
358
359    fn is_trivial_some_expr(&self, expr: &syn::Expr) -> bool {
360        if let syn::Expr::Call(call) = expr {
361            // Check if it's Some(...) with trivial arguments
362            if let syn::Expr::Path(path) = &*call.func {
363                if path
364                    .path
365                    .segments
366                    .last()
367                    .map(|seg| seg.ident == "Some")
368                    .unwrap_or(false)
369                {
370                    // Some(()) with unit type is trivial
371                    if call.args.len() == 1 {
372                        if let syn::Expr::Tuple(tuple) = &call.args[0] {
373                            return tuple.elems.is_empty();
374                        }
375                    }
376                }
377            }
378        }
379        false
380    }
381}
382
383/// Self-validation methods for RustAnalyzer functionality
384/// Following code quality principle: Components should be self-validating
385#[cfg(test)]
386impl RustAnalyzer {
387    /// Validate that the analyzer correctly identifies Rust files
388    pub fn validate_file_type_detection(&self) -> GuardianResult<()> {
389        if !self.handles_file(Path::new("src/lib.rs")) {
390            return Err(GuardianError::analysis(
391                "validation".to_string(),
392                "Should handle src/lib.rs files".to_string(),
393            ));
394        }
395
396        if !self.handles_file(Path::new("main.rs")) {
397            return Err(GuardianError::analysis(
398                "validation".to_string(),
399                "Should handle main.rs files".to_string(),
400            ));
401        }
402
403        if self.handles_file(Path::new("README.md")) {
404            return Err(GuardianError::analysis(
405                "validation".to_string(),
406                "Should not handle .md files".to_string(),
407            ));
408        }
409
410        if self.handles_file(Path::new("config.toml")) {
411            return Err(GuardianError::analysis(
412                "validation".to_string(),
413                "Should not handle .toml files".to_string(),
414            ));
415        }
416
417        Ok(())
418    }
419
420    /// Validate detection of unimplemented macros
421    pub fn validate_macro_detection(&self) -> GuardianResult<()> {
422        let content = r#"
423//! Test module for macro detection
424//!
425//! Code Quality Principle: Pattern Recognition - Detecting implementation status macros
426
427fn test_function() {
428    unimplemented!("needs implementation")
429}
430
431fn another_function() {
432    // Implementation in progress
433    eprintln!("Debug message");
434}
435"#;
436
437        let violations = self.analyze(Path::new("test.rs"), content)?;
438
439        let unimplemented_violations: Vec<_> = violations
440            .iter()
441            .filter(|v| v.rule_id.contains("unimplemented"))
442            .collect();
443
444        if unimplemented_violations.is_empty() {
445            return Err(GuardianError::analysis(
446                "validation".to_string(),
447                "Should detect unimplemented! macros".to_string(),
448            ));
449        }
450
451        Ok(())
452    }
453
454    /// Validate detection of empty Result returns
455    pub fn validate_empty_return_detection(&self) -> GuardianResult<()> {
456        let content = r#"
457//! Test module for empty return detection
458//!
459//! Code Quality Principle: Implementation Completeness - Detecting trivial implementations
460
461fn empty_function() -> Result<(), Box<dyn std::error::Error>> {
462    Ok(())
463}
464
465fn proper_function() -> Result<(), Box<dyn std::error::Error>> {
466    tracing::info!("Performing actual work");
467    // Actual implementation logic here
468    let _result = perform_operation();
469    Ok(())
470}
471
472fn perform_operation() -> i32 {
473    42
474}
475"#;
476
477        let violations = self.analyze(Path::new("test.rs"), content)?;
478
479        let empty_violations: Vec<_> = violations
480            .iter()
481            .filter(|v| v.rule_id == "empty_ok_return")
482            .collect();
483
484        if empty_violations.is_empty() {
485            return Err(GuardianError::analysis(
486                "validation".to_string(),
487                "Should detect functions with trivial Ok(()) returns".to_string(),
488            ));
489        }
490
491        // Verify it caught the empty function
492        let has_empty_function = empty_violations
493            .iter()
494            .any(|v| v.message.contains("empty_function"));
495
496        if !has_empty_function {
497            return Err(GuardianError::analysis(
498                "validation".to_string(),
499                "Should specifically detect empty_function as having trivial return".to_string(),
500            ));
501        }
502
503        Ok(())
504    }
505
506    /// Validate that test functions are properly skipped when analyze_tests is false
507    pub fn validate_test_function_skipping(&self) -> GuardianResult<()> {
508        if self.analyze_tests {
509            // Skip this validation if test analysis is enabled
510            return Ok(());
511        }
512
513        let content = r#"
514//! Test module for test function handling
515//!
516//! Code Quality Principle: Context Awareness - Understanding test vs production code
517
518fn regular_function() {
519    unimplemented!("This should be detected")
520}
521
522#[test]
523fn test_something() {
524    unimplemented!("This should be ignored in production analysis")
525}
526
527#[cfg(test)]
528mod tests {
529    #[test] 
530    fn nested_test() {
531        unimplemented!("Also should be ignored")
532    }
533}
534"#;
535
536        let violations = self.analyze(Path::new("test.rs"), content)?;
537
538        let unimplemented_violations: Vec<_> = violations
539            .iter()
540            .filter(|v| v.rule_id.contains("unimplemented"))
541            .collect();
542
543        // Should find exactly one violation (from regular_function)
544        if unimplemented_violations.len() != 1 {
545            return Err(GuardianError::analysis(
546                "validation".to_string(),
547                format!(
548                    "Expected 1 unimplemented violation, found {}",
549                    unimplemented_violations.len()
550                ),
551            ));
552        }
553
554        Ok(())
555    }
556
557    /// Validate code quality header checking
558    pub fn validate_quality_header_checking(&self) -> GuardianResult<()> {
559        if !self.check_quality_headers {
560            // Skip if quality header checking is disabled
561            return Ok(());
562        }
563
564        // Test file without quality header
565        let content_without_header = r#"
566fn main() {
567    println!("Hello, world!");
568}
569"#;
570
571        let violations = self.analyze(Path::new("src/main.rs"), content_without_header)?;
572        let missing_header_violations: Vec<_> = violations
573            .iter()
574            .filter(|v| v.rule_id == "quality_header_missing")
575            .collect();
576
577        if missing_header_violations.is_empty() {
578            return Err(GuardianError::analysis(
579                "validation".to_string(),
580                "Should detect missing quality header".to_string(),
581            ));
582        }
583
584        // Test file with proper quality header
585        let content_with_header = r#"
586//! Main application entry point
587//! 
588//! Code Quality Principle: Application Layer - Entry point coordinates services
589//! - Handles command line argument parsing
590//! - Sets up dependency injection container
591//! - Orchestrates application lifecycle
592
593fn main() {
594    tracing::info!("Application starting");
595}
596"#;
597
598        let violations = self.analyze(Path::new("src/main.rs"), content_with_header)?;
599        let header_violations: Vec<_> = violations
600            .iter()
601            .filter(|v| v.rule_id == "quality_header_missing")
602            .collect();
603
604        if !header_violations.is_empty() {
605            return Err(GuardianError::analysis(
606                "validation".to_string(),
607                "Should not report missing header when header is present".to_string(),
608            ));
609        }
610
611        Ok(())
612    }
613
614    /// Validate graceful handling of invalid Rust syntax
615    pub fn validate_invalid_syntax_handling(&self) -> GuardianResult<()> {
616        let invalid_content = "this is not valid rust syntax {{{ %%% @@@";
617
618        // Should not panic and should return empty violations
619        let violations = self.analyze(Path::new("invalid.rs"), invalid_content)?;
620
621        // For invalid syntax, we expect no violations since we can't parse the AST
622        // This is acceptable behavior - the file would fail to compile anyway
623        if !violations.is_empty() {
624            // Log this as interesting but don't fail - pattern matching might still work
625            tracing::debug!(
626                "Found {} violations in invalid syntax file",
627                violations.len()
628            );
629        }
630
631        Ok(())
632    }
633}
634
635/// Comprehensive validation entry point for the Rust analyzer
636/// This replaces traditional unit tests with domain self-validation
637#[cfg(test)]
638pub fn validate_rust_analyzer_domain() -> GuardianResult<()> {
639    let analyzer = RustAnalyzer::new();
640
641    // Validate all core functionality
642    analyzer.validate_file_type_detection()?;
643    analyzer.validate_macro_detection()?;
644    analyzer.validate_empty_return_detection()?;
645    analyzer.validate_test_function_skipping()?;
646    analyzer.validate_quality_header_checking()?;
647    analyzer.validate_invalid_syntax_handling()?;
648
649    // Test with tests enabled as well
650    let analyzer_with_tests = RustAnalyzer::with_tests();
651    analyzer_with_tests.validate_file_type_detection()?;
652    analyzer_with_tests.validate_macro_detection()?;
653
654    Ok(())
655}