wowlab_tidy/languages/rust/rules/hygiene/
closure_param_position.rs1use ra_ap_syntax::ast::{self, HasGenericParams, HasName, HasTypeBounds, TypeBoundKind};
2use wowlab_types::sim::FastSet;
3
4use super::super::support::{is_inside_trait, type_name};
5use crate::{AstCtx, Example, Violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "closure before value param",
11 code: "fn f(cb: impl Fn(), x: u32) {}",
12 pass: false,
13 },
14 Example {
15 label: "generic closure before value param",
16 code: "fn f<F: FnMut()>(cb: F, x: u32) {}",
17 pass: false,
18 },
19 Example {
20 label: "where-bounded closure before value param",
21 code: "fn f<F>(cb: F, x: u32) where F: Fn() -> u32 {}",
22 pass: false,
23 },
24 Example {
25 label: "two closure params",
26 code: "fn f(a: impl Fn(), b: impl FnOnce()) {}",
27 pass: false,
28 },
29 Example {
30 label: "closure last",
31 code: "fn f(x: u32, cb: impl Fn()) {}",
32 pass: true,
33 },
34 Example {
35 label: "where-bounded closure last",
36 code: "fn f<F>(x: u32, cb: F) where F: FnOnce() {}",
37 pass: true,
38 },
39 Example {
40 label: "fn pointer is not a closure",
41 code: "fn f(cb: fn(), x: u32) {}",
42 pass: true,
43 },
44 Example {
45 label: "no closure params",
46 code: "fn f(x: u32, y: u32) {}",
47 pass: true,
48 },
49 Example {
50 label: "closure first in test module",
51 code: "#[cfg(test)]\nmod tests {\n fn f(cb: impl Fn(), x: u32) {}\n}",
52 pass: true,
53 },
54];
55
56crate::ast_rule!(
57 closure_param_position,
58 "Flag closure parameters that are not last, and fns taking more than one closure.",
59 "Closures go last so multi-line closure arguments read naturally at call sites; more than one closure parameter makes calls unreadable and argument order ambiguous.",
60 Low,
61);
62
63#[expect(
64 clippy::needless_pass_by_value,
65 reason = "ra_ap_syntax bound iterators yield owned facade nodes and this predicate is used directly by Iterator::any"
66)]
67fn is_fn_bound(bound: ast::TypeBound) -> bool {
68 matches!(
69 bound.kind(),
70 Some(TypeBoundKind::PathType(_, path_type))
71 if path_type.path().and_then(|path| path.segment()).and_then(|segment| segment.name_ref()).is_some_and(|name| matches!(name.text().as_str(), "Fn" | "FnMut" | "FnOnce"))
72 )
73}
74
75fn closure_generics(function: &ast::Fn) -> FastSet<String> {
76 let mut names = FastSet::default();
77
78 if let Some(generics) = function.generic_param_list() {
79 names.extend(generics.generic_params().filter_map(|parameter| {
80 let ast::GenericParam::TypeParam(parameter) = parameter else {
81 return None;
82 };
83
84 parameter
85 .type_bound_list()?
86 .bounds()
87 .any(is_fn_bound)
88 .then(|| parameter.name().map(|name| name.text().to_string()))?
89 }));
90 }
91
92 if let Some(where_clause) = function.where_clause() {
93 names.extend(where_clause.predicates().filter_map(|predicate| {
94 predicate
95 .type_bound_list()?
96 .bounds()
97 .any(is_fn_bound)
98 .then(|| type_name(&predicate.ty()?))?
99 }));
100 }
101
102 names
103}
104
105fn is_closure_type(mut ty: ast::Type, generics: &FastSet<String>) -> bool {
106 while let ast::Type::RefType(reference) = ty {
107 let Some(inner) = reference.ty() else {
108 return false;
109 };
110
111 ty = inner;
112 }
113
114 match ty {
115 ast::Type::ImplTraitType(impl_trait) => impl_trait
116 .type_bound_list()
117 .is_some_and(|bounds| bounds.bounds().any(is_fn_bound)),
118 ast::Type::PathType(_) => type_name(&ty).is_some_and(|name| generics.contains(&name)),
119 _ => false,
120 }
121}
122
123fn check_closure_param_position(ctx: &AstCtx<'_>) -> Vec<Violation> {
124 let mut violations = Vec::new();
125
126 for function in ctx
127 .nodes::<ast::Fn>()
128 .filter(|function| !ctx.is_in_test(function) && !is_inside_trait(function))
129 {
130 let generics = closure_generics(&function);
131 let parameters = function
132 .param_list()
133 .into_iter()
134 .flat_map(|parameters| parameters.params());
135 let flags: Vec<bool> = parameters
136 .filter_map(|parameter| parameter.ty())
137 .map(|ty| is_closure_type(ty, &generics))
138 .collect();
139 let Some(name) = function.name() else {
140 continue;
141 };
142
143 if flags.iter().filter(|&&flag| flag).count() > 1 {
144 violations.push(ctx.violation(
145 &name,
146 format!("fn `{name}` takes more than one closure parameter — accept at most one"),
147 ));
148 }
149
150 if flags
151 .iter()
152 .zip(flags.iter().skip(1))
153 .any(|(current, next)| *current && !*next)
154 {
155 violations.push(ctx.violation(
156 &name,
157 format!(
158 "fn `{name}` has a closure parameter before a non-closure parameter — closures go last"
159 ),
160 ));
161 }
162 }
163
164 violations
165}
166
167crate::tidy_ast_test!(check_closure_param_position, {
168 crate::example_tests!(EXAMPLES, check_closure_param_position);
169});