wowlab_tidy/languages/rust/rules/complexity/
closure_dense_method_chain.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4 AstNode,
5 ast::{self, HasArgList},
6};
7
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12 Example {
13 label: "twenty uniform fluent calls",
14 code: "fn f(w: Writer) { let _ = w.push_bind(1).push_bind(2).push_bind(3).push_bind(4).push_bind(5).push_bind(6).push_bind(7).push_bind(8).push_bind(9).push_bind(10).push_bind(11).push_bind(12).push_bind(13).push_bind(14).push_bind(15).push_bind(16).push_bind(17).push_bind(18).push_bind(19).push_bind(20); }",
15 pass: true,
16 },
17 Example {
18 label: "long closure-free heterogeneous builder",
19 code: "fn f(builder: Builder) { let _ = builder.apply_cooldown(1).cost(2).optional(true).damage(3).school(4).target(5).finish(); }",
20 pass: true,
21 },
22 Example {
23 label: "one inline closure",
24 code: "fn f(xs: &[i32]) { let _ = xs.iter().copied().map(|x| x + 1).collect::<Vec<_>>(); }",
25 pass: true,
26 },
27 Example {
28 label: "two inline closures",
29 code: "fn f(xs: &[i32]) { let _ = xs.iter().filter(|x| **x > 0).map(|x| x + 1).collect::<Vec<_>>(); }",
30 pass: true,
31 },
32 Example {
33 label: "original rune selection chain",
34 code: "fn f(slots: &[Slot]) { let _ = slots.iter().enumerate().filter_map(|(index, slot)| ready(slot).then_some((index, slot))).min_by(|left, right| compare(left, right)).map(first).or_else(|| depleted(slots)); }",
35 pass: false,
36 },
37 Example {
38 label: "closure nested in a closure body is not an extra chain closure",
39 code: "fn f(xs: Xs) { let _ = xs.map(|| ys.iter().filter(|y| ready(y))).inspect(noop).collect(); }",
40 pass: true,
41 },
42 Example {
43 label: "closure in a nested method-call argument belongs to that nested chain",
44 code: "fn f(xs: Xs) { let _ = xs.consume(factory.items().filter(|x| ready(x))).map(|x| value(x)); }",
45 pass: true,
46 },
47 Example {
48 label: "closure in an unrelated base call is not counted",
49 code: "fn f() { let _ = make(|| 1).iter().filter(|x| ready(x)).map(|x| value(x)); }",
50 pass: true,
51 },
52];
53
54crate::ast_rule!(
55 closure_dense_method_chain,
56 "Flag method-call chains containing at least the configured number of inline closure arguments.",
57 "Closure-dense fluent chains hide several branching decisions in one expression. Name an intermediate result or extract a helper.",
58 Medium,
59 params { threshold: i64 = 3 },
60);
61
62fn check_closure_dense_method_chain(ctx: &AstCtx<'_>) -> Vec<Violation> {
63 let threshold = ctx
64 .file
65 .config
66 .get_usize("rust_closure_dense_method_chain", &PARAMS[0]);
67
68 let outermost_calls = ctx
69 .nodes::<ast::MethodCallExpr>()
70 .filter(|call| !ctx.is_in_test(call))
71 .filter(|call| !is_receiver_of_method_call(call));
72
73 outermost_calls
74 .filter_map(|call| {
75 let closure_count = method_chain_inline_closures(&call);
76
77 (closure_count >= threshold).then(|| {
78 ctx.violation(
79 &call,
80 format!(
81 "method-call chain contains {closure_count} inline closure arguments (threshold {threshold})"
82 ),
83 )
84 })
85 })
86 .collect()
87}
88
89fn method_chain_inline_closures(call: &ast::MethodCallExpr) -> usize {
90 let mut closure_count = inline_closure_arguments(call);
91 let mut receiver = call.receiver();
92
93 while let Some(ast::Expr::MethodCallExpr(inner)) = receiver {
94 closure_count += inline_closure_arguments(&inner);
95 receiver = inner.receiver();
96 }
97
98 closure_count
99}
100
101fn inline_closure_arguments(call: &ast::MethodCallExpr) -> usize {
102 let Some(arguments) = call.arg_list() else {
103 return 0;
104 };
105 let mut closure_count = 0;
106
107 for argument in arguments.args() {
108 for node in argument.syntax().descendants() {
109 let Some(closure) = ast::ClosureExpr::cast(node) else {
110 continue;
111 };
112
113 if closure_belongs_to_argument(&closure, call) {
114 closure_count += 1;
115 }
116 }
117 }
118
119 closure_count
120}
121
122fn closure_belongs_to_argument(closure: &ast::ClosureExpr, call: &ast::MethodCallExpr) -> bool {
123 for ancestor in closure.syntax().ancestors().skip(1) {
124 if &ancestor == call.syntax() {
125 return true;
126 }
127
128 if ast::ClosureExpr::can_cast(ancestor.kind())
129 || ast::MethodCallExpr::can_cast(ancestor.kind())
130 {
131 return false;
132 }
133 }
134
135 false
136}
137
138fn is_receiver_of_method_call(call: &ast::MethodCallExpr) -> bool {
139 let Some(parent) = call.syntax().parent().and_then(ast::MethodCallExpr::cast) else {
140 return false;
141 };
142
143 parent
144 .receiver()
145 .is_some_and(|receiver| receiver.syntax() == call.syntax())
146}
147
148crate::tidy_ast_test!(check_closure_dense_method_chain, {
149 crate::example_tests!(EXAMPLES, check_closure_dense_method_chain);
150
151 #[gtest]
152 fn reports_only_the_outermost_call() -> Result<()> {
153 let violations = run(
154 "fn f(slots: &[Slot]) { let _ = slots.iter().enumerate().filter_map(|(index, slot)| ready(slot).then_some((index, slot))).min_by(|left, right| compare(left, right)).map(first).or_else(|| depleted(slots)); }",
155 );
156
157 verify_that!(violations, len(eq(1)))?;
158 verify_true!(violations[0].message.contains("3 inline closure arguments"))
159 }
160});