Skip to main content

wowlab_tidy/languages/rust/rules/style/
pub_use_grouping.rs

1use 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 != &current) {
98            closed.insert(previous.take().unwrap_or_default());
99        }
100
101        if closed.contains(&current) {
102            return false;
103        }
104
105        previous = Some(current);
106    }
107
108    true
109}
110
111// #t(fn: rust_clone_in_loop) syntax-editor sorting requires detached copies of source items
112fn 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});