wowlab_tidy/languages/rust/rules/style/
pub_use_grouping.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasVisibility},
4 syntax_editor::SyntaxEditor,
5};
6use wowlab_types::sim::{FastMap, FastSet};
7
8use super::support::item_lists;
9use crate::{AstCtx, Example, Violation};
10
11#[rustfmt::skip]
12const EXAMPLES: &[Example] = &[
13 Example {
14 label: "origins are adjacent",
15 code: "pub use alpha::One;\npub use alpha::Two;\npub use beta::Three;",
16 pass: true,
17 },
18 Example {
19 label: "origin repeats after another group",
20 code: "pub use alpha::One;\npub use beta::Two;\npub use alpha::Three;",
21 pass: false,
22 },
23 Example {
24 label: "origin group order is author chosen",
25 code: "pub use zebra::One;\npub use alpha::Two;",
26 pass: true,
27 },
28 Example {
29 label: "plain use separates blocks",
30 code: "pub use alpha::One;\nuse beta::Two;\npub use alpha::Three;",
31 pass: true,
32 },
33];
34
35crate::ast_tree_rule!(
36 pub_use_grouping,
37 "Require public re-exports from the same origin to be adjacent.",
38 "Keeping each re-export origin contiguous makes public API inventories easier to scan.",
39 Low,
40 fix_pub_use_grouping,
41);
42
43fn check_pub_use_grouping(ctx: &AstCtx<'_>) -> Vec<Violation> {
44 item_lists(ctx.root)
45 .flat_map(public_use_blocks)
46 .filter_map(|block| {
47 (!origins_are_grouped(&block)).then(|| {
48 ctx.violation(
49 &block[0],
50 "public re-exports from the same first path segment must be adjacent",
51 )
52 })
53 })
54 .collect()
55}
56
57fn public_use_blocks(items: Vec<ast::Item>) -> impl Iterator<Item = Vec<ast::Use>> {
58 let mut blocks = Vec::new();
59 let mut current = Vec::new();
60
61 for item in items {
62 if let ast::Item::Use(use_item) = item
63 && use_item.visibility().is_some()
64 {
65 current.push(use_item);
66 } else if !current.is_empty() {
67 blocks.push(std::mem::take(&mut current));
68 }
69 }
70
71 if !current.is_empty() {
72 blocks.push(current);
73 }
74
75 blocks.into_iter().filter(|block| block.len() > 1)
76}
77
78fn origin(item: &ast::Use) -> String {
79 let Some(mut path) = item.use_tree().and_then(|tree| tree.path()) else {
80 return String::new();
81 };
82
83 while let Some(qualifier) = path.qualifier() {
84 path = qualifier;
85 }
86
87 path.segment()
88 .and_then(|segment| segment.name_ref())
89 .map_or_else(String::new, |name| name.text().to_string())
90}
91
92fn origins_are_grouped(block: &[ast::Use]) -> bool {
93 let mut closed = FastSet::default();
94 let mut previous = None;
95
96 for current in block.iter().map(origin) {
97 if previous.as_ref().is_some_and(|last| last != ¤t) {
98 closed.insert(previous.take().unwrap_or_default());
99 }
100
101 if closed.contains(¤t) {
102 return false;
103 }
104
105 previous = Some(current);
106 }
107
108 true
109}
110
111fn fix_pub_use_grouping(ctx: &AstCtx<'_>, _violations: &[Violation]) -> Option<String> {
113 let (editor, root) = SyntaxEditor::with_ast_node(ctx.root);
114 let mut changed = false;
115
116 for block in item_lists(&root).flat_map(public_use_blocks) {
117 if origins_are_grouped(&block) {
118 continue;
119 }
120
121 let mut groups: Vec<Vec<ast::Use>> = Vec::with_capacity(block.len());
122 let mut positions = FastMap::<String, usize>::default();
123
124 for item in &block {
125 let key = origin(item);
126 let next = positions.len();
127 let index = *positions.entry(key).or_insert_with(|| {
128 groups.push(Vec::new());
129
130 next
131 });
132
133 groups
134 .get_mut(index)
135 .expect("origin position always names an initialized group")
136 .push(item.clone());
137 }
138
139 let target: Vec<ast::Use> = groups.into_iter().flatten().collect();
140
141 for (old, new) in block.iter().zip(&target) {
142 editor.replace(old.syntax().clone(), new.syntax().clone_subtree());
143 }
144
145 changed = true;
146 }
147
148 changed.then(|| editor.finish().new_root().to_string())
149}
150
151crate::tidy_ast_test!(check_pub_use_grouping, {
152 crate::example_tests!(EXAMPLES, check_pub_use_grouping);
153 crate::fix_tests!(ast_tree, check_pub_use_grouping, fix_pub_use_grouping);
154});