wowlab_tidy/languages/rust/rules/hygiene/
subtractive_feature_cfg.rs1use 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});