rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
use ra_ap_syntax::{
    AstNode,
    ast::{self, ArithOp, BinaryOp, CmpOp, LogicOp},
};

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "x == x",
        code: "fn f(x: i32) { if x == x {} }",
        pass: false,
    },
    Example {
        label: "x == y",
        code: "fn f(x: i32, y: i32) { if x == y {} }",
        pass: true,
    },
    Example {
        label: "a - a",
        code: "fn f(a: i32) { let _z = a - a; }",
        pass: false,
    },
    Example {
        label: "a + b",
        code: "fn f(a: i32, b: i32) { let _z = a + b; }",
        pass: true,
    },
    Example {
        label: "b && b",
        code: "fn f(b: bool) { if b && b {} }",
        pass: false,
    },
    Example {
        label: "dup in test module",
        code: "#[cfg(test)]\nmod tests {\n    fn t(x: i32) { if x == x {} }\n}",
        pass: true,
    },
];

crate::ast_rule!(
    dup_expressions,
    "Flag identical sub-expressions like `x == x`, `a - a`, `b && b`.",
    "Identical operands on both sides of an operator (x == x, a - a) are almost always copy-paste bugs.",
    High,
);

fn check_dup_expressions(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::BinExpr>()
        .filter(|expr| !ctx.is_in_test(expr))
        .filter_map(|expr| {
            let op = expr.op_kind()?;

            if !is_suspicious_op(op) {
                return None;
            }

            let (left, right) = expr.sub_exprs();
            let (left, right) = (left?, right?);

            (tokens_to_string(&left) == tokens_to_string(&right)).then(|| {
                let message = format!("identical sub-expressions on both sides of `{op}`");

                ctx.violation(&expr, message)
            })
        })
        .collect()
}

fn tokens_to_string(expr: &ast::Expr) -> String {
    expr.syntax()
        .descendants_with_tokens()
        .filter_map(ra_ap_syntax::NodeOrToken::into_token)
        .filter(|token| !token.kind().is_trivia())
        .fold(String::new(), |mut source, token| {
            source.push_str(token.text());

            source
        })
}

fn is_suspicious_op(op: BinaryOp) -> bool {
    matches!(
        op,
        BinaryOp::CmpOp(CmpOp::Eq { .. })
            | BinaryOp::LogicOp(LogicOp::And | LogicOp::Or)
            | BinaryOp::ArithOp(
                ArithOp::Sub
                    | ArithOp::Div
                    | ArithOp::Rem
                    | ArithOp::BitXor
                    | ArithOp::BitAnd
                    | ArithOp::BitOr
            )
    )
}

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