Skip to main content

wowlab_tidy/languages/rust/rules/interop/
ffi_in_core.rs

1#[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    // #t(fn: rust_duplicate_strings) This linter scenario intentionally repeats source fixtures used to verify independent paths.
187    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}