use crate::analyzers::Analyzer;
use crate::context::rules::DebtPattern;
use crate::context::{
detect_file_type, ContextDetector, ContextRuleEngine, FileType, FunctionContext, RuleAction,
};
use crate::core::{
ast::{Ast, RustAst},
DebtItem, DebtType, FileMetrics, Language, Priority,
};
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use syn::visit::Visit;
pub struct ContextAwareAnalyzer {
inner: Box<dyn Analyzer>,
rule_engine: RwLock<ContextRuleEngine>,
enabled: bool,
}
impl ContextAwareAnalyzer {
pub fn new(inner: Box<dyn Analyzer>) -> Self {
Self {
inner,
rule_engine: RwLock::new(ContextRuleEngine::new()),
enabled: true,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
fn process_rule_action(
&self,
action: RuleAction,
item: &mut DebtItem,
pattern: &DebtPattern,
context: &FunctionContext,
) -> bool {
match action {
RuleAction::Allow | RuleAction::Skip => false,
RuleAction::Warn => {
item.priority = adjust_priority(item.priority, -2);
self.add_context_note(item, pattern, context);
true
}
RuleAction::ReduceSeverity(n) => {
item.priority = adjust_priority(item.priority, -n);
self.add_context_note(item, pattern, context);
true
}
RuleAction::Deny => true,
}
}
fn add_context_note(
&self,
item: &mut DebtItem,
pattern: &DebtPattern,
context: &FunctionContext,
) {
if let Some(reason) = self
.rule_engine
.write()
.unwrap()
.get_reason(pattern, context)
{
item.message = format!("{} (Context: {})", item.message, reason);
}
}
fn process_rust_items(
&self,
metrics: &mut FileMetrics,
rust_ast: &RustAst,
file_type: FileType,
) {
let mut detector = ContextDetector::new(file_type);
detector.visit_file(&rust_ast.file);
metrics.debt_items.retain_mut(|item| {
let context = detector
.get_context_for_line(item.line)
.cloned()
.unwrap_or_else(|| FunctionContext::new().with_file_type(file_type));
let pattern = debt_type_to_pattern(&item.debt_type, &item.message);
let action = self
.rule_engine
.write()
.unwrap()
.evaluate(&pattern, &context);
self.process_rule_action(action, item, &pattern, &context)
});
}
fn process_non_rust_items(&self, metrics: &mut FileMetrics, file_type: FileType) {
let context = FunctionContext::new().with_file_type(file_type);
metrics.debt_items.retain_mut(|item| {
let pattern = debt_type_to_pattern(&item.debt_type, &item.message);
let action = self
.rule_engine
.write()
.unwrap()
.evaluate(&pattern, &context);
self.process_rule_action(action, item, &pattern, &context)
});
}
fn filter_debt_items(&self, mut metrics: FileMetrics, ast: &Ast, path: &Path) -> FileMetrics {
if !self.enabled {
return metrics;
}
let file_type = detect_file_type(path);
if let Ast::Rust(rust_ast) = ast {
self.process_rust_items(&mut metrics, rust_ast, file_type);
} else {
self.process_non_rust_items(&mut metrics, file_type);
}
metrics
}
}
impl Analyzer for ContextAwareAnalyzer {
fn parse(&self, content: &str, path: PathBuf) -> Result<Ast> {
self.inner.parse(content, path)
}
fn analyze(&self, ast: &Ast) -> FileMetrics {
let metrics = self.inner.analyze(ast);
if !metrics.path.as_os_str().is_empty() {
let path = metrics.path.clone();
self.filter_debt_items(metrics, ast, &path)
} else {
metrics
}
}
fn language(&self) -> Language {
self.inner.language()
}
}
fn debt_type_to_pattern(debt_type: &DebtType, _message: &str) -> DebtPattern {
DebtPattern::DebtType(debt_type.clone())
}
fn adjust_priority(priority: Priority, adjustment: i32) -> Priority {
if adjustment == -999 {
return Priority::Low;
}
let current = match priority {
Priority::Critical => 4,
Priority::High => 3,
Priority::Medium => 2,
Priority::Low => 1,
};
let new = (current + adjustment).clamp(1, 4);
match new {
4 => Priority::Critical,
3 => Priority::High,
2 => Priority::Medium,
_ => Priority::Low,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{ComplexityMetrics, DebtItem};
use std::path::PathBuf;
#[test]
fn test_adjust_priority() {
assert_eq!(adjust_priority(Priority::High, -1), Priority::Medium);
assert_eq!(adjust_priority(Priority::Medium, -2), Priority::Low);
assert_eq!(adjust_priority(Priority::Low, -1), Priority::Low);
assert_eq!(adjust_priority(Priority::Critical, -999), Priority::Low);
}
#[test]
fn test_adjust_priority_edge_cases() {
assert_eq!(adjust_priority(Priority::High, 1), Priority::Critical);
assert_eq!(adjust_priority(Priority::Critical, 1), Priority::Critical);
assert_eq!(adjust_priority(Priority::Critical, -10), Priority::Low);
assert_eq!(adjust_priority(Priority::Medium, 0), Priority::Medium);
}
#[test]
fn test_debt_type_to_pattern_other_types() {
let pattern =
debt_type_to_pattern(&DebtType::Todo { reason: None }, "Implement this feature");
assert_eq!(
pattern,
DebtPattern::DebtType(DebtType::Todo { reason: None })
);
let pattern = debt_type_to_pattern(&DebtType::Fixme { reason: None }, "Fix this bug");
assert_eq!(
pattern,
DebtPattern::DebtType(DebtType::Fixme { reason: None })
);
let pattern = debt_type_to_pattern(
&DebtType::CodeSmell { smell_type: None },
"Code smell detected",
);
assert_eq!(
pattern,
DebtPattern::DebtType(DebtType::CodeSmell { smell_type: None })
);
let pattern = debt_type_to_pattern(
&DebtType::Complexity {
cyclomatic: 10,
cognitive: 8,
},
"High cyclomatic complexity",
);
assert_eq!(
pattern,
DebtPattern::DebtType(DebtType::Complexity {
cyclomatic: 10,
cognitive: 8
})
);
let pattern = debt_type_to_pattern(
&DebtType::Duplication {
instances: 2,
total_lines: 20,
},
"Duplicate code detected",
);
assert_eq!(
pattern,
DebtPattern::DebtType(DebtType::Duplication {
instances: 2,
total_lines: 20
})
);
}
#[test]
fn test_process_rule_action_allow() {
let analyzer = create_test_analyzer();
let mut item = create_test_debt_item();
let pattern = DebtPattern::DebtType(DebtType::Todo { reason: None });
let context = FunctionContext::new();
let keep = analyzer.process_rule_action(RuleAction::Allow, &mut item, &pattern, &context);
assert!(!keep, "Allow action should filter out the item");
}
#[test]
fn test_process_rule_action_skip() {
let analyzer = create_test_analyzer();
let mut item = create_test_debt_item();
let pattern = DebtPattern::DebtType(DebtType::Todo { reason: None });
let context = FunctionContext::new();
let keep = analyzer.process_rule_action(RuleAction::Skip, &mut item, &pattern, &context);
assert!(!keep, "Skip action should filter out the item");
}
#[test]
fn test_process_rule_action_warn() {
let analyzer = create_test_analyzer();
let mut item = create_test_debt_item();
let original_priority = item.priority;
let pattern = DebtPattern::DebtType(DebtType::Todo { reason: None });
let context = FunctionContext::new();
let keep = analyzer.process_rule_action(RuleAction::Warn, &mut item, &pattern, &context);
assert!(keep, "Warn action should keep the item");
assert_eq!(
item.priority,
adjust_priority(original_priority, -2),
"Priority should be reduced by 2"
);
}
#[test]
fn test_process_rule_action_reduce_severity() {
let analyzer = create_test_analyzer();
let mut item = create_test_debt_item();
let original_priority = item.priority;
let pattern = DebtPattern::DebtType(DebtType::Todo { reason: None });
let context = FunctionContext::new();
let keep = analyzer.process_rule_action(
RuleAction::ReduceSeverity(3),
&mut item,
&pattern,
&context,
);
assert!(keep, "ReduceSeverity action should keep the item");
assert_eq!(
item.priority,
adjust_priority(original_priority, -3),
"Priority should be reduced by 3"
);
}
#[test]
fn test_process_rule_action_deny() {
let analyzer = create_test_analyzer();
let mut item = create_test_debt_item();
let original_priority = item.priority;
let pattern = DebtPattern::DebtType(DebtType::Todo { reason: None });
let context = FunctionContext::new();
let keep = analyzer.process_rule_action(RuleAction::Deny, &mut item, &pattern, &context);
assert!(keep, "Deny action should keep the item");
assert_eq!(
item.priority, original_priority,
"Priority should be unchanged"
);
}
#[test]
fn test_add_context_note() {
let analyzer = create_test_analyzer();
let mut item = create_test_debt_item();
let pattern = DebtPattern::DebtType(DebtType::Todo { reason: None });
let context = FunctionContext::new();
analyzer.add_context_note(&mut item, &pattern, &context);
}
#[test]
fn test_filter_debt_items_disabled() {
let mut analyzer = create_test_analyzer();
analyzer.set_enabled(false);
let metrics = create_test_metrics();
let ast = Ast::Unknown;
let path = Path::new("test.rs");
let result = analyzer.filter_debt_items(metrics.clone(), &ast, path);
assert_eq!(
result.debt_items.len(),
metrics.debt_items.len(),
"Should not filter when disabled"
);
}
fn create_test_analyzer() -> ContextAwareAnalyzer {
struct MockAnalyzer;
impl Analyzer for MockAnalyzer {
fn parse(&self, _content: &str, _path: PathBuf) -> Result<Ast> {
Ok(Ast::Unknown)
}
fn analyze(&self, _ast: &Ast) -> FileMetrics {
create_test_metrics()
}
fn language(&self) -> Language {
Language::Rust
}
}
ContextAwareAnalyzer::new(Box::new(MockAnalyzer))
}
fn create_test_debt_item() -> DebtItem {
DebtItem {
id: "test-item".to_string(),
debt_type: DebtType::Todo { reason: None },
message: "Test debt item".to_string(),
line: 42,
column: Some(0),
priority: Priority::Medium,
file: PathBuf::from("test.rs"),
context: None,
}
}
fn create_test_metrics() -> FileMetrics {
FileMetrics {
path: PathBuf::from("test.rs"),
language: Language::Rust,
complexity: ComplexityMetrics::default(),
debt_items: vec![create_test_debt_item()],
dependencies: vec![],
duplications: vec![],
total_lines: 0,
module_scope: None,
classes: None,
}
}
}