rust_diff_analyzer/analysis/
mapper.rs1use std::{collections::HashMap, path::Path};
5
6use masterror::AppError;
7
8use super::extractor::extract_semantic_units_from_str;
9use crate::{
10 classifier::classify_unit,
11 config::Config,
12 error::FileReadError,
13 git::FileDiff,
14 types::{Change, SemanticUnit},
15};
16
17pub fn map_changes<F>(
45 diffs: &[FileDiff],
46 config: &Config,
47 file_reader: F,
48) -> Result<Vec<Change>, AppError>
49where
50 F: Fn(&Path) -> Result<String, std::io::Error>,
51{
52 let mut changes = Vec::new();
53
54 for diff in diffs {
55 if !diff.is_rust_file() {
56 continue;
57 }
58
59 if config.should_ignore(&diff.path) {
60 continue;
61 }
62
63 let content = file_reader(&diff.path)
64 .map_err(|e| AppError::from(FileReadError::new(diff.path.clone(), e)))?;
65
66 let units = extract_semantic_units_from_str(&content, &diff.path)?;
67
68 let added_lines = diff.all_added_lines();
69 let removed_lines = diff.all_removed_lines();
70
71 let mut unit_changes: HashMap<String, (usize, usize)> = HashMap::new();
72
73 for line in &added_lines {
74 if let Some(unit) = find_containing_unit(&units, *line) {
75 let entry = unit_changes.entry(unit.name.clone()).or_insert((0, 0));
76 entry.0 += 1;
77 }
78 }
79
80 for line in &removed_lines {
81 if let Some(unit) = find_containing_unit(&units, *line) {
82 let entry = unit_changes.entry(unit.name.clone()).or_insert((0, 0));
83 entry.1 += 1;
84 }
85 }
86
87 for unit in &units {
88 if let Some((added, removed)) = unit_changes.get(&unit.name) {
89 let classification = classify_unit(unit, &diff.path, config);
90
91 changes.push(Change::new(
92 diff.path.clone(),
93 unit.clone(),
94 classification,
95 *added,
96 *removed,
97 ));
98 }
99 }
100 }
101
102 Ok(changes)
103}
104
105fn find_containing_unit(units: &[SemanticUnit], line: usize) -> Option<&SemanticUnit> {
106 let mut best_match: Option<&SemanticUnit> = None;
107
108 for unit in units {
109 if unit.span.contains(line) {
110 match best_match {
111 None => best_match = Some(unit),
112 Some(current) => {
113 if unit.span.len() < current.span.len() {
114 best_match = Some(unit);
115 }
116 }
117 }
118 }
119 }
120
121 best_match
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::types::{LineSpan, SemanticUnitKind, Visibility};
128
129 #[test]
130 fn test_find_containing_unit() {
131 let units = vec![
132 SemanticUnit::new(
133 SemanticUnitKind::Module,
134 "module".to_string(),
135 Visibility::Private,
136 LineSpan::new(1, 100),
137 vec![],
138 ),
139 SemanticUnit::new(
140 SemanticUnitKind::Function,
141 "func".to_string(),
142 Visibility::Public,
143 LineSpan::new(10, 20),
144 vec![],
145 ),
146 ];
147
148 let result = find_containing_unit(&units, 15);
149 assert!(result.is_some());
150 assert_eq!(result.expect("should find unit").name, "func");
151
152 let result = find_containing_unit(&units, 50);
153 assert!(result.is_some());
154 assert_eq!(result.expect("should find unit").name, "module");
155
156 let result = find_containing_unit(&units, 200);
157 assert!(result.is_none());
158 }
159}