Skip to content

Commit 706fe8f

Browse files
author
Jeshua ben Joseph
committed
Fixing advanced patterns
1 parent 84f8c3a commit 706fe8f

File tree

1 file changed

+252
-6
lines changed

1 file changed

+252
-6
lines changed

src/patterns/mod.rs

Lines changed: 252 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)