Skip to main content

wowlab_tidy/languages/rust/rules/complexity/
closure_dense_method_chain.rs

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