rulewright 0.1.2

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "not-feature on pub fn",
        code: "#[cfg(not(feature = \"std\"))]\npub fn fallback() {}",
        pass: false,
    },
    Example {
        label: "not-feature on pub struct",
        code: "#[cfg(not(feature = \"alloc\"))]\npub struct Fallback;",
        pass: false,
    },
    Example {
        label: "not-feature on pub impl fn",
        code: "struct S;\nimpl S {\n    #[cfg(not(feature = \"std\"))]\n    pub fn fallback(&self) {}\n}",
        pass: false,
    },
    Example {
        label: "positive feature gate",
        code: "#[cfg(feature = \"std\")]\npub fn f() {}",
        pass: true,
    },
    Example {
        label: "not-feature on private fn",
        code: "#[cfg(not(feature = \"std\"))]\nfn fallback() {}",
        pass: true,
    },
    Example {
        label: "not-feature on pub(crate) fn",
        code: "#[cfg(not(feature = \"std\"))]\npub(crate) fn fallback() {}",
        pass: true,
    },
    Example {
        label: "cfg not test is not a feature",
        code: "#[cfg(not(target_arch = \"wasm32\"))]\npub fn f() {}",
        pass: true,
    },
];

crate::ast_rule!(
    subtractive_feature_cfg,
    "Flag `#[cfg(not(feature = \"...\"))]` on `pub` items — features must be additive.",
    "A feature that removes public surface when enabled breaks feature unification: any dependent enabling it silently changes the API for everyone else.",
    Medium,
);

fn is_not_feature_cfg(attr: &ast::Attr) -> bool {
    attr.simple_name().is_some_and(|name| name == "cfg")
        && attr
            .syntax()
            .text()
            .to_string()
            .chars()
            .filter(|character| !character.is_whitespace())
            .collect::<String>()
            .contains("not(feature")
}

fn is_fully_public(visibility: Option<ast::Visibility>) -> bool {
    visibility.is_some_and(|visibility| visibility.syntax().text().to_string().trim() == "pub")
}

fn check_subtractive_feature_cfg(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let public_items = ctx
        .nodes::<ast::Item>()
        .filter(|item| !matches!(item, ast::Item::Fn(_)))
        .filter(|item| !ctx.is_in_test(item));
    let item_attrs = public_items
        .filter(|item| {
            item.syntax()
                .children()
                .find_map(ast::Visibility::cast)
                .is_some_and(|visibility| is_fully_public(Some(visibility)))
        })
        .flat_map(|item| item.attrs())
        .filter(is_not_feature_cfg);
    let public_functions = ctx
        .nodes::<ast::Fn>()
        .filter(|function| !ctx.is_in_test(function))
        .filter(|function| is_fully_public(function.visibility()));
    let function_attrs = public_functions
        .flat_map(|function| function.attrs())
        .filter(is_not_feature_cfg);

    item_attrs
        .chain(function_attrs)
        .map(|attr| {
            ctx.violation(
                &attr,
                "#[cfg(not(feature = ...))] on a pub item — enabling the feature removes public surface; features must be additive",
            )
        })
        .collect()
}

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