Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
subtractive_feature_cfg.rs

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasAttrs, HasVisibility},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "not-feature on pub fn",
12        code: "#[cfg(not(feature = \"std\"))]\npub fn fallback() {}",
13        pass: false,
14    },
15    Example {
16        label: "not-feature on pub struct",
17        code: "#[cfg(not(feature = \"alloc\"))]\npub struct Fallback;",
18        pass: false,
19    },
20    Example {
21        label: "not-feature on pub impl fn",
22        code: "struct S;\nimpl S {\n    #[cfg(not(feature = \"std\"))]\n    pub fn fallback(&self) {}\n}",
23        pass: false,
24    },
25    Example {
26        label: "positive feature gate",
27        code: "#[cfg(feature = \"std\")]\npub fn f() {}",
28        pass: true,
29    },
30    Example {
31        label: "not-feature on private fn",
32        code: "#[cfg(not(feature = \"std\"))]\nfn fallback() {}",
33        pass: true,
34    },
35    Example {
36        label: "not-feature on pub(crate) fn",
37        code: "#[cfg(not(feature = \"std\"))]\npub(crate) fn fallback() {}",
38        pass: true,
39    },
40    Example {
41        label: "cfg not test is not a feature",
42        code: "#[cfg(not(target_arch = \"wasm32\"))]\npub fn f() {}",
43        pass: true,
44    },
45];
46
47crate::ast_rule!(
48    subtractive_feature_cfg,
49    "Flag `#[cfg(not(feature = \"...\"))]` on `pub` items — features must be additive.",
50    "A feature that removes public surface when enabled breaks feature unification: any dependent enabling it silently changes the API for everyone else.",
51    Medium,
52);
53
54fn is_not_feature_cfg(attr: &ast::Attr) -> bool {
55    attr.simple_name().is_some_and(|name| name == "cfg")
56        && attr
57            .syntax()
58            .text()
59            .to_string()
60            .chars()
61            .filter(|character| !character.is_whitespace())
62            .collect::<String>()
63            .contains("not(feature")
64}
65
66fn is_fully_public(visibility: Option<ast::Visibility>) -> bool {
67    visibility.is_some_and(|visibility| visibility.syntax().text().to_string().trim() == "pub")
68}
69
70fn check_subtractive_feature_cfg(ctx: &AstCtx<'_>) -> Vec<Violation> {
71    let public_items = ctx
72        .nodes::<ast::Item>()
73        .filter(|item| !matches!(item, ast::Item::Fn(_)))
74        .filter(|item| !ctx.is_in_test(item));
75    let item_attrs = public_items
76        .filter(|item| {
77            item.syntax()
78                .children()
79                .find_map(ast::Visibility::cast)
80                .is_some_and(|visibility| is_fully_public(Some(visibility)))
81        })
82        .flat_map(|item| item.attrs())
83        .filter(is_not_feature_cfg);
84    let public_functions = ctx
85        .nodes::<ast::Fn>()
86        .filter(|function| !ctx.is_in_test(function))
87        .filter(|function| is_fully_public(function.visibility()));
88    let function_attrs = public_functions
89        .flat_map(|function| function.attrs())
90        .filter(is_not_feature_cfg);
91
92    item_attrs
93        .chain(function_attrs)
94        .map(|attr| {
95            ctx.violation(
96                &attr,
97                "#[cfg(not(feature = ...))] on a pub item — enabling the feature removes public surface; features must be additive",
98            )
99        })
100        .collect()
101}
102
103crate::tidy_ast_test!(check_subtractive_feature_cfg, {
104    crate::example_tests!(EXAMPLES, check_subtractive_feature_cfg);
105});