Skip to main content

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

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasVisibility},
4    syntax_editor::SyntaxEditor,
5};
6
7use crate::{AstCtx, Example, Violation};
8
9const PUBLIC_METHOD_RANK: u8 = 2;
10const RESTRICTED_METHOD_RANK: u8 = 3;
11const PRIVATE_METHOD_RANK: u8 = 4;
12const MACRO_CALL_RANK: u8 = 5;
13
14#[rustfmt::skip]
15const EXAMPLES: &[Example] = &[
16    Example {
17        label: "canonical member groups",
18        code: "struct Item;\nimpl Item {\n    const LIMIT: usize = 1;\n    type Value = usize;\n    pub fn new() -> Self { Self }\n    pub fn value(&self) {}\n    pub(crate) fn shared(&self) {}\n    fn private(&self) {}\n}",
19        pass: true,
20    },
21    Example {
22        label: "constructor follows method",
23        code: "struct Item;\nimpl Item {\n    pub fn value(&self) {}\n    /// Builds an item.\n    #[cfg(test)]\n    pub fn new() -> Result<Self, ()> { Ok(Self) }\n}",
24        pass: false,
25    },
26    Example {
27        label: "restricted method before public method",
28        code: "struct Item;\nimpl Item {\n    pub(crate) fn shared(&self) {}\n    #[inline]\n    pub fn visible(&self) {}\n}",
29        pass: false,
30    },
31    Example {
32        label: "trait impl is exempt",
33        code: "struct Item;\nimpl Default for Item {\n    fn default() -> Self { Self }\n    const VALUE: usize = 1;\n}",
34        pass: true,
35    },
36];
37
38crate::ast_tree_rule!(
39    impl_member_order,
40    "Require inherent impl members to follow the canonical category and visibility order.",
41    "Predictable inherent impl inventories keep construction and public APIs ahead of implementation details.",
42    Medium,
43    fix_impl_member_order,
44);
45
46fn check_impl_member_order(ctx: &AstCtx<'_>) -> Vec<Violation> {
47    ctx.nodes::<ast::Impl>()
48        .filter(|item_impl| item_impl.trait_().is_none())
49        .filter_map(|item_impl| {
50            let members: Vec<ast::AssocItem> = item_impl.assoc_item_list()?.assoc_items().collect();
51            let ranks: Vec<u8> = members.iter().map(member_rank).collect();
52
53            (!ranks.is_sorted()).then(|| {
54                ctx.violation(
55                    &item_impl,
56                    "inherent impl members must be ordered as associated items, constructors, then methods by visibility",
57                )
58            })
59        })
60        .collect()
61}
62
63fn member_rank(member: &ast::AssocItem) -> u8 {
64    match member {
65        ast::AssocItem::Const(_) | ast::AssocItem::TypeAlias(_) => 0,
66        ast::AssocItem::Fn(function) if is_constructor(function) => 1,
67        ast::AssocItem::Fn(function) => visibility_rank(function),
68        ast::AssocItem::MacroCall(_) => MACRO_CALL_RANK,
69    }
70}
71
72fn is_constructor(function: &ast::Fn) -> bool {
73    if function
74        .param_list()
75        .is_some_and(|params| params.self_param().is_some())
76    {
77        return false;
78    }
79
80    let Some(return_type) = function.ret_type().and_then(|ret| ret.ty()) else {
81        return false;
82    };
83    let normalized: String = return_type
84        .syntax()
85        .text()
86        .to_string()
87        .chars()
88        .filter(|ch| !ch.is_whitespace())
89        .collect();
90
91    normalized == "Self"
92        || normalized.starts_with("Result<Self,")
93        || normalized.starts_with("Option<Self>")
94}
95
96fn visibility_rank(function: &ast::Fn) -> u8 {
97    match function.visibility() {
98        None => PRIVATE_METHOD_RANK,
99        Some(visibility) if visibility.syntax().text().to_string().trim() == "pub" => {
100            PUBLIC_METHOD_RANK
101        }
102        Some(_) => RESTRICTED_METHOD_RANK,
103    }
104}
105
106// #t(fn: rust_clone_in_loop) syntax-editor sorting requires detached copies of source items
107fn fix_impl_member_order(ctx: &AstCtx<'_>, _violations: &[Violation]) -> Option<String> {
108    let (editor, root) = SyntaxEditor::with_ast_node(ctx.root);
109    let mut changed = false;
110
111    for item_impl in root.syntax().descendants().filter_map(ast::Impl::cast) {
112        if item_impl.trait_().is_some() {
113            continue;
114        }
115
116        let Some(list) = item_impl.assoc_item_list() else {
117            continue;
118        };
119        let members: Vec<ast::AssocItem> = list.assoc_items().collect();
120        let mut target = members.clone();
121
122        target.sort_by_key(member_rank);
123
124        if members == target {
125            continue;
126        }
127
128        for (old, new) in members.iter().zip(&target) {
129            editor.replace(old.syntax().clone(), new.syntax().clone_subtree());
130        }
131
132        changed = true;
133    }
134
135    changed.then(|| editor.finish().new_root().to_string())
136}
137
138crate::tidy_ast_test!(check_impl_member_order, {
139    crate::example_tests!(EXAMPLES, check_impl_member_order);
140    crate::fix_tests!(ast_tree, check_impl_member_order, fix_impl_member_order);
141});