Skip to main content

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

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasName},
4    syntax_editor::SyntaxEditor,
5};
6
7use super::support::item_lists;
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12    Example {
13        label: "sorted declarations",
14        code: "mod alpha;\npub mod beta;\nmod gamma;",
15        pass: true,
16    },
17    Example {
18        label: "visibility does not affect ordering",
19        code: "pub mod zebra;\nmod alpha;",
20        pass: false,
21    },
22    Example {
23        label: "separate declaration blocks",
24        code: "mod zebra;\nfn boundary() {}\nmod alpha;",
25        pass: true,
26    },
27    Example {
28        label: "inline modules are boundaries",
29        code: "mod zebra {}\nmod alpha;",
30        pass: true,
31    },
32    Example {
33        label: "raw identifiers sort by their semantic name",
34        code: "mod spell;\nmod r#trait;\nmod weapon;",
35        pass: true,
36    },
37];
38
39crate::ast_tree_rule!(
40    mod_order,
41    "Require contiguous module-declaration blocks to be alphabetically sorted.",
42    "Stable module ordering makes module inventories predictable without grouping by visibility.",
43    Low,
44    fix_mod_order,
45);
46
47fn check_mod_order(ctx: &AstCtx<'_>) -> Vec<Violation> {
48    item_lists(ctx.root)
49        .flat_map(module_blocks)
50        .filter_map(|block| {
51            let names: Vec<String> = block.iter().filter_map(module_sort_key).collect();
52
53            (!names.is_sorted()).then(|| {
54                ctx.violation(
55                    &block[0],
56                    "module declarations are not alphabetically sorted",
57                )
58            })
59        })
60        .collect()
61}
62
63fn module_blocks(items: Vec<ast::Item>) -> impl Iterator<Item = Vec<ast::Module>> {
64    let mut blocks = Vec::new();
65    let mut current = Vec::new();
66
67    for item in items {
68        if let ast::Item::Module(module) = item
69            && module.item_list().is_none()
70        {
71            current.push(module);
72        } else if !current.is_empty() {
73            blocks.push(std::mem::take(&mut current));
74        }
75    }
76
77    if !current.is_empty() {
78        blocks.push(current);
79    }
80
81    blocks.into_iter().filter(|block| block.len() > 1)
82}
83
84// #t(fn: rust_clone_in_loop) syntax-editor sorting requires detached copies of source items
85fn fix_mod_order(ctx: &AstCtx<'_>, _violations: &[Violation]) -> Option<String> {
86    let (editor, root) = SyntaxEditor::with_ast_node(ctx.root);
87    let mut changed = false;
88
89    for block in item_lists(&root).flat_map(module_blocks) {
90        let mut target = block.clone();
91
92        target.sort_by_key(module_sort_key);
93
94        if block == target {
95            continue;
96        }
97
98        for (old, new) in block.iter().zip(&target) {
99            editor.replace(old.syntax().clone(), new.syntax().clone_subtree());
100        }
101
102        changed = true;
103    }
104
105    changed.then(|| editor.finish().new_root().to_string())
106}
107
108fn module_sort_key(module: &ast::Module) -> Option<String> {
109    module
110        .name()
111        .map(|name| name.text().trim_start_matches("r#").to_owned())
112}
113
114crate::tidy_ast_test!(check_mod_order, {
115    crate::example_tests!(EXAMPLES, check_mod_order);
116    crate::fix_tests!(ast_tree, check_mod_order, fix_mod_order);
117});