Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
param_order_consistency.rs

1use 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
109// #t(fn: rust_alloc_in_loop) function names are retained as owned cross-function comparison keys
110fn 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, &params))
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});