wowlab_tidy/languages/rust/rules/hygiene/
param_order_consistency.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasName},
4};
5
6use super::super::support::is_inside_trait;
7use crate::{AstCtx, Example, Violation};
8
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11 Example {
12 label: "shared pair order flips",
13 code: "fn create(tenant: u32, user: u32) {}\nfn delete(user: u32, tenant: u32) {}",
14 pass: false,
15 },
16 Example {
17 label: "flip with interleaved params",
18 code: "fn f(user: u32, tenant: u32, extra: bool) {}\nfn g(flag: bool, tenant: u32, user: u32) {}",
19 pass: false,
20 },
21 Example {
22 label: "flip across impl fns",
23 code: "struct S;\nimpl S {\n fn f(&self, user: u32, tenant: u32) {}\n fn g(&self, tenant: u32, user: u32) {}\n}",
24 pass: false,
25 },
26 Example {
27 label: "consistent order",
28 code: "fn create(tenant: u32, user: u32) {}\nfn delete(tenant: u32, user: u32) {}",
29 pass: true,
30 },
31 Example {
32 label: "only one shared pair",
33 code: "fn f(a: u32, b: String) {}\nfn g(b: String, c: u64) {}",
34 pass: true,
35 },
36 Example {
37 label: "same names different types",
38 code: "fn f(id: u32, name: String) {}\nfn g(name: u64, id: String) {}",
39 pass: true,
40 },
41 Example {
42 label: "single-param fns",
43 code: "fn f(x: u32) {}\nfn g(x: u32) {}",
44 pass: true,
45 },
46 Example {
47 label: "flip in test module",
48 code: "#[cfg(test)]\nmod tests {\n fn f(user: u32, tenant: u32) {}\n fn g(tenant: u32, user: u32) {}\n}",
49 pass: true,
50 },
51];
52
53crate::ast_rule!(
54 param_order_consistency,
55 "Flag fns whose shared parameters appear in a different order than an earlier fn in the file.",
56 "The same conceptual parameters appearing in flipping orders across sibling functions invites transposed-argument bugs and raises call-site friction.",
57 Low,
58);
59
60const MIN_SHARED_PAIRS: usize = 2;
61
62type ParamPair = (String, String);
63
64struct FnParams {
65 name: String,
66 params: Vec<ParamPair>,
67}
68
69fn param_pairs(function: &ast::Fn) -> Vec<ParamPair> {
70 function
71 .param_list()
72 .into_iter()
73 .flat_map(|parameters| parameters.params())
74 .filter_map(|parameter| {
75 let ast::Pat::IdentPat(pattern) = parameter.pat()? else {
76 return None;
77 };
78 let ty: String = parameter
79 .ty()?
80 .syntax()
81 .text()
82 .to_string()
83 .split_whitespace()
84 .collect();
85
86 Some((pattern.name()?.text().to_string(), ty))
87 })
88 .collect()
89}
90
91fn order_conflicts(earlier: &[ParamPair], later: &[ParamPair]) -> bool {
92 let shared: Vec<&ParamPair> = earlier.iter().filter(|p| later.contains(p)).collect();
93
94 if shared.len() < MIN_SHARED_PAIRS {
95 return false;
96 }
97
98 let later_positions: Vec<usize> = shared
99 .iter()
100 .filter_map(|p| later.iter().position(|q| &q == p))
101 .collect();
102
103 later_positions
104 .iter()
105 .zip(later_positions.iter().skip(1))
106 .any(|(a, b)| a > b)
107}
108
109fn check_param_order_consistency(ctx: &AstCtx<'_>) -> Vec<Violation> {
111 let mut seen = Vec::new();
112 let mut violations = Vec::new();
113
114 for function in ctx
115 .nodes::<ast::Fn>()
116 .filter(|function| !ctx.is_in_test(function) && !is_inside_trait(function))
117 {
118 let params = param_pairs(&function);
119
120 if params.len() < MIN_SHARED_PAIRS {
121 continue;
122 }
123
124 let Some(name) = function.name() else {
125 continue;
126 };
127
128 if let Some(earlier) = seen
129 .iter()
130 .find(|earlier: &&FnParams| order_conflicts(&earlier.params, ¶ms))
131 {
132 violations
133 .push(ctx.violation(&name, conflict_message(name.text().as_str(), &earlier.name)));
134 }
135
136 seen.push(FnParams {
137 name: name.text().to_string(),
138 params,
139 });
140 }
141
142 violations
143}
144
145fn conflict_message(later: &str, earlier: &str) -> String {
146 format!(
147 "fn `{later}` orders shared parameters differently than fn `{earlier}` — keep parameter order consistent"
148 )
149}
150
151crate::tidy_ast_test!(check_param_order_consistency, {
152 crate::example_tests!(EXAMPLES, check_param_order_consistency);
153});