wowlab_tidy/languages/rust/rules/interop/
ffi_in_core.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4 AstNode,
5 ast::{self, HasAttrs, HasName},
6};
7
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12 Example {
13 label: "no_mangle extern C fn in core crate",
14 code: "#[no_mangle]\npub extern \"C\" fn engine_tick() {}",
15 pass: false,
16 },
17 Example {
18 label: "unsafe no_mangle extern C fn in core crate",
19 code: "#[unsafe(no_mangle)]\npub extern \"C\" fn engine_tick() {}",
20 pass: false,
21 },
22 Example {
23 label: "repr(C) struct with raw-pointer field",
24 code: "#[repr(C)]\npub struct Msg {\n data: *mut u8,\n len: usize,\n}",
25 pass: false,
26 },
27 Example {
28 label: "repr(C) struct without pointers",
29 code: "#[repr(C)]\npub struct Point {\n x: f32,\n y: f32,\n}",
30 pass: true,
31 },
32 Example {
33 label: "raw pointer without repr(C)",
34 code: "pub struct Msg {\n data: *mut u8,\n}",
35 pass: true,
36 },
37 Example {
38 label: "extern C fn without no_mangle",
39 code: "pub extern \"C\" fn callback() {}",
40 pass: true,
41 },
42 Example {
43 label: "no_mangle without extern C abi",
44 code: "#[no_mangle]\npub fn plain() {}",
45 pass: true,
46 },
47 Example {
48 label: "wasm_bindgen files follow their own convention",
49 code: "use wasm_bindgen::prelude::JsValue;\n#[no_mangle]\npub extern \"C\" fn engine_tick() {}",
50 pass: true,
51 },
52 Example {
53 label: "export in test module",
54 code: "#[cfg(test)]\nmod tests {\n #[no_mangle]\n pub extern \"C\" fn engine_tick() {}\n}",
55 pass: true,
56 },
57];
58
59crate::ast_rule!(
60 ffi_in_core,
61 "Flag `#[no_mangle] extern \"C\"` exports and `#[repr(C)]` raw-pointer structs in non-FFI crates.",
62 "Business logic belongs in core crates as idiomatic safe Rust; interop concerns leaking into core force FFI ownership and data models onto everyone (M-FFI-TRANSLATES).",
63 Medium,
64);
65
66fn check_ffi_in_core(ctx: &AstCtx<'_>) -> Vec<Violation> {
67 let Some(name) = super::crate_name(ctx.file.rel) else {
68 return Vec::new();
69 };
70
71 if super::is_ffi_crate(name)
72 || super::is_sys_crate(name)
73 || name == "wasm"
74 || super::wasm_exempt(ctx.file.contents)
75 {
76 return Vec::new();
77 }
78
79 let ffi_functions = ctx
80 .nodes::<ast::Fn>()
81 .filter(|function| {
82 super::support::is_item_or_impl_fn(function) && !ctx.is_in_test(function)
83 })
84 .filter(|function| has_no_mangle(function) && super::is_extern_c(function.abi()));
85 let mut violations: Vec<_> = ffi_functions
86 .filter_map(|function| {
87 let name = function.name()?;
88
89 Some(ctx.violation(
90 &name,
91 format!(
92 "C export `{name}` in a core crate — move FFI glue into a dedicated `*-ffi` crate"
93 ),
94 ))
95 })
96 .collect();
97
98 violations.extend(
99 {
100 let ffi_structs = ctx
101 .nodes::<ast::Struct>()
102 .filter(|item| !ctx.is_in_test(item))
103 .filter(|item| has_repr_c(item) && has_raw_pointer_field(item));
104
105 ffi_structs
106 .filter_map(|item| {
107 let name = item.name()?;
108
109 Some(ctx.violation(
110 &name,
111 format!(
112 "`#[repr(C)]` struct `{name}` with raw-pointer fields in a core crate — FFI data models belong in a dedicated `*-ffi` crate"
113 ),
114 ))
115 })
116 },
117 );
118
119 violations
120}
121
122fn has_no_mangle(node: &impl HasAttrs) -> bool {
123 node.attrs().any(|attr| {
124 attr.simple_name().is_some_and(|name| name == "no_mangle")
125 || attr
126 .syntax()
127 .to_string()
128 .replace(' ', "")
129 .contains("unsafe(no_mangle)")
130 })
131}
132
133fn has_repr_c(node: &impl HasAttrs) -> bool {
134 node.attrs().any(|attr| {
135 attr.simple_name().is_some_and(|name| name == "repr")
136 && attr
137 .syntax()
138 .descendants_with_tokens()
139 .filter_map(ra_ap_syntax::NodeOrToken::into_token)
140 .any(|token| token.text() == "C")
141 })
142}
143
144fn has_raw_pointer_field(item: &ast::Struct) -> bool {
145 item.field_list().is_some_and(|fields| {
146 super::support::field_types(fields)
147 .into_iter()
148 .any(|ty| matches!(ty, ast::Type::PtrType(_)))
149 })
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 const CORE_REL: &str = "crates/engine/src/lib.rs";
157
158 fn run_at(rel: &str, source: &str) -> Vec<Violation> {
159 crate::test_support::check_source_ast_at(rel, source, check_ffi_in_core)
160 }
161
162 #[gtest]
163 fn examples() -> Result<()> {
164 for ex in EXAMPLES {
165 let violations = run_at(CORE_REL, ex.code);
166
167 verify_eq!(violations.is_empty(), ex.pass)?;
168 }
169
170 Ok(())
171 }
172
173 #[gtest]
174 fn ffi_sys_and_wasm_crates_are_exempt() -> Result<()> {
175 let export = "#[no_mangle]\npub extern \"C\" fn engine_tick() {}";
176
177 verify_true!(run_at("crates/engine-ffi/src/lib.rs", export).is_empty())?;
178 verify_true!(run_at("crates/engine_ffi/src/lib.rs", export).is_empty())?;
179 verify_true!(run_at("crates/native-sys/src/lib.rs", export).is_empty())?;
180 verify_true!(run_at("crates/wasm/src/lib.rs", export).is_empty())?;
181
182 Ok(())
183 }
184
185 #[gtest]
186 fn files_outside_crates_are_skipped() -> Result<()> {
188 verify_true!(run_at("clean.rs", "#[no_mangle]\npub extern \"C\" fn f() {}").is_empty())?;
189
190 Ok(())
191 }
192}