rust_diff_analyzer/analysis/
mapper.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use 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
17/// Maps diff changes to semantic units
18///
19/// # Arguments
20///
21/// * `diffs` - Vector of file diffs
22/// * `config` - Configuration
23/// * `file_reader` - Function to read file contents
24///
25/// # Returns
26///
27/// Vector of changes or error
28///
29/// # Errors
30///
31/// Returns error if file reading or parsing fails
32///
33/// # Examples
34///
35/// ```no_run
36/// use std::fs;
37///
38/// use rust_diff_analyzer::{analysis::map_changes, config::Config};
39///
40/// let diffs = vec![];
41/// let config = Config::default();
42/// let changes = map_changes(&diffs, &config, |p| fs::read_to_string(p));
43/// ```
44pub 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}