wowlab_tidy/languages/rust/rules/style/
attr_order.rs1use ra_ap_syntax::{
2 AstNode, AstToken, SyntaxElement,
3 ast::{self},
4 syntax_editor::SyntaxEditor,
5};
6
7use crate::{AstCtx, Example, Violation};
8
9const OTHER_ATTRIBUTE_RANK: u8 = 2;
10
11#[rustfmt::skip]
12const EXAMPLES: &[Example] = &[
13 Example {
14 label: "docs derive then other attributes",
15 code: "/// Item docs.\n#[derive(Debug)]\n#[cfg(test)]\nstruct Item;",
16 pass: true,
17 },
18 Example {
19 label: "derive follows other attribute",
20 code: "#[cfg(test)]\n#[derive(Debug)]\nstruct Item;",
21 pass: false,
22 },
23 Example {
24 label: "docs must be first",
25 code: "#[derive(Debug)]\n/// Item docs.\nstruct Item;",
26 pass: false,
27 },
28 Example {
29 label: "stable order within category",
30 code: "#[cfg(unix)]\n#[allow(dead_code)]\nstruct Item;",
31 pass: true,
32 },
33];
34
35crate::ast_tree_rule!(
36 attr_order,
37 "Require item attributes to be ordered as docs, derives, then other attributes.",
38 "Consistent attribute categories keep API documentation and generated traits prominent.",
39 Low,
40 fix_attr_order,
41);
42
43fn check_attr_order(ctx: &AstCtx<'_>) -> Vec<Violation> {
44 ctx.nodes::<ast::Item>()
45 .filter_map(|item| {
46 let attributes = attribute_elements(&item);
47 let ranks: Vec<u8> = attributes.iter().map(|(rank, _)| *rank).collect();
48
49 (!ranks.is_sorted()).then(|| {
50 ctx.violation(
51 &item,
52 "attributes must be ordered as documentation, derives, then other attributes",
53 )
54 })
55 })
56 .collect()
57}
58
59fn attribute_elements(item: &ast::Item) -> Vec<(u8, SyntaxElement)> {
60 item.syntax()
61 .children_with_tokens()
62 .filter_map(|element| match &element {
63 SyntaxElement::Token(token)
64 if ast::Comment::cast(token.clone()).is_some_and(|comment| comment.is_doc()) =>
65 {
66 Some((0, element))
67 }
68 SyntaxElement::Node(node) => ast::Attr::cast(node.clone()).map(|attr| {
69 let rank = if attr
70 .as_simple_call()
71 .is_some_and(|(name, _)| name == "derive")
72 {
73 1
74 } else {
75 OTHER_ATTRIBUTE_RANK
76 };
77
78 (rank, element)
79 }),
80 SyntaxElement::Token(_) => None,
81 })
82 .collect()
83}
84
85fn fix_attr_order(ctx: &AstCtx<'_>, _violations: &[Violation]) -> Option<String> {
87 let (editor, root) = SyntaxEditor::with_ast_node(ctx.root);
88 let mut changed = false;
89
90 for item in root.syntax().descendants().filter_map(ast::Item::cast) {
91 let attributes = attribute_elements(&item);
92 let mut target = attributes.clone();
93
94 target.sort_by_key(|(rank, _)| *rank);
95
96 if attributes == target {
97 continue;
98 }
99
100 for ((_, old), (_, new)) in attributes.iter().zip(&target) {
101 editor.replace(old.clone(), new.clone());
102 }
103
104 changed = true;
105 }
106
107 changed.then(|| editor.finish().new_root().to_string())
108}
109
110crate::tidy_ast_test!(check_attr_order, {
111 crate::example_tests!(EXAMPLES, check_attr_order);
112 crate::fix_tests!(ast_tree, check_attr_order, fix_attr_order);
113});