Skip to main content

ryo_executor/engine/impls/
organize_imports.rs

1//! V2 ASTRegApply implementation for OrganizeImportsMutation
2//!
3//! # Implementation Notes
4//!
5//! OrganizeImports operates on module_items stored in ASTRegistry.
6//! This enables access to use statements which are not individual symbols.
7//!
8//! The mutation:
9//! 1. Iterates through all modules with stored module_items
10//! 2. Extracts use statements from each module
11//! 3. Applies deduplication and/or merging based on options
12//! 4. Updates the module_items with reorganized uses
13
14use 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        // Collect module IDs that have module_items
27        let module_ids: Vec<_> = ctx
28            .symbol_registry
29            .iter()
30            .filter(|(id, _)| {
31                matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Mod))
32                    && ctx.ast_registry.has_module_items(*id)
33            })
34            .map(|(id, _)| id)
35            .collect();
36
37        let module_count = module_ids.len();
38
39        // Process each module
40        for module_id in module_ids {
41            if let Some(module_items) = ctx.ast_registry.get_module_items_mut(module_id) {
42                let changes =
43                    organize_module_items(module_items, self.deduplicate, self.merge_groups);
44                if changes > 0 {
45                    // Emit event so sync_files_and_rebuild regenerates this module's file
46                    ctx.emit_modified(
47                        module_id,
48                        crate::engine::events::ModificationType::Other(
49                            "imports organized".to_string(),
50                        ),
51                    );
52                }
53                total_changes += changes;
54            }
55        }
56
57        MutationResult {
58            mutation_type: self.mutation_type().to_string(),
59            changes: total_changes,
60            description: if total_changes > 0 {
61                format!("Organized imports in {} module(s)", module_count)
62            } else {
63                "No imports to organize".to_string()
64            },
65        }
66    }
67}
68
69/// Organize use statements within module items
70///
71/// Returns number of changes made.
72fn organize_module_items(
73    items: &mut Vec<PureItem>,
74    deduplicate: bool,
75    merge_groups: bool,
76) -> usize {
77    // Extract use statements
78    let mut uses: Vec<PureUse> = Vec::new();
79    let mut other_items: Vec<PureItem> = Vec::new();
80
81    for item in items.drain(..) {
82        if let PureItem::Use(u) = item {
83            uses.push(u);
84        } else {
85            other_items.push(item);
86        }
87    }
88
89    let original_count = uses.len();
90
91    if uses.is_empty() {
92        *items = other_items;
93        return 0;
94    }
95
96    // Deduplicate if requested
97    if deduplicate {
98        let mut seen = Vec::new();
99        uses.retain(|u| {
100            let dominated = seen
101                .iter()
102                .any(|existing: &PureUseTree| trees_equal(existing, &u.tree));
103            if !dominated {
104                seen.push(u.tree.clone());
105                true
106            } else {
107                false
108            }
109        });
110    }
111
112    // Merge groups if requested
113    if merge_groups {
114        // Group by visibility first
115        let mut pub_uses: Vec<PureUseTree> = Vec::new();
116        let mut priv_uses: Vec<PureUseTree> = Vec::new();
117
118        for u in uses {
119            match u.vis {
120                PureVis::Private => priv_uses.push(u.tree),
121                _ => pub_uses.push(u.tree),
122            }
123        }
124
125        // Merge each group
126        let merged_priv = merge_trees(priv_uses);
127        let merged_pub = merge_trees(pub_uses);
128
129        uses = merged_priv
130            .into_iter()
131            .map(|tree| PureUse {
132                vis: PureVis::Private,
133                tree,
134            })
135            .chain(merged_pub.into_iter().map(|tree| PureUse {
136                vis: PureVis::Public,
137                tree,
138            }))
139            .collect();
140    }
141
142    // Sort uses for deterministic output
143    uses.sort_by(use_cmp);
144
145    // Calculate changes
146    let changes = if uses.len() != original_count {
147        original_count.saturating_sub(uses.len())
148    } else {
149        0
150    };
151
152    // Rebuild items: uses first, then other items
153    items.clear();
154    items.extend(uses.into_iter().map(PureItem::Use));
155    items.extend(other_items);
156
157    changes
158}
159
160/// Check if two use trees are structurally equal
161fn trees_equal(a: &PureUseTree, b: &PureUseTree) -> bool {
162    match (a, b) {
163        (PureUseTree::Path { path: pa, tree: ta }, PureUseTree::Path { path: pb, tree: tb }) => {
164            pa == pb && trees_equal(ta, tb)
165        }
166        (PureUseTree::Name(na), PureUseTree::Name(nb)) => na == nb,
167        (
168            PureUseTree::Rename {
169                name: na,
170                rename: ra,
171            },
172            PureUseTree::Rename {
173                name: nb,
174                rename: rb,
175            },
176        ) => na == nb && ra == rb,
177        (PureUseTree::Glob, PureUseTree::Glob) => true,
178        (PureUseTree::Group(ga), PureUseTree::Group(gb)) => {
179            ga.len() == gb.len() && ga.iter().zip(gb.iter()).all(|(a, b)| trees_equal(a, b))
180        }
181        _ => false,
182    }
183}
184
185/// Merge use trees by common prefix
186fn merge_trees(trees: Vec<PureUseTree>) -> Vec<PureUseTree> {
187    if trees.is_empty() {
188        return vec![];
189    }
190
191    // Group by first path segment
192    let mut groups: BTreeMap<String, Vec<PureUseTree>> = BTreeMap::new();
193    let mut ungroupable: Vec<PureUseTree> = Vec::new();
194
195    for tree in trees {
196        match &tree {
197            PureUseTree::Path {
198                path,
199                tree: subtree,
200            } => {
201                groups
202                    .entry(path.clone())
203                    .or_default()
204                    .push((**subtree).clone());
205            }
206            _ => ungroupable.push(tree),
207        }
208    }
209
210    // Build merged trees
211    let mut result = ungroupable;
212    for (path, subtrees) in groups {
213        if subtrees.len() == 1 {
214            result.push(PureUseTree::Path {
215                path,
216                tree: Box::new(
217                    subtrees
218                        .into_iter()
219                        .next()
220                        .expect("len() == 1 guard above guarantees at least one element"),
221                ),
222            });
223        } else {
224            // Recursively merge subtrees
225            let merged = merge_trees(subtrees);
226            result.push(PureUseTree::Path {
227                path,
228                tree: Box::new(PureUseTree::Group(merged)),
229            });
230        }
231    }
232
233    result
234}
235
236/// Compare use statements for sorting
237fn use_cmp(a: &PureUse, b: &PureUse) -> std::cmp::Ordering {
238    // pub uses come after private uses
239    let vis_cmp = match (&a.vis, &b.vis) {
240        (PureVis::Private, PureVis::Private) => std::cmp::Ordering::Equal,
241        (PureVis::Private, _) => std::cmp::Ordering::Less,
242        (_, PureVis::Private) => std::cmp::Ordering::Greater,
243        _ => std::cmp::Ordering::Equal,
244    };
245
246    if vis_cmp != std::cmp::Ordering::Equal {
247        return vis_cmp;
248    }
249
250    // Then sort by tree string representation
251    tree_cmp(&a.tree, &b.tree)
252}
253
254fn tree_cmp(a: &PureUseTree, b: &PureUseTree) -> std::cmp::Ordering {
255    let a_str = tree_to_string(a);
256    let b_str = tree_to_string(b);
257    a_str.cmp(&b_str)
258}
259
260fn tree_to_string(tree: &PureUseTree) -> String {
261    match tree {
262        PureUseTree::Path { path, tree } => format!("{}::{}", path, tree_to_string(tree)),
263        PureUseTree::Name(name) => name.clone(),
264        PureUseTree::Rename { name, rename } => format!("{} as {}", name, rename),
265        PureUseTree::Glob => "*".to_string(),
266        PureUseTree::Group(trees) => {
267            let parts: Vec<_> = trees.iter().map(tree_to_string).collect();
268            format!("{{{}}}", parts.join(", "))
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    fn make_use(path: &str) -> PureUse {
278        let parts: Vec<&str> = path.split("::").collect();
279        let tree = parts
280            .iter()
281            .rev()
282            .fold(None, |acc, part| {
283                Some(match acc {
284                    None => PureUseTree::Name(part.to_string()),
285                    Some(subtree) => PureUseTree::Path {
286                        path: part.to_string(),
287                        tree: Box::new(subtree),
288                    },
289                })
290            })
291            .unwrap();
292
293        PureUse {
294            vis: PureVis::Private,
295            tree,
296        }
297    }
298
299    #[test]
300    fn test_organize_deduplicates() {
301        let mut items = vec![
302            PureItem::Use(make_use("std::io")),
303            PureItem::Use(make_use("std::io")),
304            PureItem::Use(make_use("std::fs")),
305        ];
306
307        let changes = organize_module_items(&mut items, true, false);
308
309        assert_eq!(changes, 1);
310        assert_eq!(
311            items
312                .iter()
313                .filter(|i| matches!(i, PureItem::Use(_)))
314                .count(),
315            2
316        );
317    }
318
319    #[test]
320    fn test_organize_no_changes_when_unique() {
321        let mut items = vec![
322            PureItem::Use(make_use("std::io")),
323            PureItem::Use(make_use("std::fs")),
324        ];
325
326        let changes = organize_module_items(&mut items, true, false);
327
328        assert_eq!(changes, 0);
329    }
330
331    #[test]
332    fn test_trees_equal() {
333        let tree1 = PureUseTree::Name("foo".to_string());
334        let tree2 = PureUseTree::Name("foo".to_string());
335
336        assert!(trees_equal(&tree1, &tree2));
337    }
338
339    #[test]
340    fn test_trees_not_equal_different_names() {
341        let tree1 = PureUseTree::Name("foo".to_string());
342        let tree2 = PureUseTree::Name("bar".to_string());
343
344        assert!(!trees_equal(&tree1, &tree2));
345    }
346}