rulewright 0.1.2

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

use crate::{AstCtx, Example, Fix, Violation, infra::parse};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "redundant field name",
        code: "struct S { x: i32 }\nfn f(x: i32) -> S { S { x: x } }",
        pass: false,
    },
    Example {
        label: "shorthand field init",
        code: "struct S { x: i32 }\nfn f(x: i32) -> S { S { x } }",
        pass: true,
    },
    Example {
        label: "different name and value",
        code: "struct S { x: i32 }\nfn f(y: i32) -> S { S { x: y } }",
        pass: true,
    },
    Example {
        label: "redundant in test",
        code: "#[cfg(test)]\nmod tests {\n    struct S { x: i32 }\n    fn t(x: i32) -> S { S { x: x } }\n}",
        pass: true,
    },
];

crate::ast_rule!(
    redundant_field_names,
    "Flag `Foo { x: x }` — use shorthand `Foo { x }` instead.",
    "Rust supports field init shorthand (Foo { x } instead of Foo { x: x }). The long form is needless noise.",
    Low,
    fix_redundant_field_names,
);

fn check_redundant_field_names(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::RecordExprField>()
        .filter(|field| !ctx.is_in_test(field) && field.colon_token().is_some())
        .filter_map(|field| {
            let name = field.name_ref()?;
            let ast::Expr::PathExpr(path) = field.expr()? else {
                return None;
            };

            (path.syntax().text().to_string() == name.text()).then(|| {
                let name_text = name.text();

                ctx.violation(
                    &name,
                    format!(
                        "redundant field initializer `{name_text}: {name_text}` — use shorthand `{name_text}`"
                    ),
                )
            })
        })
        .collect()
}

fn fix_redundant_field_names(ctx: &AstCtx<'_>, v: &Violation) -> Option<Fix> {
    let line = ctx.file.line(v.line)?;
    let name = parse::redundant_field_name(&v.message)?;
    let redundant = format!("{name}: {name}");

    line.contains(&redundant)
        .then(|| Fix::replace_line(v.line, line.replacen(&redundant, name, 1)))
}

crate::rulewright_ast_test!(check_redundant_field_names, {
    crate::example_tests!(EXAMPLES, check_redundant_field_names);
    crate::fix_tests!(
        EXAMPLES,
        ast,
        check_redundant_field_names,
        fix_redundant_field_names
    );
});