rulewright 0.1.1

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "magic number in function",
        code: "fn f() { let x = 42; }",
        pass: false,
    },
    Example {
        label: "zero allowed by default",
        code: "fn f() { let x = 0; }",
        pass: true,
    },
    Example {
        label: "one allowed by default",
        code: "fn f() { let x = 1; }",
        pass: true,
    },
    Example {
        label: "power of two is magic",
        code: "fn f() { let x = 256; }",
        pass: false,
    },
    Example {
        label: "small int is magic",
        code: "fn f() { let x = 2; }",
        pass: false,
    },
    Example {
        label: "round number is magic",
        code: "fn f() { let x = 100; }",
        pass: false,
    },
    Example {
        label: "const passes",
        code: "const N: i32 = 42;",
        pass: true,
    },
    Example {
        label: "static passes",
        code: "static N: i32 = 42;",
        pass: true,
    },
    Example {
        label: "enum discriminant passes",
        code: "enum E { A = 42 }",
        pass: true,
    },
    Example {
        label: "float magic number",
        code: "fn f() { let x = 3.14; }",
        pass: false,
    },
    Example {
        label: "float zero allowed by default",
        code: "fn f() { let x = 0.0; }",
        pass: true,
    },
    Example {
        label: "magic number in test module",
        code: "#[cfg(test)]\nmod tests {\n    fn f() { let x = 42; }\n}",
        pass: true,
    },
    Example {
        label: "negative magic number",
        code: "fn f() { let x = -42; }",
        pass: false,
    },
    Example {
        label: "negative one allowed by default",
        code: "fn f() { let x = -1; }",
        pass: true,
    },
    Example {
        label: "underscored literal is magic",
        code: "fn f() { let x = 1_000; }",
        pass: false,
    },
    Example {
        label: "hex literal is magic",
        code: "fn f() { let x = 0xff; }",
        pass: false,
    },
    Example {
        label: "integer match patterns are not expressions",
        code: "fn f(x: i32) { match x { 2 | 42 => {}, _ => {} } }",
        pass: true,
    },
    Example {
        label: "negative integer match pattern is not an expression",
        code: "fn f(x: i32) { match x { -42 => {}, _ => {} } }",
        pass: true,
    },
    Example {
        label: "literal in match arm body remains magic",
        code: "fn f(x: i32) -> i32 { match x { 0 => 42, _ => 1 } }",
        pass: false,
    },
];

crate::ast_rule!(
    magic_numbers,
    "Flag numeric literals outside the configured allowlist for review.",
    "Unnamed numeric literals can obscure intent. Use a named constant when its name explains the value; otherwise tune the allowlist or scope to match the project's policy.",
    Low,
    default = false,
    params {
        allowed: [String] = ["0", "1", "0.0", "1.0"]
    },
);

fn check_magic_numbers(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let allowed = ctx
        .file
        .config
        .get_str_array("rust_magic_numbers", &PARAMS[0]);

    ctx.nodes::<ast::Literal>()
        .filter(|literal| {
            !ctx.is_in_test(literal)
                && !is_pattern_literal(literal)
                && !is_in_const_context(literal)
        })
        .filter_map(|literal| match literal.kind() {
            LiteralKind::IntNumber(number) => {
                let digits = number.value().ok()?.to_string();

                if is_negated(&literal) {
                    (!allowed.iter().any(|allowed| allowed == &digits)).then(|| {
                        let prefix = literal
                            .syntax()
                            .parent()
                            .and_then(ast::PrefixExpr::cast)
                            .expect("negated literal has a prefix expression");

                        ctx.violation(&prefix, review_message(&format!("-{digits}")))
                    })
                } else {
                    (!allowed.iter().any(|allowed| allowed == &digits))
                        .then(|| ctx.violation(&literal, review_message(&digits)))
                }
            }
            LiteralKind::FloatNumber(number) => {
                let digits = number.value_string();

                (!allowed.iter().any(|allowed| allowed == &digits))
                    .then(|| ctx.violation(&literal, review_message(&digits)))
            }
            _ => None,
        })
        .collect()
}

fn review_message(number: &str) -> String {
    format!(
        "numeric literal `{number}` is outside the configured allowlist — name it only when the name adds meaning; otherwise tune or justify the rule"
    )
}

fn is_pattern_literal(literal: &ast::Literal) -> bool {
    literal
        .syntax()
        .parent()
        .is_some_and(|parent| ast::LiteralPat::can_cast(parent.kind()))
}

fn is_negated(literal: &ast::Literal) -> bool {
    literal
        .syntax()
        .parent()
        .and_then(ast::PrefixExpr::cast)
        .is_some_and(|prefix| prefix.op_kind() == Some(UnaryOp::Neg))
}

fn is_in_const_context(literal: &ast::Literal) -> bool {
    literal.syntax().ancestors().any(|ancestor| {
        ast::Static::can_cast(ancestor.kind())
            || ast::Enum::can_cast(ancestor.kind())
            || ast::Const::cast(ancestor).is_some_and(|item| !is_trait_associated_const(&item))
    })
}

fn is_trait_associated_const(item: &ast::Const) -> bool {
    item.syntax()
        .parent()
        .and_then(ast::AssocItemList::cast)
        .and_then(|list| list.syntax().parent())
        .and_then(ast::Trait::cast)
        .is_some()
}

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