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        // When target_module is set, restrict to that module only (no leak to
28        // unrelated modules in the same workspace).
29        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        // Process each module
51        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                    // Emit event so sync_files_and_rebuild regenerates this module's file
57                    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
80/// Organize use statements within module items
81///
82/// Returns number of changes made.
83fn organize_module_items(
84    items: &mut Vec<PureItem>,
85    deduplicate: bool,
86    merge_groups: bool,
87) -> usize {
88    // Extract use statements
89    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    // Deduplicate if requested
108    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    // Merge groups if requested
124    if merge_groups {
125        // Group by visibility first
126        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        // Merge each group
137        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    // Sort uses for deterministic output
154    uses.sort_by(use_cmp);
155
156    // Calculate changes
157    let changes = if uses.len() != original_count {
158        original_count.saturating_sub(uses.len())
159    } else {
160        0
161    };
162
163    // Rebuild items: uses first, then other items
164    items.clear();
165    items.extend(uses.into_iter().map(PureItem::Use));
166    items.extend(other_items);
167
168    changes
169}
170
171/// Check if two use trees are structurally equal
172fn 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
196/// Merge use trees by common prefix
197fn merge_trees(trees: Vec<PureUseTree>) -> Vec<PureUseTree> {
198    if trees.is_empty() {
199        return vec![];
200    }
201
202    // Group by first path segment
203    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    // Build merged trees
222    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            // Recursively merge subtrees
236            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
247/// Compare use statements for sorting
248fn use_cmp(a: &PureUse, b: &PureUse) -> std::cmp::Ordering {
249    // pub uses come after private uses
250    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    // Then sort by tree string representation
262    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        // Boundary regression: when target_module is set to a non-Mod kind id
293        // (e.g. a Function id via scope_spec_to_function defensive setting),
294        // the impl must not iterate any module. Pre-fix the converter discarded
295        // module_id via `..` and the impl iterated every module unconditionally,
296        // leaking import reorganization across the workspace.
297        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        // Pass a Function id as target_module — must result in 0 changes
319        // because the impl filters to SymbolKind::Mod only.
320        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}