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