1use anyhow::Result;
2
3use crate::priority::{UnifiedAnalysis, UnifiedDebtItem};
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum LocationPattern {
8 Exact {
10 file: String,
11 function: String,
12 line: usize,
13 },
14 Function { file: String, function: String },
16 File { file: String },
18 LineRange { file: String, line: usize },
20}
21
22impl LocationPattern {
23 pub fn parse(location: &str) -> Result<Self> {
25 let parts: Vec<&str> = location.split(':').collect();
26 match parts.len() {
27 1 => Ok(Self::File {
28 file: parts[0].to_string(),
29 }),
30 2 => Ok(Self::Function {
31 file: parts[0].to_string(),
32 function: parts[1].to_string(),
33 }),
34 3 => {
35 if parts[1] == "*" {
36 Ok(Self::LineRange {
37 file: parts[0].to_string(),
38 line: parts[2].parse()?,
39 })
40 } else {
41 Ok(Self::Exact {
42 file: parts[0].to_string(),
43 function: parts[1].to_string(),
44 line: parts[2].parse()?,
45 })
46 }
47 }
48 _ => Err(anyhow::anyhow!(
49 "Invalid location format: {}. Expected file[:function[:line]]",
50 location
51 )),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum MatchStrategy {
59 Exact,
61 FunctionLevel,
63 ApproximateName,
65 FileLevel,
67}
68
69impl MatchStrategy {
70 pub fn confidence(&self) -> f64 {
72 match self {
73 Self::Exact => 1.0,
74 Self::FunctionLevel => 0.8,
75 Self::ApproximateName => 0.6,
76 Self::FileLevel => 0.4,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct MatchResult<'a> {
84 pub items: Vec<&'a UnifiedDebtItem>,
85 pub strategy: MatchStrategy,
86 pub confidence: f64,
87}
88
89pub struct LocationMatcher;
91
92impl LocationMatcher {
93 pub fn new() -> Self {
94 Self
95 }
96
97 pub fn find_matches<'a>(
99 &self,
100 analysis: &'a UnifiedAnalysis,
101 location: &str,
102 ) -> Result<MatchResult<'a>> {
103 let pattern = LocationPattern::parse(location)?;
104
105 if let Some(result) = self.try_exact_match(analysis, &pattern) {
107 return Ok(result);
108 }
109
110 if let Some(result) = self.try_function_match(analysis, &pattern) {
111 return Ok(result);
112 }
113
114 if let Some(result) = self.try_approximate_match(analysis, &pattern) {
115 return Ok(result);
116 }
117
118 if let Some(result) = self.try_file_match(analysis, &pattern) {
119 return Ok(result);
120 }
121
122 Err(anyhow::anyhow!(
123 "No items found matching location: {} (tried all strategies: exact, function-level, approximate, file-level)",
124 location
125 ))
126 }
127
128 fn try_exact_match<'a>(
130 &self,
131 analysis: &'a UnifiedAnalysis,
132 pattern: &LocationPattern,
133 ) -> Option<MatchResult<'a>> {
134 let (file, function, line) = match pattern {
135 LocationPattern::Exact {
136 file,
137 function,
138 line,
139 } => (file, function, *line),
140 _ => return None,
141 };
142
143 let item = analysis.items.iter().find(|item| {
144 let item_file = normalize_path(&item.location.file);
145 let target_file = normalize_path_str(file);
146
147 item_file == target_file
148 && item.location.function == *function
149 && item.location.line == line
150 })?;
151
152 Some(MatchResult {
153 items: vec![item],
154 strategy: MatchStrategy::Exact,
155 confidence: MatchStrategy::Exact.confidence(),
156 })
157 }
158
159 fn try_function_match<'a>(
161 &self,
162 analysis: &'a UnifiedAnalysis,
163 pattern: &LocationPattern,
164 ) -> Option<MatchResult<'a>> {
165 let (file, function) = match pattern {
166 LocationPattern::Exact { file, function, .. }
167 | LocationPattern::Function { file, function } => (file, function),
168 _ => return None,
169 };
170
171 let items: Vec<&UnifiedDebtItem> = analysis
172 .items
173 .iter()
174 .filter(|item| {
175 let item_file = normalize_path(&item.location.file);
176 let target_file = normalize_path_str(file);
177
178 item_file == target_file && item.location.function == *function
179 })
180 .collect();
181
182 if items.is_empty() {
183 None
184 } else {
185 Some(MatchResult {
186 items,
187 strategy: MatchStrategy::FunctionLevel,
188 confidence: MatchStrategy::FunctionLevel.confidence(),
189 })
190 }
191 }
192
193 fn try_approximate_match<'a>(
195 &self,
196 analysis: &'a UnifiedAnalysis,
197 pattern: &LocationPattern,
198 ) -> Option<MatchResult<'a>> {
199 let (file, target_name) = match pattern {
200 LocationPattern::Exact { file, function, .. }
201 | LocationPattern::Function { file, function } => (file, function),
202 _ => return None,
203 };
204
205 let target_file = normalize_path_str(file);
206
207 let candidates: Vec<(&UnifiedDebtItem, f64)> = analysis
209 .items
210 .iter()
211 .filter(|item| normalize_path(&item.location.file) == target_file)
212 .filter_map(|item| {
213 let similarity = calculate_similarity(target_name, &item.location.function);
214 if similarity >= 0.5 {
215 Some((item, similarity))
216 } else {
217 None
218 }
219 })
220 .collect();
221
222 if candidates.is_empty() {
223 return None;
224 }
225
226 let max_similarity = candidates
228 .iter()
229 .map(|(_, sim)| *sim)
230 .fold(0.0f64, |a, b| a.max(b));
231
232 let items: Vec<&UnifiedDebtItem> = candidates
233 .iter()
234 .filter(|(_, sim)| *sim == max_similarity)
235 .map(|(item, _)| *item)
236 .collect();
237
238 Some(MatchResult {
239 items,
240 strategy: MatchStrategy::ApproximateName,
241 confidence: max_similarity * MatchStrategy::ApproximateName.confidence(),
242 })
243 }
244
245 fn try_file_match<'a>(
247 &self,
248 analysis: &'a UnifiedAnalysis,
249 pattern: &LocationPattern,
250 ) -> Option<MatchResult<'a>> {
251 let file = match pattern {
252 LocationPattern::File { file } => file,
253 LocationPattern::Exact { file, .. }
254 | LocationPattern::Function { file, .. }
255 | LocationPattern::LineRange { file, .. } => file,
256 };
257
258 let target_file = normalize_path_str(file);
259
260 let items: Vec<&UnifiedDebtItem> = analysis
261 .items
262 .iter()
263 .filter(|item| normalize_path(&item.location.file) == target_file)
264 .collect();
265
266 if items.is_empty() {
267 None
268 } else {
269 let confidence =
271 MatchStrategy::FileLevel.confidence() * (1.0 / (items.len() as f64).sqrt());
272 Some(MatchResult {
273 items,
274 strategy: MatchStrategy::FileLevel,
275 confidence: confidence.max(0.3), })
277 }
278 }
279}
280
281impl Default for LocationMatcher {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287fn normalize_path(path: &std::path::Path) -> String {
289 let path_str = path.to_string_lossy();
290 normalize_path_str(&path_str)
291}
292
293fn normalize_path_str(path: &str) -> String {
295 path.strip_prefix("./").unwrap_or(path).to_string()
296}
297
298fn calculate_similarity(a: &str, b: &str) -> f64 {
300 if a == b {
301 return 1.0;
302 }
303
304 let a_lower = a.to_lowercase();
305 let b_lower = b.to_lowercase();
306
307 if a_lower.starts_with(&b_lower) || b_lower.starts_with(&a_lower) {
309 let shorter = a_lower.len().min(b_lower.len());
310 let longer = a_lower.len().max(b_lower.len());
311 return shorter as f64 / longer as f64;
312 }
313
314 let common_prefix_len = a_lower
316 .chars()
317 .zip(b_lower.chars())
318 .take_while(|(ca, cb)| ca == cb)
319 .count();
320
321 let max_len = a_lower.len().max(b_lower.len());
322 if max_len == 0 {
323 return 0.0;
324 }
325
326 common_prefix_len as f64 / max_len as f64
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 use crate::priority::{
335 DebtType, FunctionRole, ImpactMetrics,
336 unified_scorer::{Location, UnifiedScore},
337 };
338 use im::Vector;
339 use std::path::PathBuf;
340
341 fn create_test_item(file: &str, function: &str, line: usize) -> UnifiedDebtItem {
342 UnifiedDebtItem {
343 location: Location {
344 file: PathBuf::from(file),
345 function: function.to_string(),
346 line,
347 },
348 debt_type: DebtType::TestingGap {
349 coverage: 0.0,
350 cyclomatic: 10,
351 cognitive: 20,
352 },
353 unified_score: UnifiedScore {
354 complexity_factor: 5.0,
355 coverage_factor: 0.0,
356 dependency_factor: 0.0,
357 role_multiplier: 1.0,
358 final_score: 50.0,
359 base_score: None,
360 exponential_factor: None,
361 risk_boost: None,
362 pre_adjustment_score: None,
363 adjustment_applied: None,
364 purity_factor: None,
365 refactorability_factor: None,
366 pattern_factor: None,
367 debt_adjustment: None,
369 pre_normalization_score: None,
370 structural_multiplier: Some(1.0),
371 has_coverage_data: false,
372 contextual_risk_multiplier: None,
373 pre_contextual_score: None,
374 debt_type_multiplier: None,
375 },
376 function_role: FunctionRole::PureLogic,
377 recommendation: crate::priority::ActionableRecommendation {
378 primary_action: "Test".to_string(),
379 rationale: "Test".to_string(),
380 implementation_steps: vec![],
381 related_items: vec![],
382 steps: None,
383 estimated_effort_hours: None,
384 },
385 expected_impact: ImpactMetrics {
386 coverage_improvement: 0.0,
387 lines_reduction: 0,
388 complexity_reduction: 0.0,
389 risk_reduction: 0.0,
390 },
391 transitive_coverage: None,
392 file_context: None,
393 upstream_dependencies: 0,
394 downstream_dependencies: 0,
395 upstream_callers: vec![],
396 downstream_callees: vec![],
397 upstream_production_callers: vec![],
398 upstream_test_callers: vec![],
399 production_blast_radius: 0,
400 nesting_depth: 2,
401 function_length: 50,
402 cyclomatic_complexity: 10,
403 cognitive_complexity: 20,
404 is_pure: None,
405 purity_confidence: None,
406 purity_level: None,
407 god_object_indicators: None,
408 tier: None,
409 function_context: None,
410 context_confidence: None,
411 contextual_recommendation: None,
412 pattern_analysis: None,
413 context_multiplier: None,
414 context_type: None,
415 language_specific: None, detected_pattern: None,
417 contextual_risk: None, file_line_count: None,
419 responsibility_category: None,
420 error_swallowing_count: None,
421 error_swallowing_patterns: None,
422 entropy_analysis: None,
423 context_suggestion: None,
424 }
425 }
426
427 fn create_test_analysis(items: Vec<UnifiedDebtItem>) -> UnifiedAnalysis {
428 UnifiedAnalysis {
429 items: Vector::from(items),
430 file_items: Vector::new(),
431 total_impact: ImpactMetrics {
432 coverage_improvement: 0.0,
433 lines_reduction: 0,
434 complexity_reduction: 0.0,
435 risk_reduction: 0.0,
436 },
437 total_debt_score: 50.0,
438 debt_density: 0.0,
439 total_lines_of_code: 1000,
440 call_graph: crate::priority::CallGraph::new(),
441 data_flow_graph: crate::data_flow::DataFlowGraph::new(),
442 overall_coverage: None,
443 has_coverage_data: false,
444 timings: None,
445 stats: crate::priority::FilterStatistics::new(),
446 analyzed_files: std::collections::HashMap::new(),
447 }
448 }
449
450 #[test]
451 fn test_parse_exact_location() {
452 let pattern = LocationPattern::parse("src/main.rs:func:42").unwrap();
453 assert_eq!(
454 pattern,
455 LocationPattern::Exact {
456 file: "src/main.rs".to_string(),
457 function: "func".to_string(),
458 line: 42
459 }
460 );
461 }
462
463 #[test]
464 fn test_parse_function_location() {
465 let pattern = LocationPattern::parse("src/main.rs:func").unwrap();
466 assert_eq!(
467 pattern,
468 LocationPattern::Function {
469 file: "src/main.rs".to_string(),
470 function: "func".to_string(),
471 }
472 );
473 }
474
475 #[test]
476 fn test_parse_file_location() {
477 let pattern = LocationPattern::parse("src/main.rs").unwrap();
478 assert_eq!(
479 pattern,
480 LocationPattern::File {
481 file: "src/main.rs".to_string()
482 }
483 );
484 }
485
486 #[test]
487 fn test_parse_line_range() {
488 let pattern = LocationPattern::parse("src/main.rs:*:42").unwrap();
489 assert_eq!(
490 pattern,
491 LocationPattern::LineRange {
492 file: "src/main.rs".to_string(),
493 line: 42
494 }
495 );
496 }
497
498 #[test]
499 fn test_exact_match() {
500 let analysis = create_test_analysis(vec![create_test_item("src/main.rs", "func", 42)]);
501 let matcher = LocationMatcher::new();
502
503 let result = matcher
504 .find_matches(&analysis, "src/main.rs:func:42")
505 .unwrap();
506 assert_eq!(result.strategy, MatchStrategy::Exact);
507 assert_eq!(result.items.len(), 1);
508 assert_eq!(result.confidence, 1.0);
509 }
510
511 #[test]
512 fn test_exact_match_with_path_normalization() {
513 let analysis = create_test_analysis(vec![create_test_item("./src/main.rs", "func", 42)]);
514 let matcher = LocationMatcher::new();
515
516 let result = matcher
517 .find_matches(&analysis, "src/main.rs:func:42")
518 .unwrap();
519 assert_eq!(result.strategy, MatchStrategy::Exact);
520 assert_eq!(result.items.len(), 1);
521 }
522
523 #[test]
524 fn test_function_level_match() {
525 let analysis = create_test_analysis(vec![
526 create_test_item("src/main.rs", "func", 42),
527 create_test_item("src/main.rs", "func", 50),
528 ]);
529 let matcher = LocationMatcher::new();
530
531 let result = matcher
533 .find_matches(&analysis, "src/main.rs:func:99")
534 .unwrap();
535 assert_eq!(result.strategy, MatchStrategy::FunctionLevel);
536 assert_eq!(result.items.len(), 2);
537 assert_eq!(result.confidence, 0.8);
538 }
539
540 #[test]
541 fn test_approximate_match() {
542 let analysis = create_test_analysis(vec![
543 create_test_item("src/main.rs", "EnhancedMarkdownWriter", 10),
544 create_test_item("src/main.rs", "other_func", 20),
545 ]);
546 let matcher = LocationMatcher::new();
547
548 let result = matcher
550 .find_matches(&analysis, "src/main.rs:EnhancedMarkdown:1")
551 .unwrap();
552 assert_eq!(result.strategy, MatchStrategy::ApproximateName);
553 assert_eq!(result.items.len(), 1);
554 assert!(result.confidence > 0.3);
557 }
558
559 #[test]
560 fn test_file_level_match() {
561 let analysis = create_test_analysis(vec![
562 create_test_item("src/main.rs", "func1", 10),
563 create_test_item("src/main.rs", "func2", 20),
564 create_test_item("src/other.rs", "func3", 30),
565 ]);
566 let matcher = LocationMatcher::new();
567
568 let result = matcher
570 .find_matches(&analysis, "src/main.rs:nonexistent:1")
571 .unwrap();
572 assert_eq!(result.strategy, MatchStrategy::FileLevel);
573 assert_eq!(result.items.len(), 2);
574 assert!(result.confidence >= 0.3);
575 }
576
577 #[test]
578 fn test_no_match() {
579 let analysis = create_test_analysis(vec![create_test_item("src/main.rs", "func", 42)]);
580 let matcher = LocationMatcher::new();
581
582 let result = matcher.find_matches(&analysis, "src/other.rs:func:42");
583 assert!(result.is_err());
584 }
585
586 #[test]
587 fn test_similarity_exact() {
588 assert_eq!(calculate_similarity("func", "func"), 1.0);
589 }
590
591 #[test]
592 fn test_similarity_prefix() {
593 let sim = calculate_similarity("EnhancedMarkdownWriter", "EnhancedMarkdown");
594 assert!(sim > 0.5);
595 assert!(sim < 1.0);
596 }
597
598 #[test]
599 fn test_similarity_different() {
600 let sim = calculate_similarity("func1", "other");
601 assert!(sim < 0.5);
602 }
603}