Skip to main content

wowlab_tidy/languages/rust/rules/safety/
ambient_syscall.rs

1use ra_ap_syntax::ast::{self};
2
3use super::support::path_segments;
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8    Example {
9        label: "ambient fs read",
10        code: "fn load() { let _ = std::fs::read(\"cfg.toml\"); }",
11        pass: false,
12    },
13    Example {
14        label: "ambient fs write via import",
15        code: "use std::fs;\nfn save() { let _ = fs::write(\"out.bin\", b\"data\"); }",
16        pass: false,
17    },
18    Example {
19        label: "ambient File open",
20        code: "fn open() { let _ = std::fs::File::open(\"cfg.toml\"); }",
21        pass: false,
22    },
23    Example {
24        label: "ambient clock",
25        code: "fn stamp() { let _ = std::time::Instant::now(); }",
26        pass: false,
27    },
28    Example {
29        label: "ambient system time",
30        code: "use std::time::SystemTime;\nfn stamp() { let _ = SystemTime::now(); }",
31        pass: false,
32    },
33    Example {
34        label: "ambient env read",
35        code: "fn cfg() { let _ = std::env::var(\"MODE\"); }",
36        pass: false,
37    },
38    Example {
39        label: "ambient network connect",
40        code: "fn dial(addr: &str) { let _ = std::net::TcpStream::connect(addr); }",
41        pass: false,
42    },
43    Example {
44        label: "ambient entropy",
45        code: "fn roll() -> u32 { rand::random() }",
46        pass: false,
47    },
48    Example {
49        label: "ambient thread_rng import",
50        code: "use rand::thread_rng;\nfn roll() { let _ = thread_rng(); }",
51        pass: false,
52    },
53    Example {
54        label: "injected clock is fine",
55        code: "fn stamp(clock: &dyn Clock) { let _ = clock.now(); }",
56        pass: true,
57    },
58    Example {
59        label: "unrelated fs module",
60        code: "fn load() { let _ = custom::fs::read(1); }",
61        pass: true,
62    },
63    Example {
64        label: "syscall in test module",
65        code: "#[cfg(test)]\nmod tests {\n    fn t() { let _ = std::time::Instant::now(); }\n}",
66        pass: true,
67    },
68];
69
70crate::ast_rule!(
71    ambient_syscall,
72    "Flag ambient I/O, clock, env, and entropy calls in library code.",
73    "Syscalls called ambiently cannot be mocked, making edge cases untestable — inject them through an abstraction (M-MOCKABLE-SYSCALLS).",
74    Medium,
75);
76
77fn check_ambient_syscall(ctx: &AstCtx<'_>) -> Vec<Violation> {
78    ctx.nodes::<ast::CallExpr>()
79        .filter(|call| !ctx.is_in_test(call))
80        .filter_map(|call| {
81            let ast::Expr::PathExpr(path_expr) = call.expr()? else {
82                return None;
83            };
84            let path = path_expr.path()?;
85            let segments = path_segments(&path);
86
87            is_ambient_call(&segments).then(|| {
88                ctx.violation(
89                    &path_expr,
90                    format!(
91                        "ambient syscall `{}` — inject I/O, clocks, and entropy through \
92                         a mockable abstraction (M-MOCKABLE-SYSCALLS)",
93                        segments.join("::")
94                    ),
95                )
96            })
97        })
98        .collect()
99}
100
101fn is_ambient_call(segments: &[String]) -> bool {
102    match segments {
103        [] => false,
104        [only] => only == "thread_rng",
105        [.., a, b] => {
106            is_type_syscall(a, b)
107                || is_std_module_call(segments, a, b)
108                || (a == "rand" && (b == "thread_rng" || b == "random"))
109        }
110    }
111}
112
113fn is_type_syscall(ty: &str, method: &str) -> bool {
114    matches!(
115        (ty, method),
116        ("File", "open" | "create")
117            | ("TcpStream", "connect")
118            | ("TcpListener" | "UdpSocket", "bind")
119            | ("SystemTime" | "Instant", "now")
120    )
121}
122
123/// An unprefixed `module::fn` call is exactly two path segments.
124const MODULE_CALL_SEGMENTS: usize = 2;
125
126fn is_std_module_call(segments: &[String], module: &str, name: &str) -> bool {
127    let rooted = segments.len() == MODULE_CALL_SEGMENTS
128        || segments.first().is_some_and(|root| root == "std");
129
130    if !rooted {
131        return false;
132    }
133
134    match module {
135        "fs" => true,
136        "env" => matches!(name, "var" | "var_os" | "vars"),
137        _ => false,
138    }
139}
140
141crate::tidy_ast_test!(check_ambient_syscall, {
142    crate::example_tests!(EXAMPLES, check_ambient_syscall);
143});