rulewright 0.1.1

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "inherent impl first",
        code: "struct Item;\nimpl Item { fn new() -> Self { Self } }\nimpl Default for Item { fn default() -> Self { Self } }",
        pass: true,
    },
    Example {
        label: "trait impl first",
        code: "struct Item;\nimpl Default for Item { fn default() -> Self { Self } }\nimpl Item { fn new() -> Self { Self } }",
        pass: false,
    },
    Example {
        label: "different types",
        code: "struct One; struct Two;\nimpl Default for One { fn default() -> Self { Self } }\nimpl Two {}",
        pass: true,
    },
    Example {
        label: "trait impl only",
        code: "struct Item;\nimpl Default for Item { fn default() -> Self { Self } }",
        pass: true,
    },
];

crate::ast_rule!(
    inherent_before_trait_impl,
    "Require an inherent impl to precede trait impls for the same local type.",
    "Putting the type's own API first makes its primary behavior easier to discover.",
    Low,
);

// #rw(fn: rust_alloc_in_loop) normalized self-type keys are owned by the cross-impl index
fn check_inherent_before_trait_impl(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let mut first_inherent = HashMap::<String, usize>::default();
    let mut trait_impls = Vec::new();

    for item_impl in ctx.nodes::<ast::Impl>() {
        let Some(self_type) = item_impl.self_ty() else {
            continue;
        };
        let key: String = self_type
            .syntax()
            .text()
            .to_string()
            .chars()
            .filter(|ch| !ch.is_whitespace())
            .collect();
        let offset: usize = item_impl.syntax().text_range().start().into();

        if item_impl.trait_().is_none() {
            first_inherent.entry(key).or_insert(offset);
        } else {
            trait_impls.push((key, offset, item_impl));
        }
    }

    trait_impls
        .into_iter()
        .filter_map(|(key, offset, item_impl)| {
            first_inherent
                .get(&key)
                .is_some_and(|inherent_offset| offset < *inherent_offset)
                .then(|| {
                    ctx.violation(
                        &item_impl,
                        "trait impl appears before the inherent impl for this type",
                    )
                })
        })
        .collect()
}

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