rulewright 0.1.1

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "Rc::new held across await",
        code: "async fn f() { let rc = Rc::new(1); g().await; }",
        pass: false,
    },
    Example {
        label: "RefCell::new held across await",
        code: "async fn f() { let cell = RefCell::new(1); g().await; }",
        pass: false,
    },
    Example {
        label: "Rc::clone held across await",
        code: "async fn f(other: &Rc<u8>) { let rc = Rc::clone(other); g().await; }",
        pass: false,
    },
    Example {
        label: "Rc in async block before await",
        code: "fn f() { let fut = async { let rc = Rc::new(1); g().await; }; }",
        pass: false,
    },
    Example {
        label: "await before the Rc binding",
        code: "async fn f() { g().await; let rc = Rc::new(1); }",
        pass: true,
    },
    Example {
        label: "Arc is Send",
        code: "async fn f() { let arc = Arc::new(1); g().await; }",
        pass: true,
    },
    Example {
        label: "Rc in sync fn",
        code: "fn f() { let rc = Rc::new(1); }",
        pass: true,
    },
    Example {
        label: "await only inside a nested async block",
        code: "async fn f() { let rc = Rc::new(1); let fut = async { g().await; }; }",
        pass: true,
    },
    Example {
        label: "Rc across await in test module",
        code: "#[cfg(test)]\nmod tests {\n    async fn f() { let rc = Rc::new(1); g().await; }\n}",
        pass: true,
    },
];

crate::ast_rule!(
    nonsend_across_await,
    "Flag `Rc`/`RefCell` bindings in async code when an `.await` occurs later in the same block.",
    "!Send values held across an await point make the entire future !Send, breaking Tokio and other work-stealing runtimes.",
    Medium,
);

fn check_nonsend_across_await(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let mut violations = Vec::new();

    for function in ctx.nodes::<ast::Fn>().filter(|function| {
        super::support::is_item_or_impl_fn(function)
            && !ctx.is_in_test(function)
            && function.async_token().is_some()
    }) {
        if let Some(block) = function.body() {
            scan_block(ctx, &block, &mut violations);
        }
    }

    for block in ctx
        .nodes::<ast::BlockExpr>()
        .filter(|block| !ctx.is_in_test(block) && block.async_token().is_some())
    {
        scan_block(ctx, &block, &mut violations);
    }

    violations
}

fn ctor_label(path: ast::Path) -> Option<&'static str> {
    let names = super::support::path_names(path);
    let mut rev = names.iter().rev();
    let method = rev.next()?;
    let ty = rev.next()?;

    if ty == "Rc" && method == "new" {
        return Some("Rc::new");
    }

    if ty == "Rc" && method == "clone" {
        return Some("Rc::clone");
    }

    if ty == "RefCell" && method == "new" {
        return Some("RefCell::new");
    }

    None
}

fn nonsend_init(stmt: &ast::Stmt) -> Option<(String, &'static str)> {
    let ast::Stmt::LetStmt(local) = stmt else {
        return None;
    };
    let ast::Expr::CallExpr(call) = local.initializer()? else {
        return None;
    };
    let ast::Expr::PathExpr(func) = call.expr()? else {
        return None;
    };
    let ctor = ctor_label(func.path()?)?;
    let name = match local.pat()? {
        ast::Pat::IdentPat(pat) => pat.name()?.text().to_string(),
        _ => "binding".to_string(),
    };

    Some((name, ctor))
}

fn stmt_awaits(stmt: &ast::Stmt) -> bool {
    node_awaits(stmt.syntax())
}

fn node_awaits(root: &SyntaxNode) -> bool {
    root.descendants()
        .filter_map(ast::AwaitExpr::cast)
        .any(|await_expr| {
            !await_expr
                .syntax()
                .ancestors()
                .skip(1)
                .take_while(|node| node != root)
                .any(|node| {
                    ast::ClosureExpr::cast(node.clone()).is_some()
                        || ast::Item::cast(node.clone()).is_some()
                        || ast::BlockExpr::cast(node)
                            .is_some_and(|block| block.async_token().is_some())
                })
        })
}

fn scan_block(ctx: &AstCtx<'_>, block: &ast::BlockExpr, violations: &mut Vec<Violation>) {
    let Some(list) = block.stmt_list() else {
        return;
    };
    let statements: Vec<_> = list.statements().collect();
    let mut awaits: Vec<bool> = statements.iter().map(stmt_awaits).collect();

    awaits.extend(
        list.tail_expr()
            .map(|expression| node_awaits(expression.syntax())),
    );
    let found: Vec<Violation> = statements
        .iter()
        .enumerate()
        .filter(|&(i, _)| awaits.iter().skip(i + 1).any(|&later| later))
        .filter_map(|(_, stmt)| {
            nonsend_init(stmt).map(|(name, ctor)| {
                ctx.violation(
                    stmt,
                    format!(
                        "`{name}` is created with `{ctor}` and held across a later `.await` — !Send values across await points make the whole future !Send"
                    ),
                )
            })
        })
        .collect();

    violations.extend(found);
}

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