@@ -54,6 +54,14 @@ enum AstPatternType {
5454 EmptyOkReturn ,
5555 /// Look for missing CDD headers in files
5656 MissingCddHeader ,
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 direct substrate access (semantic pattern)
62+ DirectSubstrateAccess ( regex:: Regex ) ,
63+ /// Look for domain modules importing infrastructure (semantic pattern)
64+ DomainImportsInfrastructure ( regex:: Regex ) ,
5765}
5866
5967/// A match found by a pattern
@@ -110,8 +118,15 @@ impl PatternEngine {
110118 } ) ;
111119 }
112120 RuleType :: Semantic | RuleType :: ImportAnalysis => {
113- // TODO: Implement semantic and import analysis patterns
114- tracing:: warn!( "Semantic and import analysis patterns not yet implemented: {}" , rule. id) ;
121+ let pattern_type = self . parse_semantic_pattern ( & rule. pattern , & rule. id ) ?;
122+
123+ self . ast_patterns . insert ( rule. id . clone ( ) , AstPattern {
124+ pattern_type,
125+ rule_id : rule. id . clone ( ) ,
126+ message_template : rule. message . clone ( ) ,
127+ severity : effective_severity,
128+ exclude_conditions : rule. exclude_if . clone ( ) ,
129+ } ) ;
115130 }
116131 }
117132
@@ -131,11 +146,30 @@ impl PatternEngine {
131146 Ok ( AstPatternType :: EmptyOkReturn )
132147 } else if pattern. contains ( "CDD Principle:" ) {
133148 Ok ( AstPatternType :: MissingCddHeader )
149+ } else if pattern == "empty_function_body" {
150+ Ok ( AstPatternType :: EmptyFunctionBody )
151+ } else if pattern == "unwrap_or_expect_without_message" {
152+ Ok ( AstPatternType :: UnwrapOrExpectWithoutMessage )
134153 } else {
135154 Err ( GuardianError :: pattern ( format ! ( "Unknown AST pattern type in rule '{}': {}" , rule_id, pattern) ) )
136155 }
137156 }
138157
158+ /// Parse semantic pattern string into typed pattern
159+ fn parse_semantic_pattern ( & self , pattern : & str , rule_id : & str ) -> GuardianResult < AstPatternType > {
160+ // Build regex for semantic patterns
161+ let regex = regex:: Regex :: new ( pattern)
162+ . map_err ( |e| GuardianError :: pattern ( format ! ( "Invalid semantic pattern regex in rule '{}': {}" , rule_id, e) ) ) ?;
163+
164+ if pattern. contains ( "substrate" ) && !pattern. contains ( "traits" ) {
165+ Ok ( AstPatternType :: DirectSubstrateAccess ( regex) )
166+ } else if pattern. contains ( "infrastructure" ) {
167+ Ok ( AstPatternType :: DomainImportsInfrastructure ( regex) )
168+ } else {
169+ Err ( GuardianError :: pattern ( format ! ( "Unknown semantic pattern type in rule '{}': {}" , rule_id, pattern) ) )
170+ }
171+ }
172+
139173 /// Analyze a file and return all pattern matches
140174 pub fn analyze_file < P : AsRef < Path > > ( & self , file_path : P , content : & str ) -> GuardianResult < Vec < PatternMatch > > {
141175 let file_path = file_path. as_ref ( ) ;
@@ -226,6 +260,46 @@ impl PatternEngine {
226260 } ) ;
227261 }
228262 }
263+ AstPatternType :: DirectSubstrateAccess ( regex) => {
264+ let found_matches = self . find_import_pattern_matches ( & syntax_tree, content, regex) ;
265+ for ( line, col, import_text, context) in found_matches {
266+ // Check exclude conditions
267+ if self . should_exclude_ast_match ( pattern. exclude_conditions . as_ref ( ) , file_path, & syntax_tree, line) {
268+ continue ;
269+ }
270+
271+ matches. push ( PatternMatch {
272+ rule_id : pattern. rule_id . clone ( ) ,
273+ file_path : file_path. to_path_buf ( ) ,
274+ line_number : Some ( line) ,
275+ column_number : Some ( col) ,
276+ matched_text : import_text,
277+ message : pattern. message_template . clone ( ) ,
278+ severity : pattern. severity ,
279+ context : Some ( context) ,
280+ } ) ;
281+ }
282+ }
283+ AstPatternType :: DomainImportsInfrastructure ( regex) => {
284+ let found_matches = self . find_import_pattern_matches ( & syntax_tree, content, regex) ;
285+ for ( line, col, import_text, context) in found_matches {
286+ // Check exclude conditions
287+ if self . should_exclude_ast_match ( pattern. exclude_conditions . as_ref ( ) , file_path, & syntax_tree, line) {
288+ continue ;
289+ }
290+
291+ matches. push ( PatternMatch {
292+ rule_id : pattern. rule_id . clone ( ) ,
293+ file_path : file_path. to_path_buf ( ) ,
294+ line_number : Some ( line) ,
295+ column_number : Some ( col) ,
296+ matched_text : import_text,
297+ message : pattern. message_template . clone ( ) ,
298+ severity : pattern. severity ,
299+ context : Some ( context) ,
300+ } ) ;
301+ }
302+ }
229303 AstPatternType :: EmptyOkReturn => {
230304 let found_matches = self . find_empty_ok_returns ( & syntax_tree) ;
231305 for ( line, col, context) in found_matches {
@@ -260,6 +334,50 @@ impl PatternEngine {
260334 } ) ;
261335 }
262336 }
337+ AstPatternType :: EmptyFunctionBody => {
338+ let found_matches = self . find_empty_function_bodies ( & syntax_tree) ;
339+ for ( line, col, fn_name, context) in found_matches {
340+ // Check exclude conditions
341+ if self . should_exclude_ast_match ( pattern. exclude_conditions . as_ref ( ) , file_path, & syntax_tree, line) {
342+ continue ;
343+ }
344+
345+ let message = pattern. message_template . replace ( "{function_name}" , & fn_name) ;
346+
347+ matches. push ( PatternMatch {
348+ rule_id : pattern. rule_id . clone ( ) ,
349+ file_path : file_path. to_path_buf ( ) ,
350+ line_number : Some ( line) ,
351+ column_number : Some ( col) ,
352+ matched_text : format ! ( "fn {}" , fn_name) ,
353+ message,
354+ severity : pattern. severity ,
355+ context : Some ( context) ,
356+ } ) ;
357+ }
358+ }
359+ AstPatternType :: UnwrapOrExpectWithoutMessage => {
360+ let found_matches = self . find_unwrap_without_message ( & syntax_tree) ;
361+ for ( line, col, method_name, context) in found_matches {
362+ // Check exclude conditions
363+ if self . should_exclude_ast_match ( pattern. exclude_conditions . as_ref ( ) , file_path, & syntax_tree, line) {
364+ continue ;
365+ }
366+
367+ let message = pattern. message_template . replace ( "{method}" , & method_name) ;
368+
369+ matches. push ( PatternMatch {
370+ rule_id : pattern. rule_id . clone ( ) ,
371+ file_path : file_path. to_path_buf ( ) ,
372+ line_number : Some ( line) ,
373+ column_number : Some ( col) ,
374+ matched_text : format ! ( ".{}()" , method_name) ,
375+ message,
376+ severity : pattern. severity ,
377+ context : Some ( context) ,
378+ } ) ;
379+ }
380+ }
263381 }
264382
265383 Ok ( matches)
@@ -280,10 +398,10 @@ impl PatternEngine {
280398 let macro_name = ident. to_string ( ) ;
281399 if self . target_macros . contains ( & macro_name) {
282400 let _span = mac. path . span ( ) ;
283- // Use a simple line-based location since proc_macro2::Span doesn't have start() method
284- // Use a simple line-based location since proc_macro2::Span doesn't have start() method
285- let ( line , col , context) = ( 1 , 1 , String :: new ( ) ) ;
286- self . matches . push ( ( line , col , macro_name, context) ) ;
401+ // proc_macro2::Span doesn't provide direct line/column access in stable Rust
402+ // For now, use line 1 but provide better context
403+ let context = format ! ( "{}!()" , macro_name ) ;
404+ self . matches . push ( ( 1 , 1 , macro_name, context) ) ;
287405 }
288406 }
289407 syn:: visit:: visit_macro ( self , mac) ;
@@ -299,6 +417,7 @@ impl PatternEngine {
299417 visitor. matches
300418 }
301419
420+
302421 /// Find functions that return empty Ok(()) responses
303422 fn find_empty_ok_returns ( & self , syntax_tree : & syn:: File ) -> Vec < ( u32 , u32 , String ) > {
304423 use syn:: visit:: Visit ;
@@ -379,6 +498,133 @@ impl PatternEngine {
379498 visitor. matches
380499 }
381500
501+ /// Find functions with empty bodies
502+ fn find_empty_function_bodies ( & self , syntax_tree : & syn:: File ) -> Vec < ( u32 , u32 , String , String ) > {
503+ use syn:: visit:: Visit ;
504+
505+ struct EmptyBodyVisitor {
506+ matches : Vec < ( u32 , u32 , String , String ) > ,
507+ }
508+
509+ impl Visit < ' _ > for EmptyBodyVisitor {
510+ fn visit_item_fn ( & mut self , func : & syn:: ItemFn ) {
511+ let fn_name = func. sig . ident . to_string ( ) ;
512+
513+ // Check if function body is empty or has only comments/whitespace
514+ if func. block . stmts . is_empty ( ) {
515+ // Function has completely empty body
516+ let ( line, col, context) = ( 1 , 1 , format ! ( "fn {} {{ }}" , fn_name) ) ;
517+ self . matches . push ( ( line, col, fn_name, context) ) ;
518+ } else if func. block . stmts . len ( ) == 1 {
519+ // Check if the single statement is just a comment or empty expression
520+ if let syn:: Stmt :: Expr ( expr, _) = & func. block . stmts [ 0 ] {
521+ if matches ! ( expr, syn:: Expr :: Tuple ( tuple) if tuple. elems. is_empty( ) ) {
522+ // Function body contains only ()
523+ let ( line, col, context) = ( 1 , 1 , format ! ( "fn {} {{ () }}" , fn_name) ) ;
524+ self . matches . push ( ( line, col, fn_name, context) ) ;
525+ }
526+ }
527+ }
528+
529+ syn:: visit:: visit_item_fn ( self , func) ;
530+ }
531+ }
532+
533+ let mut visitor = EmptyBodyVisitor {
534+ matches : Vec :: new ( ) ,
535+ } ;
536+
537+ visitor. visit_file ( syntax_tree) ;
538+ visitor. matches
539+ }
540+
541+ /// Find unwrap() or expect() calls without meaningful error messages
542+ fn find_unwrap_without_message ( & self , syntax_tree : & syn:: File ) -> Vec < ( u32 , u32 , String , String ) > {
543+ use syn:: visit:: Visit ;
544+
545+ struct UnwrapVisitor {
546+ matches : Vec < ( u32 , u32 , String , String ) > ,
547+ }
548+
549+ impl Visit < ' _ > for UnwrapVisitor {
550+ fn visit_expr_method_call ( & mut self , method_call : & syn:: ExprMethodCall ) {
551+ let method_name = method_call. method . to_string ( ) ;
552+
553+ match method_name. as_str ( ) {
554+ "unwrap" => {
555+ // unwrap() calls are always problematic
556+ let ( line, col, context) = ( 1 , 1 , format ! ( ".unwrap()" ) ) ;
557+ self . matches . push ( ( line, col, "unwrap" . to_string ( ) , context) ) ;
558+ }
559+ "expect" => {
560+ // Check if expect() has a meaningful message
561+ if method_call. args . is_empty ( ) {
562+ // expect() without any message
563+ let ( line, col, context) = ( 1 , 1 , format ! ( ".expect()" ) ) ;
564+ self . matches . push ( ( line, col, "expect" . to_string ( ) , context) ) ;
565+ } else if let syn:: Expr :: Lit ( syn:: ExprLit { lit : syn:: Lit :: Str ( lit_str) , .. } ) = & method_call. args [ 0 ] {
566+ let message = lit_str. value ( ) ;
567+ // Check for generic/unhelpful messages
568+ if message. is_empty ( ) ||
569+ message. len ( ) < 5 ||
570+ message. to_lowercase ( ) . contains ( "error" ) && message. len ( ) < 10 {
571+ let ( line, col, context) = ( 1 , 1 , format ! ( ".expect(\" {}\" )" , message) ) ;
572+ self . matches . push ( ( line, col, "expect" . to_string ( ) , context) ) ;
573+ }
574+ }
575+ }
576+ _ => { }
577+ }
578+
579+ syn:: visit:: visit_expr_method_call ( self , method_call) ;
580+ }
581+ }
582+
583+ let mut visitor = UnwrapVisitor {
584+ matches : Vec :: new ( ) ,
585+ } ;
586+
587+ visitor. visit_file ( syntax_tree) ;
588+ visitor. matches
589+ }
590+
591+ /// Find import patterns using regex matching on use statements
592+ fn find_import_pattern_matches ( & self , syntax_tree : & syn:: File , content : & str , regex : & regex:: Regex ) -> Vec < ( u32 , u32 , String , String ) > {
593+ use syn:: visit:: Visit ;
594+
595+ struct ImportVisitor < ' a > {
596+ regex : & ' a regex:: Regex ,
597+ content : & ' a str ,
598+ matches : Vec < ( u32 , u32 , String , String ) > ,
599+ }
600+
601+ impl < ' a > Visit < ' _ > for ImportVisitor < ' a > {
602+ fn visit_item_use ( & mut self , use_item : & syn:: ItemUse ) {
603+ // Convert the use statement back to string for regex matching
604+ let use_string = format ! ( "use {};" , quote:: quote!( #use_item) . to_string( ) . trim_start_matches( "use " ) ) ;
605+
606+ if self . regex . is_match ( & use_string) {
607+ // Extract line information from the use statement
608+ // For now, use simple line tracking - in a real implementation,
609+ // we'd use syn span information for precise location
610+ let ( line, col, context) = ( 1 , 1 , use_string. clone ( ) ) ;
611+ self . matches . push ( ( line, col, use_string, context) ) ;
612+ }
613+
614+ syn:: visit:: visit_item_use ( self , use_item) ;
615+ }
616+ }
617+
618+ let mut visitor = ImportVisitor {
619+ regex,
620+ content,
621+ matches : Vec :: new ( ) ,
622+ } ;
623+
624+ visitor. visit_file ( syntax_tree) ;
625+ visitor. matches
626+ }
627+
382628 /// Get line and column number from byte offset in content
383629 fn get_match_location ( & self , content : & str , byte_offset : usize ) -> ( u32 , u32 , String ) {
384630 let mut line = 1 ;
0 commit comments