use anyhow::Result;
use crate::priority::{UnifiedAnalysis, UnifiedDebtItem};
#[derive(Debug, Clone, PartialEq)]
pub enum LocationPattern {
Exact {
file: String,
function: String,
line: usize,
},
Function { file: String, function: String },
File { file: String },
LineRange { file: String, line: usize },
}
impl LocationPattern {
pub fn parse(location: &str) -> Result<Self> {
let parts: Vec<&str> = location.split(':').collect();
match parts.len() {
1 => Ok(Self::File {
file: parts[0].to_string(),
}),
2 => Ok(Self::Function {
file: parts[0].to_string(),
function: parts[1].to_string(),
}),
3 => {
if parts[1] == "*" {
Ok(Self::LineRange {
file: parts[0].to_string(),
line: parts[2].parse()?,
})
} else {
Ok(Self::Exact {
file: parts[0].to_string(),
function: parts[1].to_string(),
line: parts[2].parse()?,
})
}
}
_ => Err(anyhow::anyhow!(
"Invalid location format: {}. Expected file[:function[:line]]",
location
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchStrategy {
Exact,
FunctionLevel,
ApproximateName,
FileLevel,
}
impl MatchStrategy {
pub fn confidence(&self) -> f64 {
match self {
Self::Exact => 1.0,
Self::FunctionLevel => 0.8,
Self::ApproximateName => 0.6,
Self::FileLevel => 0.4,
}
}
}
#[derive(Debug, Clone)]
pub struct MatchResult<'a> {
pub items: Vec<&'a UnifiedDebtItem>,
pub strategy: MatchStrategy,
pub confidence: f64,
}
pub struct LocationMatcher;
impl LocationMatcher {
pub fn new() -> Self {
Self
}
pub fn find_matches<'a>(
&self,
analysis: &'a UnifiedAnalysis,
location: &str,
) -> Result<MatchResult<'a>> {
let pattern = LocationPattern::parse(location)?;
if let Some(result) = self.try_exact_match(analysis, &pattern) {
return Ok(result);
}
if let Some(result) = self.try_function_match(analysis, &pattern) {
return Ok(result);
}
if let Some(result) = self.try_approximate_match(analysis, &pattern) {
return Ok(result);
}
if let Some(result) = self.try_file_match(analysis, &pattern) {
return Ok(result);
}
Err(anyhow::anyhow!(
"No items found matching location: {} (tried all strategies: exact, function-level, approximate, file-level)",
location
))
}
fn try_exact_match<'a>(
&self,
analysis: &'a UnifiedAnalysis,
pattern: &LocationPattern,
) -> Option<MatchResult<'a>> {
let (file, function, line) = match pattern {
LocationPattern::Exact {
file,
function,
line,
} => (file, function, *line),
_ => return None,
};
let item = analysis.items.iter().find(|item| {
let item_file = normalize_path(&item.location.file);
let target_file = normalize_path_str(file);
item_file == target_file
&& item.location.function == *function
&& item.location.line == line
})?;
Some(MatchResult {
items: vec![item],
strategy: MatchStrategy::Exact,
confidence: MatchStrategy::Exact.confidence(),
})
}
fn try_function_match<'a>(
&self,
analysis: &'a UnifiedAnalysis,
pattern: &LocationPattern,
) -> Option<MatchResult<'a>> {
let (file, function) = match pattern {
LocationPattern::Exact { file, function, .. }
| LocationPattern::Function { file, function } => (file, function),
_ => return None,
};
let items: Vec<&UnifiedDebtItem> = analysis
.items
.iter()
.filter(|item| {
let item_file = normalize_path(&item.location.file);
let target_file = normalize_path_str(file);
item_file == target_file && item.location.function == *function
})
.collect();
if items.is_empty() {
None
} else {
Some(MatchResult {
items,
strategy: MatchStrategy::FunctionLevel,
confidence: MatchStrategy::FunctionLevel.confidence(),
})
}
}
fn try_approximate_match<'a>(
&self,
analysis: &'a UnifiedAnalysis,
pattern: &LocationPattern,
) -> Option<MatchResult<'a>> {
let (file, target_name) = match pattern {
LocationPattern::Exact { file, function, .. }
| LocationPattern::Function { file, function } => (file, function),
_ => return None,
};
let target_file = normalize_path_str(file);
let candidates: Vec<(&UnifiedDebtItem, f64)> = analysis
.items
.iter()
.filter(|item| normalize_path(&item.location.file) == target_file)
.filter_map(|item| {
let similarity = calculate_similarity(target_name, &item.location.function);
if similarity >= 0.5 {
Some((item, similarity))
} else {
None
}
})
.collect();
if candidates.is_empty() {
return None;
}
let max_similarity = candidates
.iter()
.map(|(_, sim)| *sim)
.fold(0.0f64, |a, b| a.max(b));
let items: Vec<&UnifiedDebtItem> = candidates
.iter()
.filter(|(_, sim)| *sim == max_similarity)
.map(|(item, _)| *item)
.collect();
Some(MatchResult {
items,
strategy: MatchStrategy::ApproximateName,
confidence: max_similarity * MatchStrategy::ApproximateName.confidence(),
})
}
fn try_file_match<'a>(
&self,
analysis: &'a UnifiedAnalysis,
pattern: &LocationPattern,
) -> Option<MatchResult<'a>> {
let file = match pattern {
LocationPattern::File { file } => file,
LocationPattern::Exact { file, .. }
| LocationPattern::Function { file, .. }
| LocationPattern::LineRange { file, .. } => file,
};
let target_file = normalize_path_str(file);
let items: Vec<&UnifiedDebtItem> = analysis
.items
.iter()
.filter(|item| normalize_path(&item.location.file) == target_file)
.collect();
if items.is_empty() {
None
} else {
let confidence =
MatchStrategy::FileLevel.confidence() * (1.0 / (items.len() as f64).sqrt());
Some(MatchResult {
items,
strategy: MatchStrategy::FileLevel,
confidence: confidence.max(0.3), })
}
}
}
impl Default for LocationMatcher {
fn default() -> Self {
Self::new()
}
}
fn normalize_path(path: &std::path::Path) -> String {
let path_str = path.to_string_lossy();
normalize_path_str(&path_str)
}
fn normalize_path_str(path: &str) -> String {
path.strip_prefix("./").unwrap_or(path).to_string()
}
fn calculate_similarity(a: &str, b: &str) -> f64 {
if a == b {
return 1.0;
}
let a_lower = a.to_lowercase();
let b_lower = b.to_lowercase();
if a_lower.starts_with(&b_lower) || b_lower.starts_with(&a_lower) {
let shorter = a_lower.len().min(b_lower.len());
let longer = a_lower.len().max(b_lower.len());
return shorter as f64 / longer as f64;
}
let common_prefix_len = a_lower
.chars()
.zip(b_lower.chars())
.take_while(|(ca, cb)| ca == cb)
.count();
let max_len = a_lower.len().max(b_lower.len());
if max_len == 0 {
return 0.0;
}
common_prefix_len as f64 / max_len as f64
}
#[cfg(test)]
mod tests {
use super::*;
use crate::priority::{
unified_scorer::{Location, UnifiedScore},
DebtType, FunctionRole, ImpactMetrics,
};
use im::Vector;
use std::path::PathBuf;
fn create_test_item(file: &str, function: &str, line: usize) -> UnifiedDebtItem {
UnifiedDebtItem {
location: Location {
file: PathBuf::from(file),
function: function.to_string(),
line,
},
debt_type: DebtType::TestingGap {
coverage: 0.0,
cyclomatic: 10,
cognitive: 20,
},
unified_score: UnifiedScore {
complexity_factor: 5.0,
coverage_factor: 0.0,
dependency_factor: 0.0,
role_multiplier: 1.0,
final_score: 50.0,
base_score: None,
exponential_factor: None,
risk_boost: None,
pre_adjustment_score: None,
adjustment_applied: None,
purity_factor: None,
refactorability_factor: None,
pattern_factor: None,
debt_adjustment: None,
pre_normalization_score: None,
structural_multiplier: Some(1.0),
has_coverage_data: false,
contextual_risk_multiplier: None,
pre_contextual_score: None,
debt_type_multiplier: None,
},
function_role: FunctionRole::PureLogic,
recommendation: crate::priority::ActionableRecommendation {
primary_action: "Test".to_string(),
rationale: "Test".to_string(),
implementation_steps: vec![],
related_items: vec![],
steps: None,
estimated_effort_hours: None,
},
expected_impact: ImpactMetrics {
coverage_improvement: 0.0,
lines_reduction: 0,
complexity_reduction: 0.0,
risk_reduction: 0.0,
},
transitive_coverage: None,
file_context: None,
upstream_dependencies: 0,
downstream_dependencies: 0,
upstream_callers: vec![],
downstream_callees: vec![],
upstream_production_callers: vec![],
upstream_test_callers: vec![],
production_blast_radius: 0,
nesting_depth: 2,
function_length: 50,
cyclomatic_complexity: 10,
cognitive_complexity: 20,
is_pure: None,
purity_confidence: None,
purity_level: None,
god_object_indicators: None,
tier: None,
function_context: None,
context_confidence: None,
contextual_recommendation: None,
pattern_analysis: None,
context_multiplier: None,
context_type: None,
language_specific: None, detected_pattern: None,
contextual_risk: None, file_line_count: None,
responsibility_category: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
context_suggestion: None,
}
}
fn create_test_analysis(items: Vec<UnifiedDebtItem>) -> UnifiedAnalysis {
UnifiedAnalysis {
items: Vector::from(items),
file_items: Vector::new(),
total_impact: ImpactMetrics {
coverage_improvement: 0.0,
lines_reduction: 0,
complexity_reduction: 0.0,
risk_reduction: 0.0,
},
total_debt_score: 50.0,
debt_density: 0.0,
total_lines_of_code: 1000,
call_graph: crate::priority::CallGraph::new(),
data_flow_graph: crate::data_flow::DataFlowGraph::new(),
overall_coverage: None,
has_coverage_data: false,
timings: None,
stats: crate::priority::FilterStatistics::new(),
analyzed_files: std::collections::HashMap::new(),
}
}
#[test]
fn test_parse_exact_location() {
let pattern = LocationPattern::parse("src/main.rs:func:42").unwrap();
assert_eq!(
pattern,
LocationPattern::Exact {
file: "src/main.rs".to_string(),
function: "func".to_string(),
line: 42
}
);
}
#[test]
fn test_parse_function_location() {
let pattern = LocationPattern::parse("src/main.rs:func").unwrap();
assert_eq!(
pattern,
LocationPattern::Function {
file: "src/main.rs".to_string(),
function: "func".to_string(),
}
);
}
#[test]
fn test_parse_file_location() {
let pattern = LocationPattern::parse("src/main.rs").unwrap();
assert_eq!(
pattern,
LocationPattern::File {
file: "src/main.rs".to_string()
}
);
}
#[test]
fn test_parse_line_range() {
let pattern = LocationPattern::parse("src/main.rs:*:42").unwrap();
assert_eq!(
pattern,
LocationPattern::LineRange {
file: "src/main.rs".to_string(),
line: 42
}
);
}
#[test]
fn test_exact_match() {
let analysis = create_test_analysis(vec![create_test_item("src/main.rs", "func", 42)]);
let matcher = LocationMatcher::new();
let result = matcher
.find_matches(&analysis, "src/main.rs:func:42")
.unwrap();
assert_eq!(result.strategy, MatchStrategy::Exact);
assert_eq!(result.items.len(), 1);
assert_eq!(result.confidence, 1.0);
}
#[test]
fn test_exact_match_with_path_normalization() {
let analysis = create_test_analysis(vec![create_test_item("./src/main.rs", "func", 42)]);
let matcher = LocationMatcher::new();
let result = matcher
.find_matches(&analysis, "src/main.rs:func:42")
.unwrap();
assert_eq!(result.strategy, MatchStrategy::Exact);
assert_eq!(result.items.len(), 1);
}
#[test]
fn test_function_level_match() {
let analysis = create_test_analysis(vec![
create_test_item("src/main.rs", "func", 42),
create_test_item("src/main.rs", "func", 50),
]);
let matcher = LocationMatcher::new();
let result = matcher
.find_matches(&analysis, "src/main.rs:func:99")
.unwrap();
assert_eq!(result.strategy, MatchStrategy::FunctionLevel);
assert_eq!(result.items.len(), 2);
assert_eq!(result.confidence, 0.8);
}
#[test]
fn test_approximate_match() {
let analysis = create_test_analysis(vec![
create_test_item("src/main.rs", "EnhancedMarkdownWriter", 10),
create_test_item("src/main.rs", "other_func", 20),
]);
let matcher = LocationMatcher::new();
let result = matcher
.find_matches(&analysis, "src/main.rs:EnhancedMarkdown:1")
.unwrap();
assert_eq!(result.strategy, MatchStrategy::ApproximateName);
assert_eq!(result.items.len(), 1);
assert!(result.confidence > 0.3);
}
#[test]
fn test_file_level_match() {
let analysis = create_test_analysis(vec![
create_test_item("src/main.rs", "func1", 10),
create_test_item("src/main.rs", "func2", 20),
create_test_item("src/other.rs", "func3", 30),
]);
let matcher = LocationMatcher::new();
let result = matcher
.find_matches(&analysis, "src/main.rs:nonexistent:1")
.unwrap();
assert_eq!(result.strategy, MatchStrategy::FileLevel);
assert_eq!(result.items.len(), 2);
assert!(result.confidence >= 0.3);
}
#[test]
fn test_no_match() {
let analysis = create_test_analysis(vec![create_test_item("src/main.rs", "func", 42)]);
let matcher = LocationMatcher::new();
let result = matcher.find_matches(&analysis, "src/other.rs:func:42");
assert!(result.is_err());
}
#[test]
fn test_similarity_exact() {
assert_eq!(calculate_similarity("func", "func"), 1.0);
}
#[test]
fn test_similarity_prefix() {
let sim = calculate_similarity("EnhancedMarkdownWriter", "EnhancedMarkdown");
assert!(sim > 0.5);
assert!(sim < 1.0);
}
#[test]
fn test_similarity_different() {
let sim = calculate_similarity("func1", "other");
assert!(sim < 0.5);
}
}