Skip to main content

wowlab_tidy/languages/rust/rules/correctness/
global_state.rs

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasName},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "static atomic",
12        code: "static COUNTER: AtomicUsize = AtomicUsize::new(0);",
13        pass: false,
14    },
15    Example {
16        label: "static once lock",
17        code: "static CACHE: OnceLock<String> = OnceLock::new();",
18        pass: false,
19    },
20    Example {
21        label: "nested interior mutability",
22        code: "static SLOTS: [Option<Mutex<u8>>; 2] = [None, None];",
23        pass: false,
24    },
25    Example {
26        label: "thread local",
27        code: "thread_local! {\n    static TLS: RefCell<u8> = RefCell::new(0);\n}",
28        pass: false,
29    },
30    Example {
31        label: "immutable str static",
32        code: "static NAME: &str = \"wowlab\";",
33        pass: true,
34    },
35    Example {
36        label: "immutable numeric static",
37        code: "static LIMIT: usize = 64;",
38        pass: true,
39    },
40    Example {
41        label: "local mutex",
42        code: "fn f() { let m = std::sync::Mutex::new(0); drop(m); }",
43        pass: true,
44    },
45    Example {
46        label: "static atomic in test module",
47        code: "#[cfg(test)]\nmod tests {\n    static COUNTER: AtomicUsize = AtomicUsize::new(0);\n}",
48        pass: true,
49    },
50];
51
52crate::ast_rule!(
53    global_state,
54    "Flag `static` items with interior mutability and all `thread_local!` state.",
55    "Mutable globals are secretly duplicated across linked crate versions and break test isolation; perf-only caches need a #t suppression with reason.",
56    Medium,
57);
58
59const INTERIOR_MUTABLE: &[&str] = &[
60    "Cell", "LazyCell", "LazyLock", "Mutex", "OnceCell", "OnceLock", "RefCell", "RwLock",
61];
62
63fn interior_mutable_name(ty: &ast::Type) -> Option<String> {
64    ty.syntax()
65        .descendants()
66        .filter_map(ast::NameRef::cast)
67        .map(|name| name.text().to_string())
68        .find(|name| name.starts_with("Atomic") || INTERIOR_MUTABLE.contains(&name.as_str()))
69}
70
71fn check_global_state(ctx: &AstCtx<'_>) -> Vec<Violation> {
72    let statics = ctx
73        .nodes::<ast::Static>()
74        .filter(|item| !ctx.is_in_test(item))
75        .filter_map(|item| {
76            let name = interior_mutable_name(&item.ty()?)?;
77            let ident = item.name()?;
78
79            Some(ctx.violation(
80                &ident,
81                format!(
82                    "static `{ident}` contains interior mutability (`{name}`) — pass state explicitly instead of a global"
83                ),
84            ))
85        });
86    let macro_calls = ctx
87        .nodes::<ast::MacroCall>()
88        .filter(|call| !ctx.is_in_test(call));
89    let thread_locals = macro_calls
90        .filter(|call| {
91            let name = call
92                .path()
93                .and_then(|path| path.segment())
94                .and_then(|segment| segment.name_ref());
95
96            name.is_some_and(|name| name.text() == "thread_local")
97        })
98        .map(|call| {
99            ctx.violation(
100                &call,
101                "thread_local! creates hidden global state — pass state explicitly instead",
102            )
103        });
104
105    statics.chain(thread_locals).collect()
106}
107
108crate::tidy_ast_test!(check_global_state, {
109    crate::example_tests!(EXAMPLES, check_global_state);
110});