Skip to main content

rust_guardian/patterns/
mod.rs

1//! Pattern engine for detecting code quality violations
2//!
3//! Architectural Principle: Service Layer - Pattern matching orchestrates complex analysis operations
4//! - PatternEngine coordinates different types of pattern matching (regex, AST, semantic)
5//! - Each pattern type implements the PatternMatcher trait for clean polymorphism
6//! - Pattern results are translated to quality violations at the boundary
7
8pub mod path_filter;
9
10use crate::config::{ExcludeConditions, PatternRule, RuleType};
11use crate::domain::violations::{GuardianError, GuardianResult, Severity, Violation};
12use regex::{Regex, RegexBuilder};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use syn::spanned::Spanned;
16
17pub use path_filter::PathFilter;
18
19/// Core pattern engine that coordinates different types of pattern matching
20#[derive(Debug)]
21pub struct PatternEngine {
22    /// Compiled regex patterns for fast matching
23    regex_patterns: HashMap<String, CompiledRegex>,
24    /// AST patterns for semantic analysis
25    ast_patterns: HashMap<String, AstPattern>,
26}
27
28/// A compiled regex pattern with metadata
29#[derive(Debug)]
30struct CompiledRegex {
31    regex: Regex,
32    rule_id: String,
33    message_template: String,
34    severity: Severity,
35    exclude_conditions: Option<ExcludeConditions>,
36}
37
38/// An AST pattern for structural code analysis
39#[derive(Debug)]
40struct AstPattern {
41    pattern_type: AstPatternType,
42    rule_id: String,
43    message_template: String,
44    severity: Severity,
45    exclude_conditions: Option<ExcludeConditions>,
46}
47
48/// Types of AST patterns we can detect
49#[derive(Debug, Clone)]
50enum AstPatternType {
51    /// Look for specific macro calls (unimplemented!, todo!, panic!)
52    MacroCall(Vec<String>),
53    /// Look for functions that return Ok(()) with no meaningful implementation
54    EmptyOkReturn,
55    /// Look for missing architectural headers in files
56    MissingArchitecturalHeader,
57    /// Look for functions with empty bodies
58    EmptyFunctionBody,
59    /// Look for unwrap() or expect() calls without meaningful error messages
60    UnwrapOrExpectWithoutMessage,
61    /// Look for abstraction layer violations (semantic pattern)
62    AbstractionLayerViolation(regex::Regex),
63    /// Advanced semantic patterns
64    CyclomaticComplexity(u32),
65    PublicWithoutDocs,
66    FunctionLinesGt(u32),
67    NestingDepthGt(u32),
68    FunctionArgsGt(u32),
69    BlockingCallInAsync,
70    FutureNotAwaited,
71    SelectWithoutBiased,
72    GenericWithoutBounds,
73    TestFnWithoutAssertion,
74    ImplWithoutTrait,
75    UnsafeBlock,
76    IgnoredTestAttribute,
77}
78
79/// A match found by a pattern
80#[derive(Debug)]
81pub struct PatternMatch {
82    pub rule_id: String,
83    pub file_path: PathBuf,
84    pub line_number: Option<u32>,
85    pub column_number: Option<u32>,
86    pub matched_text: String,
87    pub message: String,
88    pub severity: Severity,
89    pub context: Option<String>,
90}
91
92impl PatternEngine {
93    /// Create a new pattern engine
94    pub fn new() -> Self {
95        Self {
96            regex_patterns: HashMap::new(),
97            ast_patterns: HashMap::new(),
98        }
99    }
100
101    /// Add a pattern rule to the engine
102    pub fn add_rule(
103        &mut self,
104        rule: &PatternRule,
105        effective_severity: Severity,
106    ) -> GuardianResult<()> {
107        tracing::debug!(
108            "Adding rule '{}' of type {:?} with pattern '{}' and severity {:?}",
109            rule.id,
110            rule.rule_type,
111            rule.pattern,
112            effective_severity
113        );
114
115        match rule.rule_type {
116            RuleType::Regex => {
117                tracing::debug!(
118                    "Compiling regex pattern '{}' for rule '{}'",
119                    rule.pattern,
120                    rule.id
121                );
122                let regex = if rule.case_sensitive {
123                    Regex::new(&rule.pattern)
124                } else {
125                    RegexBuilder::new(&rule.pattern)
126                        .case_insensitive(true)
127                        .build()
128                }
129                .map_err(|e| {
130                    GuardianError::pattern(format!("Invalid regex '{}': {}", rule.pattern, e))
131                })?;
132
133                self.regex_patterns.insert(
134                    rule.id.clone(),
135                    CompiledRegex {
136                        regex,
137                        rule_id: rule.id.clone(),
138                        message_template: rule.message.clone(),
139                        severity: effective_severity,
140                        exclude_conditions: rule.exclude_if.clone(),
141                    },
142                );
143            }
144            RuleType::Ast => {
145                let pattern_type = self.parse_ast_pattern(&rule.pattern, &rule.id)?;
146
147                self.ast_patterns.insert(
148                    rule.id.clone(),
149                    AstPattern {
150                        pattern_type,
151                        rule_id: rule.id.clone(),
152                        message_template: rule.message.clone(),
153                        severity: effective_severity,
154                        exclude_conditions: rule.exclude_if.clone(),
155                    },
156                );
157            }
158            RuleType::Semantic | RuleType::ImportAnalysis => {
159                let pattern_type = self.parse_semantic_pattern(&rule.pattern, &rule.id)?;
160
161                self.ast_patterns.insert(
162                    rule.id.clone(),
163                    AstPattern {
164                        pattern_type,
165                        rule_id: rule.id.clone(),
166                        message_template: rule.message.clone(),
167                        severity: effective_severity,
168                        exclude_conditions: rule.exclude_if.clone(),
169                    },
170                );
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Parse AST pattern string into typed pattern
178    fn parse_ast_pattern(&self, pattern: &str, rule_id: &str) -> GuardianResult<AstPatternType> {
179        if pattern.starts_with("macro_call:") {
180            let macros = pattern
181                .strip_prefix("macro_call:")
182                .expect("pattern starts with 'macro_call:' - prefix strip should not fail")
183                .split('|')
184                .map(|s| s.trim().to_string())
185                .collect();
186            Ok(AstPatternType::MacroCall(macros))
187        } else if pattern == "return_ok_unit_with_no_logic" {
188            Ok(AstPatternType::EmptyOkReturn)
189        } else if pattern.contains("Architectural Principle:") {
190            Ok(AstPatternType::MissingArchitecturalHeader)
191        } else if pattern == "empty_function_body" {
192            Ok(AstPatternType::EmptyFunctionBody)
193        } else if pattern == "unwrap_or_expect_without_message" {
194            Ok(AstPatternType::UnwrapOrExpectWithoutMessage)
195        } else if pattern == "unsafe_block" {
196            Ok(AstPatternType::UnsafeBlock)
197        } else if pattern == "ignored_test_attribute" {
198            Ok(AstPatternType::IgnoredTestAttribute)
199        } else {
200            Err(GuardianError::pattern(format!(
201                "Unknown AST pattern type in rule '{rule_id}': {pattern}"
202            )))
203        }
204    }
205
206    /// Parse semantic pattern string into typed pattern
207    fn parse_semantic_pattern(
208        &self,
209        pattern: &str,
210        rule_id: &str,
211    ) -> GuardianResult<AstPatternType> {
212        // Handle parametric patterns first
213        if let Some(param) = pattern.strip_prefix("cyclomatic_complexity_gt:") {
214            let threshold = param.parse::<u32>().map_err(|_| {
215                GuardianError::pattern(format!("Invalid threshold in rule '{rule_id}': {param}"))
216            })?;
217            return Ok(AstPatternType::CyclomaticComplexity(threshold));
218        }
219
220        if let Some(param) = pattern.strip_prefix("function_lines_gt:") {
221            let threshold = param.parse::<u32>().map_err(|_| {
222                GuardianError::pattern(format!("Invalid threshold in rule '{rule_id}': {param}"))
223            })?;
224            return Ok(AstPatternType::FunctionLinesGt(threshold));
225        }
226
227        if let Some(param) = pattern.strip_prefix("nesting_depth_gt:") {
228            let threshold = param.parse::<u32>().map_err(|_| {
229                GuardianError::pattern(format!("Invalid threshold in rule '{rule_id}': {param}"))
230            })?;
231            return Ok(AstPatternType::NestingDepthGt(threshold));
232        }
233
234        if let Some(param) = pattern.strip_prefix("function_args_gt:") {
235            let threshold = param.parse::<u32>().map_err(|_| {
236                GuardianError::pattern(format!("Invalid threshold in rule '{rule_id}': {param}"))
237            })?;
238            return Ok(AstPatternType::FunctionArgsGt(threshold));
239        }
240
241        // Handle non-parametric semantic patterns
242        match pattern {
243            "public_without_docs" => Ok(AstPatternType::PublicWithoutDocs),
244            "blocking_call_in_async" => Ok(AstPatternType::BlockingCallInAsync),
245            "future_not_awaited" => Ok(AstPatternType::FutureNotAwaited),
246            "select_without_biased" => Ok(AstPatternType::SelectWithoutBiased),
247            "generic_without_bounds" => Ok(AstPatternType::GenericWithoutBounds),
248            "test_fn_without_assertion" => Ok(AstPatternType::TestFnWithoutAssertion),
249            "impl_without_trait" => Ok(AstPatternType::ImplWithoutTrait),
250            _ => {
251                // For unrecognized semantic patterns, check if they look like import patterns
252                // This allows users to define custom layering import checking
253                if pattern.starts_with("use")
254                    || pattern.starts_with("import:")
255                    || pattern.contains("_access")
256                {
257                    // Handle patterns like "use.*concrete" or "use.*implementation"
258                    let import_pattern = if pattern.starts_with("use") {
259                        pattern.to_string()
260                    } else if pattern.starts_with("import:") {
261                        pattern.replace("import:", "")
262                    } else {
263                        // Handle patterns like "direct_X_access"
264                        format!(
265                            r"use\s+.*{}",
266                            pattern.replace("direct_", "").replace("_access", "")
267                        )
268                    };
269
270                    if let Ok(regex) = regex::Regex::new(&import_pattern) {
271                        Ok(AstPatternType::AbstractionLayerViolation(regex))
272                    } else {
273                        Err(GuardianError::pattern(format!(
274                            "Invalid import pattern in rule '{rule_id}': {pattern}"
275                        )))
276                    }
277                } else {
278                    // Unknown pattern - could be a future extension
279                    Err(GuardianError::pattern(format!(
280                        "Unknown semantic pattern type in rule '{rule_id}': {pattern}"
281                    )))
282                }
283            }
284        }
285    }
286
287    /// Analyze a file and return all pattern matches
288    pub fn analyze_file<P: AsRef<Path>>(
289        &self,
290        file_path: P,
291        content: &str,
292    ) -> GuardianResult<Vec<PatternMatch>> {
293        let file_path = file_path.as_ref();
294        let mut matches = Vec::new();
295
296        tracing::debug!(
297            "Analyzing file '{}' with {} regex patterns and {} AST patterns",
298            file_path.display(),
299            self.regex_patterns.len(),
300            self.ast_patterns.len()
301        );
302
303        // Apply regex patterns
304        for pattern in self.regex_patterns.values() {
305            tracing::debug!("Processing regex pattern '{}'", pattern.rule_id);
306            let pattern_matches = self.apply_regex_pattern(pattern, file_path, content)?;
307            tracing::debug!(
308                "Pattern '{}' found {} matches",
309                pattern.rule_id,
310                pattern_matches.len()
311            );
312            matches.extend(pattern_matches);
313        }
314
315        // Apply AST patterns for Rust files
316        if file_path.extension().and_then(|s| s.to_str()) == Some("rs") {
317            for pattern in self.ast_patterns.values() {
318                let pattern_matches = self.apply_ast_pattern(pattern, file_path, content)?;
319                matches.extend(pattern_matches);
320            }
321        }
322
323        Ok(matches)
324    }
325
326    /// Apply a regex pattern to file content
327    fn apply_regex_pattern(
328        &self,
329        pattern: &CompiledRegex,
330        file_path: &Path,
331        content: &str,
332    ) -> GuardianResult<Vec<PatternMatch>> {
333        tracing::debug!(
334            "Applying regex pattern '{}' to file '{}'",
335            pattern.rule_id,
336            file_path.display()
337        );
338        tracing::debug!("Pattern regex: '{}'", pattern.regex.as_str());
339        tracing::debug!("Content length: {} characters", content.len());
340
341        let mut matches = Vec::new();
342
343        // Find all matches in the content
344        for regex_match in pattern.regex.find_iter(content) {
345            tracing::debug!(
346                "Found regex match: '{}' at offset {}",
347                regex_match.as_str(),
348                regex_match.start()
349            );
350            let matched_text = regex_match.as_str().to_string();
351            let (line_num, col_num, context) =
352                self.get_match_location(content, regex_match.start());
353
354            // Check exclude conditions
355            if self.should_exclude_match(
356                pattern.exclude_conditions.as_ref(),
357                file_path,
358                &matched_text,
359                content,
360                regex_match.start(),
361            ) {
362                tracing::debug!("Match '{}' excluded by conditions", matched_text);
363                continue;
364            }
365
366            let message = pattern.message_template.replace("{match}", &matched_text);
367
368            matches.push(PatternMatch {
369                rule_id: pattern.rule_id.clone(),
370                file_path: file_path.to_path_buf(),
371                line_number: Some(line_num),
372                column_number: Some(col_num),
373                matched_text,
374                message,
375                severity: pattern.severity,
376                context: Some(context),
377            });
378        }
379
380        Ok(matches)
381    }
382
383    /// Apply an AST pattern to Rust source code
384    fn apply_ast_pattern(
385        &self,
386        pattern: &AstPattern,
387        file_path: &Path,
388        content: &str,
389    ) -> GuardianResult<Vec<PatternMatch>> {
390        let mut matches = Vec::new();
391
392        // Parse Rust syntax
393        let syntax_tree = match syn::parse_file(content) {
394            Ok(tree) => tree,
395            Err(e) => {
396                // If we can't parse the file, skip AST analysis but don't fail
397                tracing::debug!("Failed to parse Rust file {}: {}", file_path.display(), e);
398                return Ok(matches);
399            }
400        };
401
402        match &pattern.pattern_type {
403            AstPatternType::MacroCall(macro_names) => {
404                let found_matches = self.find_macro_calls(&syntax_tree, macro_names);
405                for (line, col, macro_name, context) in found_matches {
406                    // Check exclude conditions
407                    if self.should_exclude_ast_match(
408                        pattern.exclude_conditions.as_ref(),
409                        file_path,
410                        &syntax_tree,
411                        line,
412                    ) {
413                        continue;
414                    }
415
416                    let message = pattern
417                        .message_template
418                        .replace("{macro_name}", &macro_name);
419
420                    matches.push(PatternMatch {
421                        rule_id: pattern.rule_id.clone(),
422                        file_path: file_path.to_path_buf(),
423                        line_number: Some(line),
424                        column_number: Some(col),
425                        matched_text: format!("{macro_name}!()"),
426                        message,
427                        severity: pattern.severity,
428                        context: Some(context),
429                    });
430                }
431            }
432            AstPatternType::CyclomaticComplexity(threshold) => {
433                let found_matches = self.find_cyclomatic_complexity(&syntax_tree, *threshold);
434                for (line, col, fn_name, complexity, context) in found_matches {
435                    if self.should_exclude_ast_match(
436                        pattern.exclude_conditions.as_ref(),
437                        file_path,
438                        &syntax_tree,
439                        line,
440                    ) {
441                        continue;
442                    }
443
444                    let message = pattern
445                        .message_template
446                        .replace("{value}", &complexity.to_string());
447
448                    matches.push(PatternMatch {
449                        rule_id: pattern.rule_id.clone(),
450                        file_path: file_path.to_path_buf(),
451                        line_number: Some(line),
452                        column_number: Some(col),
453                        matched_text: format!("fn {}", fn_name),
454                        message,
455                        severity: pattern.severity,
456                        context: Some(context),
457                    });
458                }
459            }
460            AstPatternType::PublicWithoutDocs => {
461                let found_matches = self.find_public_without_docs(&syntax_tree);
462                for (line, col, item_name, context) in found_matches {
463                    if self.should_exclude_ast_match(
464                        pattern.exclude_conditions.as_ref(),
465                        file_path,
466                        &syntax_tree,
467                        line,
468                    ) {
469                        continue;
470                    }
471
472                    matches.push(PatternMatch {
473                        rule_id: pattern.rule_id.clone(),
474                        file_path: file_path.to_path_buf(),
475                        line_number: Some(line),
476                        column_number: Some(col),
477                        matched_text: item_name,
478                        message: pattern.message_template.clone(),
479                        severity: pattern.severity,
480                        context: Some(context),
481                    });
482                }
483            }
484            AstPatternType::FunctionLinesGt(threshold) => {
485                let found_matches = self.find_long_functions(&syntax_tree, content, *threshold);
486                for (line, col, fn_name, line_count, context) in found_matches {
487                    if self.should_exclude_ast_match(
488                        pattern.exclude_conditions.as_ref(),
489                        file_path,
490                        &syntax_tree,
491                        line,
492                    ) {
493                        continue;
494                    }
495
496                    let message = pattern
497                        .message_template
498                        .replace("{lines}", &line_count.to_string());
499
500                    matches.push(PatternMatch {
501                        rule_id: pattern.rule_id.clone(),
502                        file_path: file_path.to_path_buf(),
503                        line_number: Some(line),
504                        column_number: Some(col),
505                        matched_text: format!("fn {}", fn_name),
506                        message,
507                        severity: pattern.severity,
508                        context: Some(context),
509                    });
510                }
511            }
512            AstPatternType::NestingDepthGt(threshold) => {
513                let found_matches = self.find_deep_nesting(&syntax_tree, *threshold);
514                for (line, col, depth, context) in found_matches {
515                    if self.should_exclude_ast_match(
516                        pattern.exclude_conditions.as_ref(),
517                        file_path,
518                        &syntax_tree,
519                        line,
520                    ) {
521                        continue;
522                    }
523
524                    let message = pattern
525                        .message_template
526                        .replace("{depth}", &depth.to_string());
527
528                    matches.push(PatternMatch {
529                        rule_id: pattern.rule_id.clone(),
530                        file_path: file_path.to_path_buf(),
531                        line_number: Some(line),
532                        column_number: Some(col),
533                        matched_text: "nested block".to_string(),
534                        message,
535                        severity: pattern.severity,
536                        context: Some(context),
537                    });
538                }
539            }
540            AstPatternType::FunctionArgsGt(threshold) => {
541                let found_matches = self.find_functions_with_many_args(&syntax_tree, *threshold);
542                for (line, col, fn_name, arg_count, context) in found_matches {
543                    if self.should_exclude_ast_match(
544                        pattern.exclude_conditions.as_ref(),
545                        file_path,
546                        &syntax_tree,
547                        line,
548                    ) {
549                        continue;
550                    }
551
552                    let message = pattern
553                        .message_template
554                        .replace("{count}", &arg_count.to_string());
555
556                    matches.push(PatternMatch {
557                        rule_id: pattern.rule_id.clone(),
558                        file_path: file_path.to_path_buf(),
559                        line_number: Some(line),
560                        column_number: Some(col),
561                        matched_text: format!("fn {}", fn_name),
562                        message,
563                        severity: pattern.severity,
564                        context: Some(context),
565                    });
566                }
567            }
568            AstPatternType::BlockingCallInAsync => {
569                let found_matches = self.find_blocking_in_async(&syntax_tree);
570                for (line, col, call_name, context) in found_matches {
571                    if self.should_exclude_ast_match(
572                        pattern.exclude_conditions.as_ref(),
573                        file_path,
574                        &syntax_tree,
575                        line,
576                    ) {
577                        continue;
578                    }
579
580                    matches.push(PatternMatch {
581                        rule_id: pattern.rule_id.clone(),
582                        file_path: file_path.to_path_buf(),
583                        line_number: Some(line),
584                        column_number: Some(col),
585                        matched_text: call_name,
586                        message: pattern.message_template.clone(),
587                        severity: pattern.severity,
588                        context: Some(context),
589                    });
590                }
591            }
592            AstPatternType::FutureNotAwaited => {
593                let found_matches = self.find_futures_not_awaited(&syntax_tree);
594                for (line, col, expr, context) in found_matches {
595                    if self.should_exclude_ast_match(
596                        pattern.exclude_conditions.as_ref(),
597                        file_path,
598                        &syntax_tree,
599                        line,
600                    ) {
601                        continue;
602                    }
603
604                    matches.push(PatternMatch {
605                        rule_id: pattern.rule_id.clone(),
606                        file_path: file_path.to_path_buf(),
607                        line_number: Some(line),
608                        column_number: Some(col),
609                        matched_text: expr,
610                        message: pattern.message_template.clone(),
611                        severity: pattern.severity,
612                        context: Some(context),
613                    });
614                }
615            }
616            AstPatternType::SelectWithoutBiased => {
617                let found_matches = self.find_select_without_biased(&syntax_tree);
618                for (line, col, context) in found_matches {
619                    if self.should_exclude_ast_match(
620                        pattern.exclude_conditions.as_ref(),
621                        file_path,
622                        &syntax_tree,
623                        line,
624                    ) {
625                        continue;
626                    }
627
628                    matches.push(PatternMatch {
629                        rule_id: pattern.rule_id.clone(),
630                        file_path: file_path.to_path_buf(),
631                        line_number: Some(line),
632                        column_number: Some(col),
633                        matched_text: "tokio::select!".to_string(),
634                        message: pattern.message_template.clone(),
635                        severity: pattern.severity,
636                        context: Some(context),
637                    });
638                }
639            }
640            AstPatternType::GenericWithoutBounds => {
641                let found_matches = self.find_generics_without_bounds(&syntax_tree);
642                for (line, col, generic_name, context) in found_matches {
643                    if self.should_exclude_ast_match(
644                        pattern.exclude_conditions.as_ref(),
645                        file_path,
646                        &syntax_tree,
647                        line,
648                    ) {
649                        continue;
650                    }
651
652                    matches.push(PatternMatch {
653                        rule_id: pattern.rule_id.clone(),
654                        file_path: file_path.to_path_buf(),
655                        line_number: Some(line),
656                        column_number: Some(col),
657                        matched_text: generic_name,
658                        message: pattern.message_template.clone(),
659                        severity: pattern.severity,
660                        context: Some(context),
661                    });
662                }
663            }
664            AstPatternType::TestFnWithoutAssertion => {
665                let found_matches = self.find_test_functions_without_assertions(&syntax_tree);
666                for (line, col, fn_name, context) in found_matches {
667                    if self.should_exclude_ast_match(
668                        pattern.exclude_conditions.as_ref(),
669                        file_path,
670                        &syntax_tree,
671                        line,
672                    ) {
673                        continue;
674                    }
675
676                    matches.push(PatternMatch {
677                        rule_id: pattern.rule_id.clone(),
678                        file_path: file_path.to_path_buf(),
679                        line_number: Some(line),
680                        column_number: Some(col),
681                        matched_text: format!("fn {}", fn_name),
682                        message: pattern.message_template.clone(),
683                        severity: pattern.severity,
684                        context: Some(context),
685                    });
686                }
687            }
688            AstPatternType::ImplWithoutTrait => {
689                let found_matches = self.find_impl_without_trait(&syntax_tree);
690                for (line, col, impl_name, context) in found_matches {
691                    if self.should_exclude_ast_match(
692                        pattern.exclude_conditions.as_ref(),
693                        file_path,
694                        &syntax_tree,
695                        line,
696                    ) {
697                        continue;
698                    }
699
700                    matches.push(PatternMatch {
701                        rule_id: pattern.rule_id.clone(),
702                        file_path: file_path.to_path_buf(),
703                        line_number: Some(line),
704                        column_number: Some(col),
705                        matched_text: format!("impl {}", impl_name),
706                        message: pattern.message_template.clone(),
707                        severity: pattern.severity,
708                        context: Some(context),
709                    });
710                }
711            }
712            AstPatternType::UnsafeBlock => {
713                let found_matches = self.find_unsafe_blocks(&syntax_tree);
714                for (line, col, context) in found_matches {
715                    if self.should_exclude_ast_match(
716                        pattern.exclude_conditions.as_ref(),
717                        file_path,
718                        &syntax_tree,
719                        line,
720                    ) {
721                        continue;
722                    }
723
724                    matches.push(PatternMatch {
725                        rule_id: pattern.rule_id.clone(),
726                        file_path: file_path.to_path_buf(),
727                        line_number: Some(line),
728                        column_number: Some(col),
729                        matched_text: "unsafe".to_string(),
730                        message: pattern.message_template.clone(),
731                        severity: pattern.severity,
732                        context: Some(context),
733                    });
734                }
735            }
736            AstPatternType::IgnoredTestAttribute => {
737                let found_matches = self.find_ignored_tests(&syntax_tree);
738                for (line, col, fn_name, context) in found_matches {
739                    if self.should_exclude_ast_match(
740                        pattern.exclude_conditions.as_ref(),
741                        file_path,
742                        &syntax_tree,
743                        line,
744                    ) {
745                        continue;
746                    }
747
748                    matches.push(PatternMatch {
749                        rule_id: pattern.rule_id.clone(),
750                        file_path: file_path.to_path_buf(),
751                        line_number: Some(line),
752                        column_number: Some(col),
753                        matched_text: format!("#[ignore] fn {}", fn_name),
754                        message: pattern.message_template.clone(),
755                        severity: pattern.severity,
756                        context: Some(context),
757                    });
758                }
759            }
760
761            AstPatternType::EmptyOkReturn => {
762                let found_matches = self.find_empty_ok_returns(&syntax_tree);
763                for (line, col, context) in found_matches {
764                    // Check exclude conditions
765                    if self.should_exclude_ast_match(
766                        pattern.exclude_conditions.as_ref(),
767                        file_path,
768                        &syntax_tree,
769                        line,
770                    ) {
771                        continue;
772                    }
773
774                    matches.push(PatternMatch {
775                        rule_id: pattern.rule_id.clone(),
776                        file_path: file_path.to_path_buf(),
777                        line_number: Some(line),
778                        column_number: Some(col),
779                        matched_text: "Ok(())".to_string(),
780                        message: pattern.message_template.clone(),
781                        severity: pattern.severity,
782                        context: Some(context),
783                    });
784                }
785            }
786            AstPatternType::MissingArchitecturalHeader => {
787                if !content.contains("Architectural Principle:") {
788                    matches.push(PatternMatch {
789                        rule_id: pattern.rule_id.clone(),
790                        file_path: file_path.to_path_buf(),
791                        line_number: Some(1),
792                        column_number: Some(1),
793                        matched_text: "".to_string(),
794                        message: pattern.message_template.clone(),
795                        severity: pattern.severity,
796                        context: None,
797                    });
798                }
799            }
800            AstPatternType::EmptyFunctionBody => {
801                let found_matches = self.find_empty_function_bodies(&syntax_tree);
802                for (line, col, fn_name, context) in found_matches {
803                    // Check exclude conditions
804                    if self.should_exclude_ast_match(
805                        pattern.exclude_conditions.as_ref(),
806                        file_path,
807                        &syntax_tree,
808                        line,
809                    ) {
810                        continue;
811                    }
812
813                    let message = pattern
814                        .message_template
815                        .replace("{function_name}", &fn_name);
816
817                    matches.push(PatternMatch {
818                        rule_id: pattern.rule_id.clone(),
819                        file_path: file_path.to_path_buf(),
820                        line_number: Some(line),
821                        column_number: Some(col),
822                        matched_text: format!("fn {}", fn_name),
823                        message,
824                        severity: pattern.severity,
825                        context: Some(context),
826                    });
827                }
828            }
829            AstPatternType::UnwrapOrExpectWithoutMessage => {
830                let found_matches = self.find_unwrap_without_message(&syntax_tree);
831                for (line, col, method_name, context) in found_matches {
832                    // Check exclude conditions
833                    if self.should_exclude_ast_match(
834                        pattern.exclude_conditions.as_ref(),
835                        file_path,
836                        &syntax_tree,
837                        line,
838                    ) {
839                        continue;
840                    }
841
842                    let message = pattern.message_template.replace("{method}", &method_name);
843
844                    matches.push(PatternMatch {
845                        rule_id: pattern.rule_id.clone(),
846                        file_path: file_path.to_path_buf(),
847                        line_number: Some(line),
848                        column_number: Some(col),
849                        matched_text: format!(".{}()", method_name),
850                        message,
851                        severity: pattern.severity,
852                        context: Some(context),
853                    });
854                }
855            }
856            AstPatternType::AbstractionLayerViolation(regex) => {
857                let found_matches = self.find_import_pattern_matches(&syntax_tree, content, regex);
858                for (line, col, import_text, context) in found_matches {
859                    // Check exclude conditions
860                    if self.should_exclude_ast_match(
861                        pattern.exclude_conditions.as_ref(),
862                        file_path,
863                        &syntax_tree,
864                        line,
865                    ) {
866                        continue;
867                    }
868
869                    matches.push(PatternMatch {
870                        rule_id: pattern.rule_id.clone(),
871                        file_path: file_path.to_path_buf(),
872                        line_number: Some(line),
873                        column_number: Some(col),
874                        matched_text: import_text,
875                        message: pattern.message_template.clone(),
876                        severity: pattern.severity,
877                        context: Some(context),
878                    });
879                }
880            }
881        }
882
883        Ok(matches)
884    }
885
886    /// Find macro calls in the syntax tree
887    fn find_macro_calls(
888        &self,
889        syntax_tree: &syn::File,
890        target_macros: &[String],
891    ) -> Vec<(u32, u32, String, String)> {
892        use syn::visit::Visit;
893
894        struct MacroVisitor<'a> {
895            target_macros: &'a [String],
896            matches: Vec<(u32, u32, String, String)>,
897        }
898
899        impl Visit<'_> for MacroVisitor<'_> {
900            fn visit_macro(&mut self, mac: &syn::Macro) {
901                if let Some(ident) = mac.path.get_ident() {
902                    let macro_name = ident.to_string();
903                    if self.target_macros.contains(&macro_name) {
904                        let _span = mac.path.span();
905                        // proc_macro2::Span doesn't provide direct line/column access in stable Rust
906                        // Use line 1 with improved context for macro location
907                        let context = format!("{}!()", macro_name);
908                        self.matches.push((1, 1, macro_name, context));
909                    }
910                }
911                syn::visit::visit_macro(self, mac);
912            }
913        }
914
915        let mut visitor = MacroVisitor {
916            target_macros,
917            matches: Vec::new(),
918        };
919
920        visitor.visit_file(syntax_tree);
921        visitor.matches
922    }
923    /// Find functions that return empty Ok(()) responses
924    fn find_empty_ok_returns(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String)> {
925        use syn::visit::Visit;
926
927        struct EmptyOkVisitor {
928            matches: Vec<(u32, u32, String)>,
929        }
930
931        impl Visit<'_> for EmptyOkVisitor {
932            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
933                // Check if function returns Result type
934                if let syn::ReturnType::Type(_, return_type) = &func.sig.output {
935                    if self.is_result_type(return_type) {
936                        // Check if body is just Ok(()) or similar
937                        if let Some(ok_expr) = self.find_ok_unit_return(&func.block) {
938                            let _span = ok_expr.span();
939                            // Use a simple line-based location since proc_macro2::Span doesn't have start() method
940                            // Use a simple line-based location since proc_macro2::Span doesn't have start() method
941                            let (line, col, context) = (1, 1, String::new());
942                            self.matches.push((line, col, context));
943                        }
944                    }
945                }
946                syn::visit::visit_item_fn(self, func);
947            }
948        }
949
950        impl EmptyOkVisitor {
951            fn is_result_type(&self, ty: &syn::Type) -> bool {
952                match ty {
953                    syn::Type::Path(type_path) => type_path
954                        .path
955                        .segments
956                        .last()
957                        .map(|seg| seg.ident == "Result")
958                        .unwrap_or(false),
959                    _ => false,
960                }
961            }
962
963            fn find_ok_unit_return<'b>(&self, block: &'b syn::Block) -> Option<&'b syn::Expr> {
964                // Look for a block with just one statement that returns Ok(())
965                if block.stmts.len() == 1 {
966                    if let syn::Stmt::Expr(expr, _) = &block.stmts[0] {
967                        if self.is_ok_unit_expr(expr) {
968                            return Some(expr);
969                        }
970                    }
971                }
972                None
973            }
974
975            fn is_ok_unit_expr(&self, expr: &syn::Expr) -> bool {
976                if let syn::Expr::Call(call) = expr {
977                    // Check if it's Ok(())
978                    if let syn::Expr::Path(path) = &*call.func {
979                        if path
980                            .path
981                            .segments
982                            .last()
983                            .map(|seg| seg.ident == "Ok")
984                            .unwrap_or(false)
985                        {
986                            // Check if argument is unit type ()
987                            if call.args.len() == 1 {
988                                if let syn::Expr::Tuple(tuple) = &call.args[0] {
989                                    return tuple.elems.is_empty();
990                                }
991                            }
992                        }
993                    }
994                }
995                false
996            }
997        }
998
999        let mut visitor = EmptyOkVisitor {
1000            matches: Vec::new(),
1001        };
1002
1003        visitor.visit_file(syntax_tree);
1004        visitor.matches
1005    }
1006
1007    /// Find functions with empty bodies
1008    fn find_empty_function_bodies(
1009        &self,
1010        syntax_tree: &syn::File,
1011    ) -> Vec<(u32, u32, String, String)> {
1012        use syn::visit::Visit;
1013
1014        struct EmptyBodyVisitor {
1015            matches: Vec<(u32, u32, String, String)>,
1016        }
1017
1018        impl Visit<'_> for EmptyBodyVisitor {
1019            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1020                let fn_name = func.sig.ident.to_string();
1021
1022                // Check if function body is empty or has only comments/whitespace
1023                if func.block.stmts.is_empty() {
1024                    // Function has completely empty body
1025                    let (line, col, context) = (1, 1, format!("fn {} {{ }}", fn_name));
1026                    self.matches.push((line, col, fn_name, context));
1027                } else if func.block.stmts.len() == 1 {
1028                    // Check if the single statement is just a comment or empty expression
1029                    if let syn::Stmt::Expr(expr, _) = &func.block.stmts[0] {
1030                        if matches!(expr, syn::Expr::Tuple(tuple) if tuple.elems.is_empty()) {
1031                            // Function body contains only ()
1032                            let (line, col, context) = (1, 1, format!("fn {} {{ () }}", fn_name));
1033                            self.matches.push((line, col, fn_name, context));
1034                        }
1035                    }
1036                }
1037
1038                syn::visit::visit_item_fn(self, func);
1039            }
1040        }
1041
1042        let mut visitor = EmptyBodyVisitor {
1043            matches: Vec::new(),
1044        };
1045
1046        visitor.visit_file(syntax_tree);
1047        visitor.matches
1048    }
1049
1050    /// Find unwrap() or expect() calls without meaningful error messages
1051    fn find_unwrap_without_message(
1052        &self,
1053        syntax_tree: &syn::File,
1054    ) -> Vec<(u32, u32, String, String)> {
1055        use syn::visit::Visit;
1056
1057        struct UnwrapVisitor {
1058            matches: Vec<(u32, u32, String, String)>,
1059        }
1060
1061        impl Visit<'_> for UnwrapVisitor {
1062            fn visit_expr_method_call(&mut self, method_call: &syn::ExprMethodCall) {
1063                let method_name = method_call.method.to_string();
1064
1065                match method_name.as_str() {
1066                    "unwrap" => {
1067                        // unwrap() calls are always problematic
1068                        let (line, col, context) = (1, 1, ".unwrap()".to_string());
1069                        self.matches
1070                            .push((line, col, "unwrap".to_string(), context));
1071                    }
1072                    "expect" => {
1073                        // Check if expect() has a meaningful message
1074                        if method_call.args.is_empty() {
1075                            // expect() without any message
1076                            let (line, col, context) = (1, 1, ".expect()".to_string());
1077                            self.matches
1078                                .push((line, col, "expect".to_string(), context));
1079                        } else if let syn::Expr::Lit(syn::ExprLit {
1080                            lit: syn::Lit::Str(lit_str),
1081                            ..
1082                        }) = &method_call.args[0]
1083                        {
1084                            let message = lit_str.value();
1085                            // Check for generic/unhelpful messages
1086                            if message.is_empty()
1087                                || message.len() < 5
1088                                || message.to_lowercase().contains("error") && message.len() < 10
1089                            {
1090                                let (line, col, context) =
1091                                    (1, 1, format!(".expect(\"{}\")", message));
1092                                self.matches
1093                                    .push((line, col, "expect".to_string(), context));
1094                            }
1095                        }
1096                    }
1097                    _ => {}
1098                }
1099
1100                syn::visit::visit_expr_method_call(self, method_call);
1101            }
1102        }
1103
1104        let mut visitor = UnwrapVisitor {
1105            matches: Vec::new(),
1106        };
1107
1108        visitor.visit_file(syntax_tree);
1109        visitor.matches
1110    }
1111
1112    /// Find import patterns using regex matching on use statements
1113    fn find_import_pattern_matches(
1114        &self,
1115        syntax_tree: &syn::File,
1116        _content: &str,
1117        regex: &regex::Regex,
1118    ) -> Vec<(u32, u32, String, String)> {
1119        use syn::visit::Visit;
1120
1121        struct ImportVisitor<'a> {
1122            regex: &'a regex::Regex,
1123            matches: Vec<(u32, u32, String, String)>,
1124        }
1125
1126        impl Visit<'_> for ImportVisitor<'_> {
1127            fn visit_item_use(&mut self, use_item: &syn::ItemUse) {
1128                // Convert the use statement back to string for regex matching
1129                let use_string = format!(
1130                    "use {};",
1131                    quote::quote!(#use_item)
1132                        .to_string()
1133                        .trim_start_matches("use ")
1134                );
1135
1136                if self.regex.is_match(&use_string) {
1137                    // Extract line information from the use statement
1138                    // Use simple line tracking for AST span location
1139                    // we'd use syn span information for precise location
1140                    let (line, col, context) = (1, 1, use_string.clone());
1141                    self.matches.push((line, col, use_string, context));
1142                }
1143
1144                syn::visit::visit_item_use(self, use_item);
1145            }
1146        }
1147
1148        let mut visitor = ImportVisitor {
1149            regex,
1150            matches: Vec::new(),
1151        };
1152
1153        visitor.visit_file(syntax_tree);
1154        visitor.matches
1155    }
1156
1157    /// Get line and column number from byte offset in content
1158    fn get_match_location(&self, content: &str, byte_offset: usize) -> (u32, u32, String) {
1159        let mut line = 1;
1160        let mut col = 1;
1161        let mut line_start = 0;
1162
1163        for (i, ch) in content.char_indices() {
1164            if i >= byte_offset {
1165                break;
1166            }
1167            if ch == '\n' {
1168                line += 1;
1169                col = 1;
1170                line_start = i + 1;
1171            } else {
1172                col += 1;
1173            }
1174        }
1175
1176        // Extract context line
1177        let line_end = content[line_start..]
1178            .find('\n')
1179            .map(|pos| line_start + pos)
1180            .unwrap_or(content.len());
1181
1182        let context = content[line_start..line_end].trim().to_string();
1183
1184        (line, col, context)
1185    }
1186
1187    /// Check if a regex match should be excluded based on conditions
1188    fn should_exclude_match(
1189        &self,
1190        conditions: Option<&ExcludeConditions>,
1191        file_path: &Path,
1192        matched_text: &str,
1193        _content: &str,
1194        _offset: usize,
1195    ) -> bool {
1196        if let Some(conditions) = conditions {
1197            tracing::debug!(
1198                "Checking exclude conditions for match '{}' in file '{}'",
1199                matched_text,
1200                file_path.display()
1201            );
1202
1203            // Check if in test files
1204            if conditions.in_tests && self.is_test_file(file_path) {
1205                tracing::debug!("Match excluded: in_tests=true and file is test file");
1206                return true;
1207            }
1208
1209            // Check file patterns
1210            if let Some(patterns) = &conditions.file_patterns {
1211                for pattern in patterns {
1212                    if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
1213                        if glob_pattern.matches_path(file_path) {
1214                            tracing::debug!("Match excluded: file matches pattern '{}'", pattern);
1215                            return true;
1216                        }
1217                    }
1218                }
1219            }
1220
1221            // Additional condition checks can be added here when AST context is available
1222            tracing::debug!("Match not excluded by any conditions");
1223        } else {
1224            tracing::debug!("No exclude conditions to check");
1225        }
1226
1227        false
1228    }
1229
1230    /// Check if an AST match should be excluded
1231    fn should_exclude_ast_match(
1232        &self,
1233        conditions: Option<&ExcludeConditions>,
1234        file_path: &Path,
1235        _syntax_tree: &syn::File,
1236        _line: u32,
1237    ) -> bool {
1238        if let Some(conditions) = conditions {
1239            // Check if in test files
1240            if conditions.in_tests && self.is_test_file(file_path) {
1241                return true;
1242            }
1243
1244            // Check file patterns
1245            if let Some(patterns) = &conditions.file_patterns {
1246                for pattern in patterns {
1247                    if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
1248                        if glob_pattern.matches_path(file_path) {
1249                            return true;
1250                        }
1251                    }
1252                }
1253            }
1254
1255            // Future enhancement: Check for specific attributes like #[test] on functions
1256        }
1257
1258        false
1259    }
1260
1261    /// Check if a file path indicates it's a test file
1262    fn is_test_file(&self, file_path: &Path) -> bool {
1263        file_path.components().any(|component| {
1264            component
1265                .as_os_str()
1266                .to_str()
1267                .map(|s| s == "tests" || s == "test")
1268                .unwrap_or(false)
1269        }) || file_path
1270            .file_name()
1271            .and_then(|name| name.to_str())
1272            .map(|name| name.contains("test") || name.starts_with("test_"))
1273            .unwrap_or(false)
1274    }
1275
1276    /// Find functions with high cyclomatic complexity
1277    fn find_cyclomatic_complexity(
1278        &self,
1279        syntax_tree: &syn::File,
1280        threshold: u32,
1281    ) -> Vec<(u32, u32, String, u32, String)> {
1282        use syn::visit::Visit;
1283
1284        struct ComplexityVisitor {
1285            threshold: u32,
1286            matches: Vec<(u32, u32, String, u32, String)>,
1287        }
1288
1289        impl Visit<'_> for ComplexityVisitor {
1290            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1291                let fn_name = func.sig.ident.to_string();
1292                let complexity = self.calculate_complexity(&func.block);
1293
1294                if complexity > self.threshold {
1295                    let (line, col, context) =
1296                        (1, 1, format!("fn {} (complexity: {})", fn_name, complexity));
1297                    self.matches.push((line, col, fn_name, complexity, context));
1298                }
1299
1300                syn::visit::visit_item_fn(self, func);
1301            }
1302        }
1303
1304        impl ComplexityVisitor {
1305            fn calculate_complexity(&self, block: &syn::Block) -> u32 {
1306                use syn::visit::Visit;
1307
1308                struct ComplexityCalculator {
1309                    complexity: u32,
1310                }
1311
1312                impl Visit<'_> for ComplexityCalculator {
1313                    fn visit_expr_if(&mut self, expr: &syn::ExprIf) {
1314                        self.complexity += 1;
1315                        syn::visit::visit_expr_if(self, expr);
1316                    }
1317
1318                    fn visit_expr_while(&mut self, expr: &syn::ExprWhile) {
1319                        self.complexity += 1;
1320                        syn::visit::visit_expr_while(self, expr);
1321                    }
1322
1323                    fn visit_expr_for_loop(&mut self, expr: &syn::ExprForLoop) {
1324                        self.complexity += 1;
1325                        syn::visit::visit_expr_for_loop(self, expr);
1326                    }
1327
1328                    fn visit_expr_loop(&mut self, expr: &syn::ExprLoop) {
1329                        self.complexity += 1;
1330                        syn::visit::visit_expr_loop(self, expr);
1331                    }
1332
1333                    fn visit_expr_match(&mut self, expr_match: &syn::ExprMatch) {
1334                        self.complexity += expr_match.arms.len() as u32;
1335                        syn::visit::visit_expr_match(self, expr_match);
1336                    }
1337
1338                    fn visit_expr_method_call(&mut self, method_call: &syn::ExprMethodCall) {
1339                        // Check for ? operator (error propagation)
1340                        if let syn::Expr::Try(_) = &*method_call.receiver {
1341                            self.complexity += 1;
1342                        }
1343                        syn::visit::visit_expr_method_call(self, method_call);
1344                    }
1345                }
1346
1347                let mut calculator = ComplexityCalculator { complexity: 1 }; // Base complexity
1348                calculator.visit_block(block);
1349                calculator.complexity
1350            }
1351        }
1352
1353        let mut visitor = ComplexityVisitor {
1354            threshold,
1355            matches: Vec::new(),
1356        };
1357
1358        visitor.visit_file(syntax_tree);
1359        visitor.matches
1360    }
1361
1362    /// Find public items without documentation
1363    fn find_public_without_docs(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String, String)> {
1364        use syn::visit::Visit;
1365
1366        struct PublicDocsVisitor {
1367            matches: Vec<(u32, u32, String, String)>,
1368        }
1369
1370        impl Visit<'_> for PublicDocsVisitor {
1371            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1372                if matches!(func.vis, syn::Visibility::Public(_))
1373                    && !self.has_doc_comment(&func.attrs)
1374                {
1375                    let fn_name = func.sig.ident.to_string();
1376                    let (line, col, context) = (1, 1, format!("pub fn {}", fn_name));
1377                    self.matches
1378                        .push((line, col, format!("fn {}", fn_name), context));
1379                }
1380                syn::visit::visit_item_fn(self, func);
1381            }
1382
1383            fn visit_item_struct(&mut self, item_struct: &syn::ItemStruct) {
1384                if matches!(item_struct.vis, syn::Visibility::Public(_))
1385                    && !self.has_doc_comment(&item_struct.attrs)
1386                {
1387                    let struct_name = item_struct.ident.to_string();
1388                    let (line, col, context) = (1, 1, format!("pub struct {}", struct_name));
1389                    self.matches
1390                        .push((line, col, format!("struct {}", struct_name), context));
1391                }
1392                syn::visit::visit_item_struct(self, item_struct);
1393            }
1394
1395            fn visit_item_enum(&mut self, item_enum: &syn::ItemEnum) {
1396                if matches!(item_enum.vis, syn::Visibility::Public(_))
1397                    && !self.has_doc_comment(&item_enum.attrs)
1398                {
1399                    let enum_name = item_enum.ident.to_string();
1400                    let (line, col, context) = (1, 1, format!("pub enum {}", enum_name));
1401                    self.matches
1402                        .push((line, col, format!("enum {}", enum_name), context));
1403                }
1404                syn::visit::visit_item_enum(self, item_enum);
1405            }
1406
1407            fn visit_item_trait(&mut self, item_trait: &syn::ItemTrait) {
1408                if matches!(item_trait.vis, syn::Visibility::Public(_))
1409                    && !self.has_doc_comment(&item_trait.attrs)
1410                {
1411                    let trait_name = item_trait.ident.to_string();
1412                    let (line, col, context) = (1, 1, format!("pub trait {}", trait_name));
1413                    self.matches
1414                        .push((line, col, format!("trait {}", trait_name), context));
1415                }
1416                syn::visit::visit_item_trait(self, item_trait);
1417            }
1418        }
1419
1420        impl PublicDocsVisitor {
1421            fn has_doc_comment(&self, attrs: &[syn::Attribute]) -> bool {
1422                attrs.iter().any(|attr| {
1423                    attr.path().is_ident("doc")
1424                        || (attr.path().segments.len() == 1
1425                            && attr
1426                                .path()
1427                                .segments
1428                                .first()
1429                                .expect("segments.len() == 1 - first element must exist")
1430                                .ident
1431                                == "doc")
1432                })
1433            }
1434        }
1435
1436        let mut visitor = PublicDocsVisitor {
1437            matches: Vec::new(),
1438        };
1439
1440        visitor.visit_file(syntax_tree);
1441        visitor.matches
1442    }
1443
1444    /// Find functions that are too long
1445    fn find_long_functions(
1446        &self,
1447        syntax_tree: &syn::File,
1448        _content: &str,
1449        threshold: u32,
1450    ) -> Vec<(u32, u32, String, u32, String)> {
1451        use syn::visit::Visit;
1452
1453        struct LongFunctionVisitor {
1454            threshold: u32,
1455            matches: Vec<(u32, u32, String, u32, String)>,
1456        }
1457
1458        impl Visit<'_> for LongFunctionVisitor {
1459            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1460                let fn_name = func.sig.ident.to_string();
1461
1462                // Calculate function line count
1463                let line_count = self.count_function_lines(&func.block);
1464
1465                if line_count > self.threshold {
1466                    let (line, col, context) =
1467                        (1, 1, format!("fn {} ({} lines)", fn_name, line_count));
1468                    self.matches.push((line, col, fn_name, line_count, context));
1469                }
1470
1471                syn::visit::visit_item_fn(self, func);
1472            }
1473        }
1474
1475        impl LongFunctionVisitor {
1476            fn count_function_lines(&self, block: &syn::Block) -> u32 {
1477                // Simple line counting - count non-empty, non-comment lines
1478                let block_str = format!("{}", quote::quote!(#block));
1479                block_str
1480                    .lines()
1481                    .filter(|line| !line.trim().is_empty() && !line.trim().starts_with("//"))
1482                    .count() as u32
1483            }
1484        }
1485
1486        let mut visitor = LongFunctionVisitor {
1487            threshold,
1488            matches: Vec::new(),
1489        };
1490
1491        visitor.visit_file(syntax_tree);
1492        visitor.matches
1493    }
1494
1495    /// Find code with deep nesting
1496    fn find_deep_nesting(
1497        &self,
1498        syntax_tree: &syn::File,
1499        threshold: u32,
1500    ) -> Vec<(u32, u32, u32, String)> {
1501        use syn::visit::Visit;
1502
1503        struct NestingVisitor {
1504            threshold: u32,
1505            current_depth: u32,
1506            matches: Vec<(u32, u32, u32, String)>,
1507        }
1508
1509        impl Visit<'_> for NestingVisitor {
1510            fn visit_block(&mut self, block: &syn::Block) {
1511                self.current_depth += 1;
1512
1513                if self.current_depth > self.threshold {
1514                    let (line, col, context) = (
1515                        1,
1516                        1,
1517                        format!("nested block at depth {}", self.current_depth),
1518                    );
1519                    self.matches.push((line, col, self.current_depth, context));
1520                }
1521
1522                syn::visit::visit_block(self, block);
1523                self.current_depth -= 1;
1524            }
1525
1526            fn visit_expr_if(&mut self, expr_if: &syn::ExprIf) {
1527                self.current_depth += 1;
1528
1529                if self.current_depth > self.threshold {
1530                    let (line, col, context) = (
1531                        1,
1532                        1,
1533                        format!("if statement at depth {}", self.current_depth),
1534                    );
1535                    self.matches.push((line, col, self.current_depth, context));
1536                }
1537
1538                syn::visit::visit_expr_if(self, expr_if);
1539                self.current_depth -= 1;
1540            }
1541
1542            fn visit_expr_match(&mut self, expr_match: &syn::ExprMatch) {
1543                self.current_depth += 1;
1544
1545                if self.current_depth > self.threshold {
1546                    let (line, col, context) = (
1547                        1,
1548                        1,
1549                        format!("match statement at depth {}", self.current_depth),
1550                    );
1551                    self.matches.push((line, col, self.current_depth, context));
1552                }
1553
1554                syn::visit::visit_expr_match(self, expr_match);
1555                self.current_depth -= 1;
1556            }
1557        }
1558
1559        let mut visitor = NestingVisitor {
1560            threshold,
1561            current_depth: 0,
1562            matches: Vec::new(),
1563        };
1564
1565        visitor.visit_file(syntax_tree);
1566        visitor.matches
1567    }
1568
1569    /// Find functions with too many arguments
1570    fn find_functions_with_many_args(
1571        &self,
1572        syntax_tree: &syn::File,
1573        threshold: u32,
1574    ) -> Vec<(u32, u32, String, u32, String)> {
1575        use syn::visit::Visit;
1576
1577        struct ManyArgsVisitor {
1578            threshold: u32,
1579            matches: Vec<(u32, u32, String, u32, String)>,
1580        }
1581
1582        impl Visit<'_> for ManyArgsVisitor {
1583            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1584                let fn_name = func.sig.ident.to_string();
1585                let arg_count = func.sig.inputs.len() as u32;
1586
1587                if arg_count > self.threshold {
1588                    let (line, col, context) =
1589                        (1, 1, format!("fn {} ({} args)", fn_name, arg_count));
1590                    self.matches.push((line, col, fn_name, arg_count, context));
1591                }
1592
1593                syn::visit::visit_item_fn(self, func);
1594            }
1595        }
1596
1597        let mut visitor = ManyArgsVisitor {
1598            threshold,
1599            matches: Vec::new(),
1600        };
1601
1602        visitor.visit_file(syntax_tree);
1603        visitor.matches
1604    }
1605
1606    /// Find blocking calls in async functions
1607    fn find_blocking_in_async(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String, String)> {
1608        use syn::visit::Visit;
1609
1610        struct BlockingInAsyncVisitor {
1611            in_async_fn: bool,
1612            matches: Vec<(u32, u32, String, String)>,
1613        }
1614
1615        impl Visit<'_> for BlockingInAsyncVisitor {
1616            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1617                let was_async = self.in_async_fn;
1618                self.in_async_fn = func.sig.asyncness.is_some();
1619
1620                syn::visit::visit_item_fn(self, func);
1621                self.in_async_fn = was_async;
1622            }
1623
1624            fn visit_expr_method_call(&mut self, method_call: &syn::ExprMethodCall) {
1625                if self.in_async_fn {
1626                    let method_name = method_call.method.to_string();
1627
1628                    // Common blocking operations
1629                    if [
1630                        "read_to_string",
1631                        "write_all",
1632                        "flush",
1633                        "recv",
1634                        "send",
1635                        "lock",
1636                        "read",
1637                        "write",
1638                    ]
1639                    .contains(&method_name.as_str())
1640                    {
1641                        // Check if it's not awaited
1642                        let (line, col, context) = (1, 1, format!(".{}()", method_name));
1643                        self.matches.push((line, col, method_name, context));
1644                    }
1645                }
1646
1647                syn::visit::visit_expr_method_call(self, method_call);
1648            }
1649
1650            fn visit_expr_call(&mut self, call: &syn::ExprCall) {
1651                if self.in_async_fn {
1652                    if let syn::Expr::Path(path) = &*call.func {
1653                        if let Some(segment) = path.path.segments.last() {
1654                            let fn_name = segment.ident.to_string();
1655
1656                            // Common blocking functions
1657                            if ["thread::sleep", "std::thread::sleep", "sleep"]
1658                                .contains(&fn_name.as_str())
1659                            {
1660                                let (line, col, context) = (1, 1, format!("{}()", fn_name));
1661                                self.matches.push((line, col, fn_name, context));
1662                            }
1663                        }
1664                    }
1665                }
1666
1667                syn::visit::visit_expr_call(self, call);
1668            }
1669        }
1670
1671        let mut visitor = BlockingInAsyncVisitor {
1672            in_async_fn: false,
1673            matches: Vec::new(),
1674        };
1675
1676        visitor.visit_file(syntax_tree);
1677        visitor.matches
1678    }
1679
1680    /// Find futures that are not awaited
1681    fn find_futures_not_awaited(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String, String)> {
1682        use syn::visit::Visit;
1683
1684        struct FutureNotAwaitedVisitor {
1685            matches: Vec<(u32, u32, String, String)>,
1686        }
1687
1688        impl Visit<'_> for FutureNotAwaitedVisitor {
1689            fn visit_expr_call(&mut self, call: &syn::ExprCall) {
1690                // Look for function calls that return futures but aren't awaited
1691                if let syn::Expr::Path(path) = &*call.func {
1692                    if let Some(segment) = path.path.segments.last() {
1693                        let fn_name = segment.ident.to_string();
1694
1695                        // Common async functions that return futures
1696                        if fn_name.ends_with("_async")
1697                            || ["spawn", "spawn_blocking", "timeout", "sleep"]
1698                                .contains(&fn_name.as_str())
1699                        {
1700                            let (line, col, context) = (1, 1, format!("{}() not awaited", fn_name));
1701                            self.matches
1702                                .push((line, col, format!("{}()", fn_name), context));
1703                        }
1704                    }
1705                }
1706
1707                syn::visit::visit_expr_call(self, call);
1708            }
1709        }
1710
1711        let mut visitor = FutureNotAwaitedVisitor {
1712            matches: Vec::new(),
1713        };
1714
1715        visitor.visit_file(syntax_tree);
1716        visitor.matches
1717    }
1718
1719    /// Find tokio::select! without biased
1720    fn find_select_without_biased(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String)> {
1721        use syn::visit::Visit;
1722
1723        struct SelectVisitor {
1724            matches: Vec<(u32, u32, String)>,
1725        }
1726
1727        impl Visit<'_> for SelectVisitor {
1728            fn visit_macro(&mut self, mac: &syn::Macro) {
1729                if let Some(ident) = mac.path.get_ident() {
1730                    if ident == "select" {
1731                        // Check if it's tokio::select!
1732                        let macro_str = format!("{}", quote::quote!(#mac));
1733                        if macro_str.contains("select!") && !macro_str.contains("biased") {
1734                            let (line, col, context) =
1735                                (1, 1, "tokio::select! without biased".to_string());
1736                            self.matches.push((line, col, context));
1737                        }
1738                    }
1739                }
1740                syn::visit::visit_macro(self, mac);
1741            }
1742        }
1743
1744        let mut visitor = SelectVisitor {
1745            matches: Vec::new(),
1746        };
1747
1748        visitor.visit_file(syntax_tree);
1749        visitor.matches
1750    }
1751
1752    /// Find generics without trait bounds
1753    fn find_generics_without_bounds(
1754        &self,
1755        syntax_tree: &syn::File,
1756    ) -> Vec<(u32, u32, String, String)> {
1757        use syn::visit::Visit;
1758
1759        struct GenericBoundsVisitor {
1760            matches: Vec<(u32, u32, String, String)>,
1761        }
1762
1763        impl Visit<'_> for GenericBoundsVisitor {
1764            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1765                for param in &func.sig.generics.params {
1766                    if let syn::GenericParam::Type(type_param) = param {
1767                        if type_param.bounds.is_empty() {
1768                            let generic_name = type_param.ident.to_string();
1769                            let (line, col, context) = (1, 1, format!("<{}>", generic_name));
1770                            self.matches.push((line, col, generic_name, context));
1771                        }
1772                    }
1773                }
1774
1775                syn::visit::visit_item_fn(self, func);
1776            }
1777
1778            fn visit_item_struct(&mut self, item_struct: &syn::ItemStruct) {
1779                for param in &item_struct.generics.params {
1780                    if let syn::GenericParam::Type(type_param) = param {
1781                        if type_param.bounds.is_empty() {
1782                            let generic_name = type_param.ident.to_string();
1783                            let (line, col, context) = (
1784                                1,
1785                                1,
1786                                format!("struct {}<{}>", item_struct.ident, generic_name),
1787                            );
1788                            self.matches.push((line, col, generic_name, context));
1789                        }
1790                    }
1791                }
1792
1793                syn::visit::visit_item_struct(self, item_struct);
1794            }
1795        }
1796
1797        let mut visitor = GenericBoundsVisitor {
1798            matches: Vec::new(),
1799        };
1800
1801        visitor.visit_file(syntax_tree);
1802        visitor.matches
1803    }
1804
1805    /// Find test functions without assertions
1806    fn find_test_functions_without_assertions(
1807        &self,
1808        syntax_tree: &syn::File,
1809    ) -> Vec<(u32, u32, String, String)> {
1810        use syn::visit::Visit;
1811
1812        struct TestAssertionVisitor {
1813            matches: Vec<(u32, u32, String, String)>,
1814        }
1815
1816        impl Visit<'_> for TestAssertionVisitor {
1817            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1818                // Check if function has #[test] attribute
1819                let is_test = func.attrs.iter().any(|attr| attr.path().is_ident("test"));
1820
1821                if is_test {
1822                    let fn_name = func.sig.ident.to_string();
1823
1824                    // Check if function body contains assertions
1825                    if !self.has_assertions(&func.block) {
1826                        let (line, col, context) = (1, 1, format!("#[test] fn {}", fn_name));
1827                        self.matches.push((line, col, fn_name, context));
1828                    }
1829                }
1830
1831                syn::visit::visit_item_fn(self, func);
1832            }
1833        }
1834
1835        impl TestAssertionVisitor {
1836            fn has_assertions(&self, block: &syn::Block) -> bool {
1837                use syn::visit::Visit;
1838
1839                struct AssertionFinder {
1840                    found: bool,
1841                }
1842
1843                impl Visit<'_> for AssertionFinder {
1844                    fn visit_expr_macro(&mut self, expr_macro: &syn::ExprMacro) {
1845                        if let Some(ident) = expr_macro.mac.path.get_ident() {
1846                            let macro_name = ident.to_string();
1847                            if macro_name.starts_with("assert") {
1848                                self.found = true;
1849                            }
1850                        }
1851                        syn::visit::visit_expr_macro(self, expr_macro);
1852                    }
1853
1854                    fn visit_expr_call(&mut self, call: &syn::ExprCall) {
1855                        if let syn::Expr::Path(path) = &*call.func {
1856                            if let Some(segment) = path.path.segments.last() {
1857                                let fn_name = segment.ident.to_string();
1858                                if fn_name.starts_with("assert") || fn_name == "panic" {
1859                                    self.found = true;
1860                                }
1861                            }
1862                        }
1863                        syn::visit::visit_expr_call(self, call);
1864                    }
1865                }
1866
1867                let mut finder = AssertionFinder { found: false };
1868                finder.visit_block(block);
1869                finder.found
1870            }
1871        }
1872
1873        let mut visitor = TestAssertionVisitor {
1874            matches: Vec::new(),
1875        };
1876
1877        visitor.visit_file(syntax_tree);
1878        visitor.matches
1879    }
1880
1881    /// Find impl blocks without traits
1882    fn find_impl_without_trait(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String, String)> {
1883        use syn::visit::Visit;
1884
1885        struct ImplTraitVisitor {
1886            matches: Vec<(u32, u32, String, String)>,
1887        }
1888
1889        impl Visit<'_> for ImplTraitVisitor {
1890            fn visit_item_impl(&mut self, impl_item: &syn::ItemImpl) {
1891                // Check if this is an inherent impl (no trait)
1892                if impl_item.trait_.is_none() {
1893                    let type_name = match &*impl_item.self_ty {
1894                        syn::Type::Path(type_path) => type_path
1895                            .path
1896                            .segments
1897                            .last()
1898                            .map(|s| s.ident.to_string())
1899                            .unwrap_or_else(|| "Unknown".to_string()),
1900                        _ => "Unknown".to_string(),
1901                    };
1902
1903                    let (line, col, context) = (1, 1, format!("impl {}", type_name));
1904                    self.matches.push((line, col, type_name, context));
1905                }
1906
1907                syn::visit::visit_item_impl(self, impl_item);
1908            }
1909        }
1910
1911        let mut visitor = ImplTraitVisitor {
1912            matches: Vec::new(),
1913        };
1914
1915        visitor.visit_file(syntax_tree);
1916        visitor.matches
1917    }
1918
1919    /// Find unsafe blocks
1920    fn find_unsafe_blocks(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String)> {
1921        use syn::visit::Visit;
1922
1923        struct UnsafeVisitor {
1924            matches: Vec<(u32, u32, String)>,
1925        }
1926
1927        impl Visit<'_> for UnsafeVisitor {
1928            fn visit_expr_unsafe(&mut self, expr: &syn::ExprUnsafe) {
1929                let (line, col, context) = (1, 1, "unsafe block".to_string());
1930                self.matches.push((line, col, context));
1931
1932                syn::visit::visit_expr_unsafe(self, expr);
1933            }
1934
1935            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1936                if func.sig.unsafety.is_some() {
1937                    let fn_name = func.sig.ident.to_string();
1938                    let (line, col, context) = (1, 1, format!("unsafe fn {}", fn_name));
1939                    self.matches.push((line, col, context));
1940                }
1941
1942                syn::visit::visit_item_fn(self, func);
1943            }
1944        }
1945
1946        let mut visitor = UnsafeVisitor {
1947            matches: Vec::new(),
1948        };
1949
1950        visitor.visit_file(syntax_tree);
1951        visitor.matches
1952    }
1953
1954    /// Find ignored test functions
1955    fn find_ignored_tests(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String, String)> {
1956        use syn::visit::Visit;
1957
1958        struct IgnoredTestVisitor {
1959            matches: Vec<(u32, u32, String, String)>,
1960        }
1961
1962        impl Visit<'_> for IgnoredTestVisitor {
1963            fn visit_item_fn(&mut self, func: &syn::ItemFn) {
1964                // Check if function has both #[test] and #[ignore] attributes
1965                let is_test = func.attrs.iter().any(|attr| attr.path().is_ident("test"));
1966                let is_ignored = func.attrs.iter().any(|attr| attr.path().is_ident("ignore"));
1967
1968                if is_test && is_ignored {
1969                    let fn_name = func.sig.ident.to_string();
1970                    let (line, col, context) = (1, 1, format!("#[ignore] #[test] fn {}", fn_name));
1971                    self.matches.push((line, col, fn_name, context));
1972                }
1973
1974                syn::visit::visit_item_fn(self, func);
1975            }
1976        }
1977
1978        let mut visitor = IgnoredTestVisitor {
1979            matches: Vec::new(),
1980        };
1981
1982        visitor.visit_file(syntax_tree);
1983        visitor.matches
1984    }
1985
1986    /// Convert pattern matches to violations
1987    pub fn matches_to_violations(&self, matches: Vec<PatternMatch>) -> Vec<Violation> {
1988        matches
1989            .into_iter()
1990            .map(|m| {
1991                let mut violation = Violation::new(m.rule_id, m.severity, m.file_path, m.message);
1992
1993                if let Some(line) = m.line_number {
1994                    if let Some(col) = m.column_number {
1995                        violation = violation.with_position(line, col);
1996                    }
1997                }
1998
1999                if let Some(context) = m.context {
2000                    violation = violation.with_context(context);
2001                }
2002
2003                violation
2004            })
2005            .collect()
2006    }
2007}
2008
2009impl Default for PatternEngine {
2010    fn default() -> Self {
2011        Self::new()
2012    }
2013}
2014
2015/// Architecture-compliant validation functions for integration testing
2016#[allow(dead_code)]
2017pub mod validation {
2018    use super::*;
2019    use crate::config::PatternRule;
2020
2021    /// Validate regex pattern functionality - designed for integration testing
2022    pub fn validate_regex_pattern_functionality() -> crate::domain::violations::GuardianResult<()> {
2023        let mut engine = PatternEngine::new();
2024
2025        let rule = PatternRule {
2026            id: "todo_validation".to_string(),
2027            rule_type: RuleType::Regex,
2028            pattern: r"\bTODO\b".to_string(),
2029            message: "TODO found: {match}".to_string(),
2030            severity: None,
2031            enabled: true,
2032            case_sensitive: true,
2033            exclude_if: None,
2034        };
2035
2036        engine.add_rule(&rule, Severity::Warning)?;
2037
2038        let content = "// TODO: implement this\nlet x = 5;";
2039        let matches = engine.analyze_file(Path::new("validation.rs"), content)?;
2040
2041        if matches.len() != 1
2042            || matches[0].rule_id != "todo_validation"
2043            || matches[0].matched_text != "TODO"
2044        {
2045            return Err(crate::domain::violations::GuardianError::pattern(
2046                "Regex pattern validation failed - incorrect match results",
2047            ));
2048        }
2049
2050        Ok(())
2051    }
2052
2053    /// Validate AST pattern functionality - designed for integration testing
2054    pub fn validate_ast_pattern_functionality() -> crate::domain::violations::GuardianResult<()> {
2055        let mut engine = PatternEngine::new();
2056
2057        let rule = PatternRule {
2058            id: "unimplemented_validation".to_string(),
2059            rule_type: RuleType::Ast,
2060            pattern: "macro_call:unimplemented|todo".to_string(),
2061            message: "Unfinished macro: {macro_name}".to_string(),
2062            severity: None,
2063            enabled: true,
2064            case_sensitive: true,
2065            exclude_if: None,
2066        };
2067
2068        engine.add_rule(&rule, Severity::Error)?;
2069
2070        let content = "fn validation() {\n    unimplemented!()\n}";
2071        let matches = engine.analyze_file(Path::new("validation.rs"), content)?;
2072
2073        if matches.len() != 1
2074            || matches[0].rule_id != "unimplemented_validation"
2075            || !matches[0].message.contains("unimplemented")
2076        {
2077            return Err(crate::domain::violations::GuardianError::pattern(
2078                "AST pattern validation failed - incorrect match results",
2079            ));
2080        }
2081
2082        Ok(())
2083    }
2084
2085    /// Validate exclude conditions functionality - designed for integration testing
2086    pub fn validate_exclude_conditions_functionality(
2087    ) -> crate::domain::violations::GuardianResult<()> {
2088        let mut engine = PatternEngine::new();
2089
2090        let rule = PatternRule {
2091            id: "todo_exclusion_validation".to_string(),
2092            rule_type: RuleType::Regex,
2093            pattern: r"\bTODO\b".to_string(),
2094            message: "TODO found: {match}".to_string(),
2095            severity: None,
2096            enabled: true,
2097            case_sensitive: true,
2098            exclude_if: Some(ExcludeConditions {
2099                attribute: None,
2100                in_tests: true,
2101                file_patterns: None,
2102            }),
2103        };
2104
2105        engine.add_rule(&rule, Severity::Warning)?;
2106
2107        let content = "// TODO: implement this";
2108
2109        // Should match in regular file
2110        let matches = engine.analyze_file(Path::new("src/lib.rs"), content)?;
2111        if matches.len() != 1 {
2112            return Err(crate::domain::violations::GuardianError::pattern(
2113                "Exclude conditions validation failed - should match in regular file",
2114            ));
2115        }
2116
2117        // Should be excluded in test file
2118        let matches = engine.analyze_file(Path::new("tests/unit.rs"), content)?;
2119        if !matches.is_empty() {
2120            return Err(crate::domain::violations::GuardianError::pattern(
2121                "Exclude conditions validation failed - should be excluded in test file",
2122            ));
2123        }
2124
2125        Ok(())
2126    }
2127}