wowlab_tidy/languages/rust/rules/safety/
ambient_syscall.rs1use 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
123const 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});