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::{AnalysisScope, Change, ExclusionReason, SemanticUnit},
15};
16
17pub struct MapResult {
19 pub changes: Vec<Change>,
21 pub scope: AnalysisScope,
23}
24
25pub fn map_changes<F>(
53 diffs: &[FileDiff],
54 config: &Config,
55 file_reader: F,
56) -> Result<MapResult, AppError>
57where
58 F: Fn(&Path) -> Result<String, std::io::Error>,
59{
60 let mut changes = Vec::new();
61 let mut scope = AnalysisScope::new();
62
63 scope.set_patterns(config.classification.ignore_paths.clone());
64
65 for diff in diffs {
66 if !diff.is_rust_file() {
67 scope.add_skipped(diff.path.clone(), ExclusionReason::NonRust);
68 continue;
69 }
70
71 if config.should_ignore(&diff.path) {
72 let pattern = config
73 .classification
74 .ignore_paths
75 .iter()
76 .find(|p| diff.path.to_string_lossy().contains(p.as_str()))
77 .cloned()
78 .unwrap_or_default();
79 scope.add_skipped(diff.path.clone(), ExclusionReason::IgnorePattern(pattern));
80 continue;
81 }
82
83 scope.add_analyzed(diff.path.clone());
84
85 let content = file_reader(&diff.path)
86 .map_err(|e| AppError::from(FileReadError::new(diff.path.clone(), e)))?;
87
88 let units = extract_semantic_units_from_str(&content, &diff.path)?;
89
90 let added_lines = diff.all_added_lines();
91 let removed_lines = diff.all_removed_lines();
92
93 let mut unit_changes: HashMap<String, (usize, usize)> = HashMap::new();
94
95 for line in &added_lines {
96 if let Some(unit) = find_containing_unit(&units, *line) {
97 let entry = unit_changes.entry(unit.qualified_name()).or_insert((0, 0));
98 entry.0 += 1;
99 }
100 }
101
102 for line in &removed_lines {
103 if let Some(unit) = find_containing_unit(&units, *line) {
104 let entry = unit_changes.entry(unit.qualified_name()).or_insert((0, 0));
105 entry.1 += 1;
106 }
107 }
108
109 for unit in &units {
110 if let Some((added, removed)) = unit_changes.get(&unit.qualified_name()) {
111 let classification = classify_unit(unit, &diff.path, config);
112
113 changes.push(Change::new(
114 diff.path.clone(),
115 unit.clone(),
116 classification,
117 *added,
118 *removed,
119 ));
120 }
121 }
122 }
123
124 Ok(MapResult { changes, scope })
125}
126
127fn find_containing_unit(units: &[SemanticUnit], line: usize) -> Option<&SemanticUnit> {
128 let mut best_match: Option<&SemanticUnit> = None;
129
130 for unit in units {
131 if unit.span.contains(line) {
132 match best_match {
133 None => best_match = Some(unit),
134 Some(current) => {
135 if unit.span.len() < current.span.len() {
136 best_match = Some(unit);
137 }
138 }
139 }
140 }
141 }
142
143 best_match
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::types::{LineSpan, SemanticUnitKind, Visibility};
150
151 #[test]
152 fn test_find_containing_unit() {
153 let units = vec![
154 SemanticUnit::new(
155 SemanticUnitKind::Module,
156 "module".to_string(),
157 Visibility::Private,
158 LineSpan::new(1, 100),
159 vec![],
160 ),
161 SemanticUnit::new(
162 SemanticUnitKind::Function,
163 "func".to_string(),
164 Visibility::Public,
165 LineSpan::new(10, 20),
166 vec![],
167 ),
168 ];
169
170 let result = find_containing_unit(&units, 15);
171 assert!(result.is_some());
172 assert_eq!(result.expect("should find unit").name, "func");
173
174 let result = find_containing_unit(&units, 50);
175 assert!(result.is_some());
176 assert_eq!(result.expect("should find unit").name, "module");
177
178 let result = find_containing_unit(&units, 200);
179 assert!(result.is_none());
180 }
181}