cargo_quality/differ/display/
grouping.rs1use std::collections::{BTreeMap, HashSet};
5
6pub fn group_imports(imports: &[&str]) -> Vec<String> {
62 if imports.is_empty() {
63 return Vec::new();
64 }
65
66 let unique: HashSet<&str> = imports.iter().copied().collect();
67 let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();
68
69 for import in unique {
70 let import_str = import
71 .trim_start_matches("use ")
72 .trim_end_matches(';')
73 .trim();
74
75 if let Some(double_colon_pos) = import_str.find("::") {
76 let root = import_str[..double_colon_pos].to_string();
77 let path = import_str[double_colon_pos + 2..].to_string();
78 grouped.entry(root).or_default().push(path);
79 } else {
80 grouped
81 .entry(import_str.to_string())
82 .or_default()
83 .push(String::new());
84 }
85 }
86
87 let mut result = Vec::with_capacity(grouped.len());
88
89 for (root, mut paths) in grouped {
90 if paths.len() == 1 && !paths[0].is_empty() {
91 result.push(format!("use {}::{};", root, paths[0]));
92 } else if paths.len() == 1 && paths[0].is_empty() {
93 result.push(format!("use {};", root));
94 } else {
95 paths.sort_unstable();
96
97 let common_prefix = find_common_prefix(&paths);
98
99 if !common_prefix.is_empty() {
100 let prefix_with_sep = if paths[0].starts_with(&format!("{}::", common_prefix)) {
101 format!("{}::", common_prefix)
102 } else {
103 common_prefix.clone()
104 };
105
106 let suffixes: Vec<String> = paths
107 .iter()
108 .map(|p| {
109 p.strip_prefix(&prefix_with_sep)
110 .unwrap_or(p.as_str())
111 .to_string()
112 })
113 .collect();
114
115 if prefix_with_sep.ends_with("::") {
116 result.push(format!(
117 "use {}::{}::{{{}}};",
118 root,
119 common_prefix,
120 suffixes.join(", ")
121 ));
122 } else {
123 result.push(format!("use {}::{{{}}};", root, suffixes.join(", ")));
124 }
125 } else {
126 result.push(format!("use {}::{{{}}};", root, paths.join(", ")));
127 }
128 }
129 }
130
131 result
132}
133
134pub fn find_common_prefix(paths: &[String]) -> String {
184 if paths.is_empty() {
185 return String::new();
186 }
187
188 if paths.len() == 1 {
189 return String::new();
190 }
191
192 let parts: Vec<Vec<&str>> = paths.iter().map(|p| p.split("::").collect()).collect();
193
194 let min_len = parts.iter().map(|p| p.len()).min().unwrap_or(0);
195
196 let mut common = Vec::with_capacity(min_len);
197
198 for i in 0..min_len {
199 let first = parts[0][i];
200 if parts.iter().all(|p| p[i] == first) {
201 common.push(first);
202 } else {
203 break;
204 }
205 }
206
207 if !common.is_empty() {
208 common.join("::")
209 } else {
210 String::new()
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_group_imports_deduplication() {
220 let imports = vec!["use std::fs::write;", "use std::fs::write;"];
221 let result = group_imports(&imports);
222 assert_eq!(result.len(), 1);
223 }
224
225 #[test]
226 fn test_group_imports_same_root() {
227 let imports = vec!["use std::fs::write;", "use std::io::read;"];
228 let result = group_imports(&imports);
229 assert_eq!(result.len(), 1);
230 assert!(result[0].contains("std::{"));
231 }
232
233 #[test]
234 fn test_group_imports_common_path() {
235 let imports = vec!["use syn::visit::visit_file;", "use syn::visit::visit_expr;"];
236 let result = group_imports(&imports);
237 assert_eq!(result.len(), 1);
238 assert!(result[0].contains("syn::visit::{"));
239 }
240
241 #[test]
242 fn test_group_imports_empty() {
243 let imports: Vec<&str> = vec![];
244 let result = group_imports(&imports);
245 assert!(result.is_empty());
246 }
247
248 #[test]
249 fn test_find_common_prefix_same() {
250 let paths = vec![
251 "visit::visit_file".to_string(),
252 "visit::visit_expr".to_string(),
253 ];
254 let prefix = find_common_prefix(&paths);
255 assert_eq!(prefix, "visit");
256 }
257
258 #[test]
259 fn test_find_common_prefix_different() {
260 let paths = vec!["fs::read".to_string(), "io::write".to_string()];
261 let prefix = find_common_prefix(&paths);
262 assert!(prefix.is_empty());
263 }
264
265 #[test]
266 fn test_find_common_prefix_empty() {
267 let paths: Vec<String> = vec![];
268 let prefix = find_common_prefix(&paths);
269 assert!(prefix.is_empty());
270 }
271
272 #[test]
273 fn test_find_common_prefix_single() {
274 let paths = vec!["test::path".to_string()];
275 let prefix = find_common_prefix(&paths);
276 assert!(prefix.is_empty());
277 }
278
279 #[test]
280 fn test_group_imports_multiple_roots() {
281 let imports = vec!["use std::fs::write;", "use syn::visit::visit_file;"];
282 let result = group_imports(&imports);
283 assert_eq!(result.len(), 2);
284 }
285
286 #[test]
287 fn test_group_imports_single_item() {
288 let imports = vec!["use std::fs::write;"];
289 let result = group_imports(&imports);
290 assert_eq!(result.len(), 1);
291 assert_eq!(result[0], "use std::fs::write;");
292 }
293
294 #[test]
295 fn test_find_common_prefix_partial() {
296 let paths = vec![
297 "a::b::c".to_string(),
298 "a::b::d".to_string(),
299 "a::b::e".to_string(),
300 ];
301 let prefix = find_common_prefix(&paths);
302 assert_eq!(prefix, "a::b");
303 }
304}