rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
use ra_ap_syntax::ast::{self, HasName, TypeBoundKind};

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "impl Future wrapping one async block",
        code: "fn f() -> impl Future<Output = u8> { async { 1 } }",
        pass: false,
    },
    Example {
        label: "qualified Future with async move block",
        code: "fn f() -> impl std::future::Future<Output = ()> { async move { } }",
        pass: false,
    },
    Example {
        label: "async fn",
        code: "async fn f() -> u8 { 1 }",
        pass: true,
    },
    Example {
        label: "body does more than one async block",
        code: "fn f() -> impl Future<Output = u8> { let x = 1; async move { x } }",
        pass: true,
    },
    Example {
        label: "trait declaration",
        code: "trait T { fn f() -> impl Future<Output = ()>; }",
        pass: true,
    },
    Example {
        label: "trait default body",
        code: "trait T { fn f() -> impl Future<Output = ()> { async { } } }",
        pass: true,
    },
    Example {
        label: "trait impl block",
        code: "struct S;\nimpl T for S { fn f() -> impl Future<Output = ()> { async { } } }",
        pass: true,
    },
    Example {
        label: "inherent impl method",
        code: "struct S;\nimpl S { fn f() -> impl Future<Output = ()> { async { } } }",
        pass: false,
    },
    Example {
        label: "non-Future impl trait return",
        code: "fn f() -> impl Iterator<Item = u8> { std::iter::once(1) }",
        pass: true,
    },
    Example {
        label: "manual future in test module",
        code: "#[cfg(test)]\nmod tests {\n    fn f() -> impl Future<Output = ()> { async { } }\n}",
        pass: true,
    },
];

crate::ast_rule!(
    manual_async_fn,
    "Flag non-async functions that return `impl Future` by wrapping the whole body in one `async` block.",
    "`async fn` reads normally and avoids signature noise; explicit `impl Future` returns are only warranted in traits or for hot-path size control.",
    Low,
);

fn check_manual_async_fn(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let eligible_functions = ctx
        .nodes::<ast::Fn>()
        .filter(|function| {
            super::support::is_item_or_impl_fn(function) && !ctx.is_in_test(function)
        })
        .filter(|function| !super::support::is_inside_trait(function));
    let future_functions = eligible_functions
        .filter(|function| {
            super::support::enclosing_impl(function)
                .is_none_or(|item_impl| item_impl.trait_().is_none())
        })
        .filter(|function| function.async_token().is_none() && returns_impl_future(function));

    future_functions
        .filter(body_is_single_async_block)
        .filter_map(|function| {
            let name = function.name()?;

            Some(ctx.violation(
                &name,
                format!(
                    "fn `{name}` returns `impl Future` from a single `async` block — declare it `async fn` instead"
                ),
            ))
        })
        .collect()
}

fn returns_impl_future(function: &ast::Fn) -> bool {
    let Some(ast::Type::ImplTraitType(impl_trait)) = function.ret_type().and_then(|ret| ret.ty())
    else {
        return false;
    };

    impl_trait.type_bound_list().is_some_and(|bounds| {
        bounds.bounds().any(|bound| {
            matches!(
                bound.kind(),
                Some(TypeBoundKind::PathType(_, path_type))
                    if path_type.path().is_some_and(|path| super::support::path_last_is(path, "Future"))
            )
        })
    })
}

fn body_is_single_async_block(function: &ast::Fn) -> bool {
    let Some(statements) = function.body().and_then(|body| body.stmt_list()) else {
        return false;
    };
    let stmts: Vec<_> = statements.statements().collect();

    match (stmts.as_slice(), statements.tail_expr()) {
        ([], Some(ast::Expr::BlockExpr(block))) => block.async_token().is_some(),
        ([ast::Stmt::ExprStmt(statement)], None) => {
            matches!(statement.expr(), Some(ast::Expr::BlockExpr(block)) if block.async_token().is_some())
        }
        _ => false,
    }
}

crate::rulewright_ast_test!(check_manual_async_fn, {
    crate::example_tests!(EXAMPLES, check_manual_async_fn);
});