ryo_executor/engine/impls/
organize_imports.rs1use ryo_analysis::SymbolKind;
15use ryo_mutations::idiom::OrganizeImportsMutation;
16use ryo_mutations::{Mutation, MutationResult};
17use ryo_source::pure::{PureItem, PureUse, PureUseTree, PureVis};
18use std::collections::BTreeMap;
19
20use crate::engine::{ASTMutationContext, ASTRegApply};
21
22impl ASTRegApply for OrganizeImportsMutation {
23 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
24 let mut total_changes = 0;
25
26 let module_ids: Vec<_> = if let Some(target) = self.target_module {
30 if matches!(ctx.symbol_registry.kind(target), Some(SymbolKind::Mod))
31 && ctx.ast_registry.has_module_items(target)
32 {
33 vec![target]
34 } else {
35 vec![]
36 }
37 } else {
38 ctx.symbol_registry
39 .iter()
40 .filter(|(id, _)| {
41 matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Mod))
42 && ctx.ast_registry.has_module_items(*id)
43 })
44 .map(|(id, _)| id)
45 .collect()
46 };
47
48 let module_count = module_ids.len();
49
50 for module_id in module_ids {
52 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(module_id) {
53 let changes =
54 organize_module_items(module_items, self.deduplicate, self.merge_groups);
55 if changes > 0 {
56 ctx.emit_modified(
58 module_id,
59 crate::engine::events::ModificationType::Other(
60 "imports organized".to_string(),
61 ),
62 );
63 }
64 total_changes += changes;
65 }
66 }
67
68 MutationResult {
69 mutation_type: self.mutation_type().to_string(),
70 changes: total_changes,
71 description: if total_changes > 0 {
72 format!("Organized imports in {} module(s)", module_count)
73 } else {
74 "No imports to organize".to_string()
75 },
76 }
77 }
78}
79
80fn organize_module_items(
84 items: &mut Vec<PureItem>,
85 deduplicate: bool,
86 merge_groups: bool,
87) -> usize {
88 let mut uses: Vec<PureUse> = Vec::new();
90 let mut other_items: Vec<PureItem> = Vec::new();
91
92 for item in items.drain(..) {
93 if let PureItem::Use(u) = item {
94 uses.push(u);
95 } else {
96 other_items.push(item);
97 }
98 }
99
100 let original_count = uses.len();
101
102 if uses.is_empty() {
103 *items = other_items;
104 return 0;
105 }
106
107 if deduplicate {
109 let mut seen = Vec::new();
110 uses.retain(|u| {
111 let dominated = seen
112 .iter()
113 .any(|existing: &PureUseTree| trees_equal(existing, &u.tree));
114 if !dominated {
115 seen.push(u.tree.clone());
116 true
117 } else {
118 false
119 }
120 });
121 }
122
123 if merge_groups {
125 let mut pub_uses: Vec<PureUseTree> = Vec::new();
127 let mut priv_uses: Vec<PureUseTree> = Vec::new();
128
129 for u in uses {
130 match u.vis {
131 PureVis::Private => priv_uses.push(u.tree),
132 _ => pub_uses.push(u.tree),
133 }
134 }
135
136 let merged_priv = merge_trees(priv_uses);
138 let merged_pub = merge_trees(pub_uses);
139
140 uses = merged_priv
141 .into_iter()
142 .map(|tree| PureUse {
143 vis: PureVis::Private,
144 tree,
145 })
146 .chain(merged_pub.into_iter().map(|tree| PureUse {
147 vis: PureVis::Public,
148 tree,
149 }))
150 .collect();
151 }
152
153 uses.sort_by(use_cmp);
155
156 let changes = if uses.len() != original_count {
158 original_count.saturating_sub(uses.len())
159 } else {
160 0
161 };
162
163 items.clear();
165 items.extend(uses.into_iter().map(PureItem::Use));
166 items.extend(other_items);
167
168 changes
169}
170
171fn trees_equal(a: &PureUseTree, b: &PureUseTree) -> bool {
173 match (a, b) {
174 (PureUseTree::Path { path: pa, tree: ta }, PureUseTree::Path { path: pb, tree: tb }) => {
175 pa == pb && trees_equal(ta, tb)
176 }
177 (PureUseTree::Name(na), PureUseTree::Name(nb)) => na == nb,
178 (
179 PureUseTree::Rename {
180 name: na,
181 rename: ra,
182 },
183 PureUseTree::Rename {
184 name: nb,
185 rename: rb,
186 },
187 ) => na == nb && ra == rb,
188 (PureUseTree::Glob, PureUseTree::Glob) => true,
189 (PureUseTree::Group(ga), PureUseTree::Group(gb)) => {
190 ga.len() == gb.len() && ga.iter().zip(gb.iter()).all(|(a, b)| trees_equal(a, b))
191 }
192 _ => false,
193 }
194}
195
196fn merge_trees(trees: Vec<PureUseTree>) -> Vec<PureUseTree> {
198 if trees.is_empty() {
199 return vec![];
200 }
201
202 let mut groups: BTreeMap<String, Vec<PureUseTree>> = BTreeMap::new();
204 let mut ungroupable: Vec<PureUseTree> = Vec::new();
205
206 for tree in trees {
207 match &tree {
208 PureUseTree::Path {
209 path,
210 tree: subtree,
211 } => {
212 groups
213 .entry(path.clone())
214 .or_default()
215 .push((**subtree).clone());
216 }
217 _ => ungroupable.push(tree),
218 }
219 }
220
221 let mut result = ungroupable;
223 for (path, subtrees) in groups {
224 if subtrees.len() == 1 {
225 result.push(PureUseTree::Path {
226 path,
227 tree: Box::new(
228 subtrees
229 .into_iter()
230 .next()
231 .expect("len() == 1 guard above guarantees at least one element"),
232 ),
233 });
234 } else {
235 let merged = merge_trees(subtrees);
237 result.push(PureUseTree::Path {
238 path,
239 tree: Box::new(PureUseTree::Group(merged)),
240 });
241 }
242 }
243
244 result
245}
246
247fn use_cmp(a: &PureUse, b: &PureUse) -> std::cmp::Ordering {
249 let vis_cmp = match (&a.vis, &b.vis) {
251 (PureVis::Private, PureVis::Private) => std::cmp::Ordering::Equal,
252 (PureVis::Private, _) => std::cmp::Ordering::Less,
253 (_, PureVis::Private) => std::cmp::Ordering::Greater,
254 _ => std::cmp::Ordering::Equal,
255 };
256
257 if vis_cmp != std::cmp::Ordering::Equal {
258 return vis_cmp;
259 }
260
261 tree_cmp(&a.tree, &b.tree)
263}
264
265fn tree_cmp(a: &PureUseTree, b: &PureUseTree) -> std::cmp::Ordering {
266 let a_str = tree_to_string(a);
267 let b_str = tree_to_string(b);
268 a_str.cmp(&b_str)
269}
270
271fn tree_to_string(tree: &PureUseTree) -> String {
272 match tree {
273 PureUseTree::Path { path, tree } => format!("{}::{}", path, tree_to_string(tree)),
274 PureUseTree::Name(name) => name.clone(),
275 PureUseTree::Rename { name, rename } => format!("{} as {}", name, rename),
276 PureUseTree::Glob => "*".to_string(),
277 PureUseTree::Group(trees) => {
278 let parts: Vec<_> = trees.iter().map(tree_to_string).collect();
279 format!("{{{}}}", parts.join(", "))
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::engine::ASTMutationEngine;
288 use ryo_analysis::testing::ContextBuilder;
289
290 #[test]
291 fn test_v2_organize_imports_target_module_non_mod_id_no_leak() {
292 let mut ctx = ContextBuilder::new()
298 .with_file(
299 "src/lib.rs",
300 r#"
301use std::io;
302use std::io;
303
304fn foo() {}
305"#,
306 )
307 .build();
308
309 let foo_id = ctx
310 .registry
311 .iter()
312 .find(|(id, path)| {
313 matches!(ctx.registry.kind(*id), Some(SymbolKind::Function)) && path.name() == "foo"
314 })
315 .map(|(id, _)| id)
316 .expect("foo function not found");
317
318 let mutation = OrganizeImportsMutation::new().in_module(foo_id);
321 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
322
323 assert_eq!(
324 result.result.changes, 0,
325 "non-Mod target_module must not leak"
326 );
327 }
328
329 fn make_use(path: &str) -> PureUse {
330 let parts: Vec<&str> = path.split("::").collect();
331 let tree = parts
332 .iter()
333 .rev()
334 .fold(None, |acc, part| {
335 Some(match acc {
336 None => PureUseTree::Name(part.to_string()),
337 Some(subtree) => PureUseTree::Path {
338 path: part.to_string(),
339 tree: Box::new(subtree),
340 },
341 })
342 })
343 .unwrap();
344
345 PureUse {
346 vis: PureVis::Private,
347 tree,
348 }
349 }
350
351 #[test]
352 fn test_organize_deduplicates() {
353 let mut items = vec![
354 PureItem::Use(make_use("std::io")),
355 PureItem::Use(make_use("std::io")),
356 PureItem::Use(make_use("std::fs")),
357 ];
358
359 let changes = organize_module_items(&mut items, true, false);
360
361 assert_eq!(changes, 1);
362 assert_eq!(
363 items
364 .iter()
365 .filter(|i| matches!(i, PureItem::Use(_)))
366 .count(),
367 2
368 );
369 }
370
371 #[test]
372 fn test_organize_no_changes_when_unique() {
373 let mut items = vec![
374 PureItem::Use(make_use("std::io")),
375 PureItem::Use(make_use("std::fs")),
376 ];
377
378 let changes = organize_module_items(&mut items, true, false);
379
380 assert_eq!(changes, 0);
381 }
382
383 #[test]
384 fn test_trees_equal() {
385 let tree1 = PureUseTree::Name("foo".to_string());
386 let tree2 = PureUseTree::Name("foo".to_string());
387
388 assert!(trees_equal(&tree1, &tree2));
389 }
390
391 #[test]
392 fn test_trees_not_equal_different_names() {
393 let tree1 = PureUseTree::Name("foo".to_string());
394 let tree2 = PureUseTree::Name("bar".to_string());
395
396 assert!(!trees_equal(&tree1, &tree2));
397 }
398}