1pub 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#[derive(Debug)]
21pub struct PatternEngine {
22 regex_patterns: HashMap<String, CompiledRegex>,
24 ast_patterns: HashMap<String, AstPattern>,
26}
27
28#[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#[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#[derive(Debug, Clone)]
50enum AstPatternType {
51 MacroCall(Vec<String>),
53 EmptyOkReturn,
55 MissingArchitecturalHeader,
57 EmptyFunctionBody,
59 UnwrapOrExpectWithoutMessage,
61 AbstractionLayerViolation(regex::Regex),
63 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#[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 pub fn new() -> Self {
95 Self {
96 regex_patterns: HashMap::new(),
97 ast_patterns: HashMap::new(),
98 }
99 }
100
101 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 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 fn parse_semantic_pattern(
208 &self,
209 pattern: &str,
210 rule_id: &str,
211 ) -> GuardianResult<AstPatternType> {
212 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 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 if pattern.starts_with("use")
254 || pattern.starts_with("import:")
255 || pattern.contains("_access")
256 {
257 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 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 Err(GuardianError::pattern(format!(
280 "Unknown semantic pattern type in rule '{rule_id}': {pattern}"
281 )))
282 }
283 }
284 }
285 }
286
287 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 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 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 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 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 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 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 let syntax_tree = match syn::parse_file(content) {
394 Ok(tree) => tree,
395 Err(e) => {
396 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 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}", ¯o_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 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 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 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 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 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(¯o_name) {
904 let _span = mac.path.span();
905 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 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 if let syn::ReturnType::Type(_, return_type) = &func.sig.output {
935 if self.is_result_type(return_type) {
936 if let Some(ok_expr) = self.find_ok_unit_return(&func.block) {
938 let _span = ok_expr.span();
939 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 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 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 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 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 if func.block.stmts.is_empty() {
1024 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 if let syn::Stmt::Expr(expr, _) = &func.block.stmts[0] {
1030 if matches!(expr, syn::Expr::Tuple(tuple) if tuple.elems.is_empty()) {
1031 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 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 let (line, col, context) = (1, 1, ".unwrap()".to_string());
1069 self.matches
1070 .push((line, col, "unwrap".to_string(), context));
1071 }
1072 "expect" => {
1073 if method_call.args.is_empty() {
1075 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 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 fn find_import_pattern_matches(
1114 &self,
1115 syntax_tree: &syn::File,
1116 _content: &str,
1117 regex: ®ex::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 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 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 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 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 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 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 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 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 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 if conditions.in_tests && self.is_test_file(file_path) {
1241 return true;
1242 }
1243
1244 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 }
1257
1258 false
1259 }
1260
1261 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 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 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 }; 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[allow(dead_code)]
2017pub mod validation {
2018 use super::*;
2019 use crate::config::PatternRule;
2020
2021 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 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 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 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 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}