rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
#[cfg(test)]
use googletest::prelude::*;
use ra_ap_syntax::{
    AstNode,
    ast::{self, HasArgList},
};

use crate::{AstCtx, Example, Violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "twenty uniform fluent calls",
        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); }",
        pass: true,
    },
    Example {
        label: "long closure-free heterogeneous builder",
        code: "fn f(builder: Builder) { let _ = builder.name(1).cost(2).optional(true).retries(3).format(4).target(5).finish(); }",
        pass: true,
    },
    Example {
        label: "one inline closure",
        code: "fn f(xs: &[i32]) { let _ = xs.iter().copied().map(|x| x + 1).collect::<Vec<_>>(); }",
        pass: true,
    },
    Example {
        label: "two inline closures",
        code: "fn f(xs: &[i32]) { let _ = xs.iter().filter(|x| **x > 0).map(|x| x + 1).collect::<Vec<_>>(); }",
        pass: true,
    },
    Example {
        label: "dense selection chain",
        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)); }",
        pass: false,
    },
    Example {
        label: "closure nested in a closure body is not an extra chain closure",
        code: "fn f(xs: Xs) { let _ = xs.map(|| ys.iter().filter(|y| ready(y))).inspect(noop).collect(); }",
        pass: true,
    },
    Example {
        label: "closure in a nested method-call argument belongs to that nested chain",
        code: "fn f(xs: Xs) { let _ = xs.consume(factory.items().filter(|x| ready(x))).map(|x| value(x)); }",
        pass: true,
    },
    Example {
        label: "closure in an unrelated base call is not counted",
        code: "fn f() { let _ = make(|| 1).iter().filter(|x| ready(x)).map(|x| value(x)); }",
        pass: true,
    },
];

crate::ast_rule!(
    closure_dense_method_chain,
    "Flag method-call chains containing at least the configured number of inline closure arguments.",
    "Closure-dense fluent chains hide several branching decisions in one expression. Name an intermediate result or extract a helper.",
    Medium,
    params { threshold: i64 = 3 },
);

fn check_closure_dense_method_chain(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let threshold = ctx
        .file
        .config
        .get_usize("rust_closure_dense_method_chain", &PARAMS[0]);

    let outermost_calls = ctx
        .nodes::<ast::MethodCallExpr>()
        .filter(|call| !ctx.is_in_test(call))
        .filter(|call| !is_receiver_of_method_call(call));

    outermost_calls
        .filter_map(|call| {
            let closure_count = method_chain_inline_closures(&call);

            (closure_count >= threshold).then(|| {
                ctx.violation(
                    &call,
                    format!(
                        "method-call chain contains {closure_count} inline closure arguments (threshold {threshold})"
                    ),
                )
            })
        })
        .collect()
}

fn method_chain_inline_closures(call: &ast::MethodCallExpr) -> usize {
    let mut closure_count = inline_closure_arguments(call);
    let mut receiver = call.receiver();

    while let Some(ast::Expr::MethodCallExpr(inner)) = receiver {
        closure_count += inline_closure_arguments(&inner);
        receiver = inner.receiver();
    }

    closure_count
}

fn inline_closure_arguments(call: &ast::MethodCallExpr) -> usize {
    let Some(arguments) = call.arg_list() else {
        return 0;
    };
    let mut closure_count = 0;

    for argument in arguments.args() {
        for node in argument.syntax().descendants() {
            let Some(closure) = ast::ClosureExpr::cast(node) else {
                continue;
            };

            if closure_belongs_to_argument(&closure, call) {
                closure_count += 1;
            }
        }
    }

    closure_count
}

fn closure_belongs_to_argument(closure: &ast::ClosureExpr, call: &ast::MethodCallExpr) -> bool {
    for ancestor in closure.syntax().ancestors().skip(1) {
        if &ancestor == call.syntax() {
            return true;
        }

        if ast::ClosureExpr::can_cast(ancestor.kind())
            || ast::MethodCallExpr::can_cast(ancestor.kind())
        {
            return false;
        }
    }

    false
}

fn is_receiver_of_method_call(call: &ast::MethodCallExpr) -> bool {
    let Some(parent) = call.syntax().parent().and_then(ast::MethodCallExpr::cast) else {
        return false;
    };

    parent
        .receiver()
        .is_some_and(|receiver| receiver.syntax() == call.syntax())
}

crate::rulewright_ast_test!(check_closure_dense_method_chain, {
    crate::example_tests!(EXAMPLES, check_closure_dense_method_chain);

    #[gtest]
    fn reports_only_the_outermost_call() -> Result<()> {
        let violations = run(
            "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)); }",
        );

        verify_that!(violations, len(eq(1)))?;
        verify_true!(violations[0].message.contains("3 inline closure arguments"))
    }
});