rulewright 0.1.2

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

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

const ADJACENT_STATEMENTS: usize = 2;

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "Vec::new then push",
        code: "fn f() { let mut v = Vec::new(); v.push(1); v.push(2); }",
        pass: false,
    },
    Example {
        label: "vec! macro is fine",
        code: "fn f() { let v = vec![1, 2]; }",
        pass: true,
    },
    Example {
        label: "Vec::new with_capacity is fine",
        code: "fn f() { let mut v = Vec::with_capacity(10); v.push(1); }",
        pass: true,
    },
    Example {
        label: "Vec::new then push in test",
        code: "#[cfg(test)]\nmod tests {\n    fn t() { let mut v = Vec::new(); v.push(1); }\n}",
        pass: true,
    },
    Example {
        label: "Vec::new with logic between",
        code: "fn f() { let mut v = Vec::new(); let x = 1; if x > 0 { v.push(x); } }",
        pass: true,
    },
    Example {
        label: "annotated Vec binding is outside the simple-pattern rule",
        code: "fn f() { let mut v: Vec<u32> = Vec::new(); v.push(1); }",
        pass: true,
    },
];

crate::ast_rule!(
    vec_init_then_push,
    "Flag `Vec::new()` immediately followed by `.push()` calls (use `vec![]` or `with_capacity`).",
    "Vec::new() followed by push() calls can be replaced with vec![...] or Vec::with_capacity for clarity and performance.",
);

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

    for statements in ctx.nodes::<ast::StmtList>() {
        let entries: Vec<ast::Stmt> = statements.statements().collect();

        for pair in entries.windows(ADJACENT_STATEMENTS) {
            let ast::Stmt::LetStmt(local) = &pair[0] else {
                continue;
            };

            if ctx.is_in_test(local) || !local.initializer().is_some_and(|expr| is_vec_new(&expr)) {
                continue;
            }

            let Some(name) = let_mut_name(local) else {
                continue;
            };

            if is_push_on(&pair[1], &name) {
                violations.push(ctx.violation(
                    local,
                    "Vec::new() immediately followed by .push() — use vec![...] or Vec::with_capacity()",
                ));
            }
        }
    }

    violations
}

fn is_vec_new(expr: &ast::Expr) -> bool {
    let ast::Expr::CallExpr(call) = expr else {
        return false;
    };
    let Some(ast::Expr::PathExpr(function)) = call.expr() else {
        return false;
    };
    let Some(path) = function.path() else {
        return false;
    };
    let last = path
        .segment()
        .and_then(|segment| segment.name_ref())
        .map(|name| name.text().to_string());
    let qualifier = path.qualifier().and_then(|qualifier| qualifier.segment());
    let owner = qualifier
        .and_then(|segment| segment.name_ref())
        .map(|name| name.text().to_string());

    owner.as_deref() == Some("Vec")
        && last.as_deref() == Some("new")
        && call
            .arg_list()
            .is_none_or(|arguments| arguments.args().next().is_none())
}

fn let_mut_name(local: &ast::LetStmt) -> Option<String> {
    if local.ty().is_some() {
        return None;
    }

    let ast::Pat::IdentPat(pattern) = local.pat()? else {
        return None;
    };

    pattern.mut_token()?;

    pattern.name().map(|name| name.text().to_string())
}

fn is_push_on(statement: &ast::Stmt, name: &str) -> bool {
    let ast::Stmt::ExprStmt(statement) = statement else {
        return false;
    };
    let Some(ast::Expr::MethodCallExpr(call)) = statement.expr() else {
        return false;
    };

    if call.name_ref().is_none_or(|method| method.text() != "push") {
        return false;
    }

    let Some(ast::Expr::PathExpr(receiver)) = call.receiver() else {
        return false;
    };

    let name_ref = receiver
        .path()
        .and_then(|path| path.segment())
        .and_then(|segment| segment.name_ref());

    name_ref.is_some_and(|receiver| receiver.text() == name)
}

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