Skip to main content

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

1use ra_ap_syntax::{AstNode, Edition, SourceFile, ast, syntax_editor::SyntaxEditor};
2
3use crate::{AstCtx, Example, Violation};
4
5const ADJACENT_PAIR_LEN: usize = 2;
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example {
10        label: "sorted derives",
11        code: "#[derive(Clone, Debug, thiserror::Error)]\nstruct Item;",
12        pass: true,
13    },
14    Example {
15        label: "unsorted derives",
16        code: "#[derive(Debug, Clone)]\nstruct Item;",
17        pass: false,
18    },
19    Example {
20        label: "qualified paths use full text",
21        code: "#[derive(thiserror::Error, Clone, Debug)]\nenum Failure {}",
22        pass: false,
23    },
24    Example {
25        label: "non-derive attribute",
26        code: "#[cfg(test)]\nstruct Item;",
27        pass: true,
28    },
29];
30
31crate::ast_tree_rule!(
32    derive_order,
33    "Require traits inside derive attributes to be sorted alphabetically.",
34    "Stable derive ordering keeps attribute diffs deterministic and makes duplicated traits easy to spot.",
35    Low,
36    fix_derive_order,
37);
38
39fn check_derive_order(ctx: &AstCtx<'_>) -> Vec<Violation> {
40    ctx.nodes::<ast::Attr>()
41        .filter_map(|attr| {
42            let entries = derive_entries(&attr)?;
43
44            (!is_sorted(&entries)).then(|| {
45                ctx.violation(
46                    &attr,
47                    "derive traits are not alphabetically sorted (case-sensitive)",
48                )
49            })
50        })
51        .collect()
52}
53
54fn derive_entries(attr: &ast::Attr) -> Option<Vec<String>> {
55    let (name, tokens) = attr.as_simple_call()?;
56
57    if name != "derive" {
58        return None;
59    }
60
61    let text = tokens.syntax().text().to_string();
62    let body = text.strip_prefix('(')?.strip_suffix(')')?;
63
64    Some(
65        body.split(',')
66            .map(str::trim)
67            .filter(|entry| !entry.is_empty())
68            .map(str::to_owned)
69            .collect(),
70    )
71}
72
73fn is_sorted(entries: &[String]) -> bool {
74    entries
75        .windows(ADJACENT_PAIR_LEN)
76        .all(|pair| pair[0] <= pair[1])
77}
78
79// #t(fn: rust_alloc_in_loop) each changed derive needs a freshly formatted replacement attribute
80// #t(fn: rust_clone_in_loop) syntax-editor replacements must be detached nodes
81fn fix_derive_order(ctx: &AstCtx<'_>, _violations: &[Violation]) -> Option<String> {
82    let (editor, root) = SyntaxEditor::with_ast_node(ctx.root);
83    let mut changed = false;
84
85    for attr in root.syntax().descendants().filter_map(ast::Attr::cast) {
86        let Some(mut entries) = derive_entries(&attr) else {
87            continue;
88        };
89
90        if is_sorted(&entries) {
91            continue;
92        }
93
94        entries.sort();
95        let replacement = parse_attr(&format!("#[derive({})]", entries.join(", ")))?;
96
97        editor.replace(attr.syntax().clone(), replacement.syntax().clone());
98        changed = true;
99    }
100
101    changed.then(|| editor.finish().new_root().to_string())
102}
103
104fn parse_attr(source: &str) -> Option<ast::Attr> {
105    let fixture = format!("{source}\nstruct Fixture;");
106    let parse = SourceFile::parse(&fixture, Edition::Edition2024);
107
108    parse
109        .errors()
110        .is_empty()
111        .then(|| parse.tree())?
112        .syntax()
113        .descendants()
114        .find_map(ast::Attr::cast)
115}
116
117crate::tidy_ast_test!(check_derive_order, {
118    crate::example_tests!(EXAMPLES, check_derive_order);
119    crate::fix_tests!(ast_tree, check_derive_order, fix_derive_order);
120});