Skip to main content

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

1use 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});