use quote::ToTokens;
use syn::{visit::Visit, Block, Expr, ExprIf, Stmt};
#[derive(Debug, Clone)]
pub struct PatternMatchInfo {
pub variable_name: String,
pub condition_count: usize,
pub has_default: bool,
pub pattern_type: PatternType,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatternType {
StringMatching, EnumMatching, RangeMatching, TypeChecking, SimpleComparison, TraitDelegation, SerializationDispatch, }
pub trait PatternRecognizer {
fn detect(&self, block: &Block) -> Option<PatternMatchInfo>;
fn adjust_complexity(&self, info: &PatternMatchInfo, base: u32) -> u32;
}
pub struct PatternMatchRecognizer;
impl Default for PatternMatchRecognizer {
fn default() -> Self {
Self
}
}
impl PatternMatchRecognizer {
pub fn new() -> Self {
Self
}
pub fn has_immediate_return(&self, block: &Block) -> bool {
if block.stmts.is_empty() || block.stmts.len() > 2 {
return false;
}
block
.stmts
.iter()
.any(|stmt| matches!(stmt, Stmt::Expr(Expr::Return(_), _)))
}
pub fn extract_tested_variable(&self, expr: &Expr) -> Option<String> {
match expr {
Expr::MethodCall(method) => {
if let Expr::Path(path) = &*method.receiver {
if let Some(ident) = path.path.get_ident() {
return Some(ident.to_string());
}
}
if let Expr::Field(field) = &*method.receiver {
return self.extract_field_path(&field.base);
}
None
}
Expr::Binary(binary) => {
if let Expr::Path(path) = &*binary.left {
if let Some(ident) = path.path.get_ident() {
return Some(ident.to_string());
}
}
if let Expr::Path(path) = &*binary.right {
if let Some(ident) = path.path.get_ident() {
return Some(ident.to_string());
}
}
None
}
Expr::Unary(unary) => self.extract_tested_variable(&unary.expr),
Expr::Paren(paren) => self.extract_tested_variable(&paren.expr),
Expr::Path(path) => path.path.get_ident().map(|ident| ident.to_string()),
_ => None,
}
}
pub fn extract_field_path(&self, expr: &Expr) -> Option<String> {
let mut path_parts = Vec::new();
let mut current = expr;
loop {
match current {
Expr::Field(field) => {
path_parts.push(field.member.to_token_stream().to_string());
current = &field.base;
}
Expr::Path(path) => {
if let Some(ident) = path.path.get_ident() {
path_parts.push(ident.to_string());
}
break;
}
_ => break,
}
}
if !path_parts.is_empty() {
path_parts.reverse();
Some(path_parts.join("."))
} else {
None
}
}
pub fn detect_pattern_type(&self, expr: &Expr) -> PatternType {
match expr {
Expr::MethodCall(method) => {
let method_name = method.method.to_string();
match method_name.as_str() {
"ends_with" | "starts_with" | "contains" => PatternType::StringMatching,
_ => PatternType::SimpleComparison,
}
}
Expr::Binary(binary) => {
use syn::BinOp;
match binary.op {
BinOp::Eq(_) | BinOp::Ne(_) => PatternType::SimpleComparison,
BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_) | BinOp::Ge(_) => {
PatternType::RangeMatching
}
_ => PatternType::SimpleComparison,
}
}
_ => PatternType::SimpleComparison,
}
}
fn detect_pattern_matching(&self, block: &Block) -> Option<PatternMatchInfo> {
let mut conditions = Vec::new();
let mut variable_name: Option<String> = None;
let mut pattern_types = Vec::new();
let mut has_else = false;
for stmt in &block.stmts {
match stmt {
Stmt::Expr(Expr::If(if_expr), _) => {
if let Some(info) = self.analyze_if_chain(if_expr, &mut variable_name) {
conditions.extend(info.0);
pattern_types.extend(info.1);
has_else = info.2;
} else {
if !conditions.is_empty() {
break;
}
}
}
_ => {
if !conditions.is_empty() {
break;
}
}
}
}
if conditions.len() >= 3 {
let pattern_type = pattern_types
.iter()
.max_by_key(|t| pattern_types.iter().filter(|pt| pt == t).count())
.cloned()
.unwrap_or(PatternType::SimpleComparison);
Some(PatternMatchInfo {
variable_name: variable_name.unwrap_or_else(|| String::from("unknown")),
condition_count: conditions.len(),
has_default: has_else,
pattern_type,
})
} else {
None
}
}
pub fn analyze_if_chain(
&self,
if_expr: &ExprIf,
tracked_var: &mut Option<String>,
) -> Option<(Vec<()>, Vec<PatternType>, bool)> {
let mut conditions = Vec::new();
let mut pattern_types = Vec::new();
let mut has_else = false;
let mut current_if = if_expr;
loop {
if let Some(var) = self.extract_tested_variable(¤t_if.cond) {
if tracked_var.is_none() {
*tracked_var = Some(var.clone());
} else if tracked_var.as_ref() != Some(&var) {
return None;
}
if !self.has_immediate_return(¤t_if.then_branch) {
if current_if.then_branch.stmts.len() != 1 {
return None;
}
}
conditions.push(());
pattern_types.push(self.detect_pattern_type(¤t_if.cond));
match ¤t_if.else_branch {
Some((_, else_expr)) => match &**else_expr {
Expr::If(next_if) => {
current_if = next_if;
continue;
}
Expr::Block(_block) => {
has_else = true;
break;
}
_ => {
has_else = true;
break;
}
},
None => break,
}
} else {
return None;
}
}
if !conditions.is_empty() {
Some((conditions, pattern_types, has_else))
} else {
None
}
}
}
impl PatternRecognizer for PatternMatchRecognizer {
fn detect(&self, block: &Block) -> Option<PatternMatchInfo> {
self.detect_pattern_matching(block)
}
fn adjust_complexity(&self, info: &PatternMatchInfo, _base: u32) -> u32 {
let adjusted = (info.condition_count as f32).log2().ceil() as u32;
let default_penalty = if !info.has_default { 1 } else { 0 };
adjusted + default_penalty
}
}
pub struct SimpleDelegationRecognizer;
impl Default for SimpleDelegationRecognizer {
fn default() -> Self {
Self
}
}
impl SimpleDelegationRecognizer {
pub fn new() -> Self {
Self
}
fn is_simple_delegation(&self, block: &Block) -> bool {
if block.stmts.is_empty() {
return false;
}
let mut control_flow_count = 0;
let mut visitor = ControlFlowCounter {
count: &mut control_flow_count,
};
visitor.visit_block(block);
control_flow_count == 0 && block.stmts.len() >= 2
}
}
impl PatternRecognizer for SimpleDelegationRecognizer {
fn detect(&self, block: &Block) -> Option<PatternMatchInfo> {
if self.is_simple_delegation(block) {
Some(PatternMatchInfo {
variable_name: String::from("delegation"),
condition_count: 0,
has_default: true,
pattern_type: PatternType::SimpleComparison,
})
} else {
None
}
}
fn adjust_complexity(&self, _info: &PatternMatchInfo, _base: u32) -> u32 {
1
}
}
struct ControlFlowCounter<'a> {
count: &'a mut u32,
}
impl<'ast> Visit<'ast> for ControlFlowCounter<'_> {
fn visit_expr(&mut self, expr: &'ast Expr) {
match expr {
Expr::If(_) | Expr::While(_) | Expr::ForLoop(_) | Expr::Loop(_) | Expr::Match(_) => {
*self.count += 1;
}
Expr::Binary(binary) => {
use syn::BinOp;
if matches!(binary.op, BinOp::And(_) | BinOp::Or(_)) {
*self.count += 1;
}
}
_ => {}
}
syn::visit::visit_expr(self, expr);
}
}
pub fn calculate_cognitive_adjusted(block: &Block, base_complexity: u32) -> u32 {
use super::match_patterns::MatchExpressionRecognizer;
for stmt in &block.stmts {
if let Stmt::Expr(Expr::Match(_match_expr), _) = stmt {
let recognizer = MatchExpressionRecognizer::new();
let temp_block = syn::Block {
brace_token: block.brace_token,
stmts: vec![stmt.clone()],
};
if let Some(info) = recognizer.detect(&temp_block) {
return recognizer.adjust_complexity(&info, base_complexity);
}
}
}
let recognizers: Vec<Box<dyn PatternRecognizer>> = vec![
Box::new(MatchExpressionRecognizer::new()),
Box::new(PatternMatchRecognizer::new()),
Box::new(SimpleDelegationRecognizer::new()),
];
for recognizer in recognizers {
if let Some(info) = recognizer.detect(block) {
return recognizer.adjust_complexity(&info, base_complexity);
}
}
base_complexity
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_pattern_matching_detection() {
let block: Block = parse_quote! {{
if path.ends_with(".rs") {
return FileType::Rust;
}
if path.ends_with(".py") {
return FileType::Python;
}
if path.ends_with(".js") {
return FileType::JavaScript;
}
if path.ends_with(".ts") {
return FileType::TypeScript;
}
}};
let recognizer = PatternMatchRecognizer::new();
let info = recognizer.detect(&block);
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.condition_count, 4);
assert_eq!(info.pattern_type, PatternType::StringMatching);
assert!(!info.has_default);
}
#[test]
fn test_simple_delegation_detection() {
let block: Block = parse_quote! {{
let result = calculate_something(x, y, z);
transform_result(result)
}};
let recognizer = SimpleDelegationRecognizer::new();
let info = recognizer.detect(&block);
assert!(info.is_some());
}
#[test]
fn test_logarithmic_scaling() {
let info = PatternMatchInfo {
variable_name: "test".to_string(),
condition_count: 8,
has_default: true,
pattern_type: PatternType::StringMatching,
};
let recognizer = PatternMatchRecognizer::new();
let adjusted = recognizer.adjust_complexity(&info, 8);
assert_eq!(adjusted, 3);
}
#[test]
fn test_non_pattern_matching() {
let block: Block = parse_quote! {{
if x > 0 {
if y > 0 {
return true;
}
}
return false;
}};
let recognizer = PatternMatchRecognizer::new();
let info = recognizer.detect(&block);
assert!(info.is_none());
}
}