use ryo_analysis::SymbolKind;
use ryo_mutations::idiom::OrganizeImportsMutation;
use ryo_mutations::{Mutation, MutationResult};
use ryo_source::pure::{PureItem, PureUse, PureUseTree, PureVis};
use std::collections::BTreeMap;
use crate::engine::{ASTMutationContext, ASTRegApply};
impl ASTRegApply for OrganizeImportsMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
let mut total_changes = 0;
let module_ids: Vec<_> = ctx
.symbol_registry
.iter()
.filter(|(id, _)| {
matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Mod))
&& ctx.ast_registry.has_module_items(*id)
})
.map(|(id, _)| id)
.collect();
let module_count = module_ids.len();
for module_id in module_ids {
if let Some(module_items) = ctx.ast_registry.get_module_items_mut(module_id) {
let changes =
organize_module_items(module_items, self.deduplicate, self.merge_groups);
if changes > 0 {
ctx.emit_modified(
module_id,
crate::engine::events::ModificationType::Other(
"imports organized".to_string(),
),
);
}
total_changes += changes;
}
}
MutationResult {
mutation_type: self.mutation_type().to_string(),
changes: total_changes,
description: if total_changes > 0 {
format!("Organized imports in {} module(s)", module_count)
} else {
"No imports to organize".to_string()
},
}
}
}
fn organize_module_items(
items: &mut Vec<PureItem>,
deduplicate: bool,
merge_groups: bool,
) -> usize {
let mut uses: Vec<PureUse> = Vec::new();
let mut other_items: Vec<PureItem> = Vec::new();
for item in items.drain(..) {
if let PureItem::Use(u) = item {
uses.push(u);
} else {
other_items.push(item);
}
}
let original_count = uses.len();
if uses.is_empty() {
*items = other_items;
return 0;
}
if deduplicate {
let mut seen = Vec::new();
uses.retain(|u| {
let dominated = seen
.iter()
.any(|existing: &PureUseTree| trees_equal(existing, &u.tree));
if !dominated {
seen.push(u.tree.clone());
true
} else {
false
}
});
}
if merge_groups {
let mut pub_uses: Vec<PureUseTree> = Vec::new();
let mut priv_uses: Vec<PureUseTree> = Vec::new();
for u in uses {
match u.vis {
PureVis::Private => priv_uses.push(u.tree),
_ => pub_uses.push(u.tree),
}
}
let merged_priv = merge_trees(priv_uses);
let merged_pub = merge_trees(pub_uses);
uses = merged_priv
.into_iter()
.map(|tree| PureUse {
vis: PureVis::Private,
tree,
})
.chain(merged_pub.into_iter().map(|tree| PureUse {
vis: PureVis::Public,
tree,
}))
.collect();
}
uses.sort_by(use_cmp);
let changes = if uses.len() != original_count {
original_count.saturating_sub(uses.len())
} else {
0
};
items.clear();
items.extend(uses.into_iter().map(PureItem::Use));
items.extend(other_items);
changes
}
fn trees_equal(a: &PureUseTree, b: &PureUseTree) -> bool {
match (a, b) {
(PureUseTree::Path { path: pa, tree: ta }, PureUseTree::Path { path: pb, tree: tb }) => {
pa == pb && trees_equal(ta, tb)
}
(PureUseTree::Name(na), PureUseTree::Name(nb)) => na == nb,
(
PureUseTree::Rename {
name: na,
rename: ra,
},
PureUseTree::Rename {
name: nb,
rename: rb,
},
) => na == nb && ra == rb,
(PureUseTree::Glob, PureUseTree::Glob) => true,
(PureUseTree::Group(ga), PureUseTree::Group(gb)) => {
ga.len() == gb.len() && ga.iter().zip(gb.iter()).all(|(a, b)| trees_equal(a, b))
}
_ => false,
}
}
fn merge_trees(trees: Vec<PureUseTree>) -> Vec<PureUseTree> {
if trees.is_empty() {
return vec![];
}
let mut groups: BTreeMap<String, Vec<PureUseTree>> = BTreeMap::new();
let mut ungroupable: Vec<PureUseTree> = Vec::new();
for tree in trees {
match &tree {
PureUseTree::Path {
path,
tree: subtree,
} => {
groups
.entry(path.clone())
.or_default()
.push((**subtree).clone());
}
_ => ungroupable.push(tree),
}
}
let mut result = ungroupable;
for (path, subtrees) in groups {
if subtrees.len() == 1 {
result.push(PureUseTree::Path {
path,
tree: Box::new(
subtrees
.into_iter()
.next()
.expect("len() == 1 guard above guarantees at least one element"),
),
});
} else {
let merged = merge_trees(subtrees);
result.push(PureUseTree::Path {
path,
tree: Box::new(PureUseTree::Group(merged)),
});
}
}
result
}
fn use_cmp(a: &PureUse, b: &PureUse) -> std::cmp::Ordering {
let vis_cmp = match (&a.vis, &b.vis) {
(PureVis::Private, PureVis::Private) => std::cmp::Ordering::Equal,
(PureVis::Private, _) => std::cmp::Ordering::Less,
(_, PureVis::Private) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
};
if vis_cmp != std::cmp::Ordering::Equal {
return vis_cmp;
}
tree_cmp(&a.tree, &b.tree)
}
fn tree_cmp(a: &PureUseTree, b: &PureUseTree) -> std::cmp::Ordering {
let a_str = tree_to_string(a);
let b_str = tree_to_string(b);
a_str.cmp(&b_str)
}
fn tree_to_string(tree: &PureUseTree) -> String {
match tree {
PureUseTree::Path { path, tree } => format!("{}::{}", path, tree_to_string(tree)),
PureUseTree::Name(name) => name.clone(),
PureUseTree::Rename { name, rename } => format!("{} as {}", name, rename),
PureUseTree::Glob => "*".to_string(),
PureUseTree::Group(trees) => {
let parts: Vec<_> = trees.iter().map(tree_to_string).collect();
format!("{{{}}}", parts.join(", "))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_use(path: &str) -> PureUse {
let parts: Vec<&str> = path.split("::").collect();
let tree = parts
.iter()
.rev()
.fold(None, |acc, part| {
Some(match acc {
None => PureUseTree::Name(part.to_string()),
Some(subtree) => PureUseTree::Path {
path: part.to_string(),
tree: Box::new(subtree),
},
})
})
.unwrap();
PureUse {
vis: PureVis::Private,
tree,
}
}
#[test]
fn test_organize_deduplicates() {
let mut items = vec![
PureItem::Use(make_use("std::io")),
PureItem::Use(make_use("std::io")),
PureItem::Use(make_use("std::fs")),
];
let changes = organize_module_items(&mut items, true, false);
assert_eq!(changes, 1);
assert_eq!(
items
.iter()
.filter(|i| matches!(i, PureItem::Use(_)))
.count(),
2
);
}
#[test]
fn test_organize_no_changes_when_unique() {
let mut items = vec![
PureItem::Use(make_use("std::io")),
PureItem::Use(make_use("std::fs")),
];
let changes = organize_module_items(&mut items, true, false);
assert_eq!(changes, 0);
}
#[test]
fn test_trees_equal() {
let tree1 = PureUseTree::Name("foo".to_string());
let tree2 = PureUseTree::Name("foo".to_string());
assert!(trees_equal(&tree1, &tree2));
}
#[test]
fn test_trees_not_equal_different_names() {
let tree1 = PureUseTree::Name("foo".to_string());
let tree2 = PureUseTree::Name("bar".to_string());
assert!(!trees_equal(&tree1, &tree2));
}
}