wowlab_tidy/languages/rust/rules/style/
impl_member_order.rs1use 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
106fn 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});