Skip to main content

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

1use 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
85// #t(fn: rust_clone_in_loop) syntax-editor sorting requires detached attribute nodes
86fn 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});