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