Skip to main content

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

1#[cfg(test)]
2use googletest::prelude::*;
3
4use crate::{Example, FileCtx, Violation, infra::scanner, violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8    Example {
9        label: "no_mangle export in non-ffi crate",
10        code: "#[no_mangle]\npub extern \"C\" fn engine_tick() {}",
11        pass: false,
12    },
13    Example {
14        label: "unsafe no_mangle export in non-ffi crate",
15        code: "#[unsafe(no_mangle)]\npub extern \"C\" fn engine_tick() {}",
16        pass: false,
17    },
18    Example {
19        label: "foreign block in non-sys crate",
20        code: "extern \"C\" {\n    fn native_call();\n}",
21        pass: false,
22    },
23    Example {
24        label: "plain rust fn",
25        code: "pub fn tick() {}",
26        pass: true,
27    },
28    Example {
29        label: "no_mangle without extern C fn",
30        code: "#[no_mangle]\npub static VERSION: u32 = 1;",
31        pass: true,
32    },
33    Example {
34        label: "extern C fn without no_mangle",
35        code: "pub extern \"C\" fn callback() {}",
36        pass: true,
37    },
38    Example {
39        label: "wasm_bindgen crate uses its own convention",
40        code: "use wasm_bindgen::prelude::JsValue;\n#[no_mangle]\npub extern \"C\" fn engine_tick() {}",
41        pass: true,
42    },
43];
44
45crate::line_rule!(
46    ffi_crate_naming,
47    "Require `-ffi` naming for crates exporting C symbols and `-sys` naming for crates linking foreign C items.",
48    "The `-ffi` (export) and `-sys` (import) suffixes make a crate's FFI role immediately recognizable across projects (M-FFI-NAMING).",
49    Low,
50);
51
52#[derive(Default)]
53struct FfiUsage {
54    no_mangle: bool,
55    extern_fn_line: Option<usize>,
56    foreign_block_line: Option<usize>,
57}
58
59fn scan_ffi_usage(lines: &[&str]) -> FfiUsage {
60    let mut usage = FfiUsage::default();
61
62    for (index, line) in lines.iter().enumerate() {
63        let code = scanner::code_only(line);
64
65        if code.contains("#[no_mangle]") || code.contains("#[unsafe(no_mangle)]") {
66            usage.no_mangle = true;
67        }
68
69        if !code.contains("extern") {
70            continue;
71        }
72
73        if usage.extern_fn_line.is_none() && line.contains("extern \"C\" fn") {
74            usage.extern_fn_line = Some(index + 1);
75        }
76
77        if usage.foreign_block_line.is_none() && line.contains("extern \"C\" {") {
78            usage.foreign_block_line = Some(index + 1);
79        }
80    }
81
82    usage
83}
84
85fn check_ffi_crate_naming(ctx: &FileCtx<'_>) -> Vec<Violation> {
86    let Some(name) = super::crate_name(ctx.rel) else {
87        return Vec::new();
88    };
89
90    if super::wasm_exempt(ctx.contents) {
91        return Vec::new();
92    }
93
94    let usage = scan_ffi_usage(ctx.lines);
95    let mut out = Vec::new();
96
97    if usage.no_mangle && !super::is_ffi_crate(name) {
98        if let Some(line) = usage.extern_fn_line {
99            out.push(violation(
100                ctx.rel,
101                line,
102                format!("crate `{name}` exports C symbols but is not named `*-ffi` — FFI export crates follow the `-ffi` naming convention"),
103            ));
104        }
105    }
106
107    if !super::is_sys_crate(name) {
108        if let Some(line) = usage.foreign_block_line {
109            out.push(violation(
110                ctx.rel,
111                line,
112                format!("crate `{name}` links foreign C items but is not named `*-sys` — FFI import crates follow the `-sys` naming convention"),
113            ));
114        }
115    }
116
117    out
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    const CORE_REL: &str = "crates/engine/src/lib.rs";
125
126    fn run_at(rel: &str, source: &str) -> Vec<Violation> {
127        crate::test_support::check_source_at(rel, source, check_ffi_crate_naming)
128    }
129
130    #[gtest]
131    fn examples() -> Result<()> {
132        for ex in EXAMPLES {
133            let violations = run_at(CORE_REL, ex.code);
134
135            verify_eq!(violations.is_empty(), ex.pass)?;
136        }
137
138        Ok(())
139    }
140
141    #[gtest]
142    fn export_from_ffi_crate_passes() -> Result<()> {
143        let source = "#[no_mangle]\npub extern \"C\" fn engine_tick() {}";
144
145        verify_true!(run_at("crates/engine-ffi/src/lib.rs", source).is_empty())?;
146        verify_true!(run_at("crates/engine_ffi/src/lib.rs", source).is_empty())?;
147
148        Ok(())
149    }
150
151    #[gtest]
152    fn foreign_block_in_sys_crate_passes() -> Result<()> {
153        let source = "extern \"C\" {\n    fn native_call();\n}";
154
155        verify_true!(run_at("crates/native-sys/src/lib.rs", source).is_empty())?;
156        verify_true!(run_at("crates/native_sys/src/lib.rs", source).is_empty())?;
157
158        Ok(())
159    }
160
161    #[gtest]
162    fn export_direction_flags_ffi_naming_only() -> Result<()> {
163        let violations = run_at(
164            "crates/engine-sys/src/lib.rs",
165            "#[no_mangle]\npub extern \"C\" fn f() {}",
166        );
167
168        verify_eq!(violations.len(), 1)?;
169
170        Ok(())
171    }
172
173    #[gtest]
174    fn files_outside_crates_are_skipped() -> Result<()> {
175        verify_true!(run_at("clean.rs", "extern \"C\" {\n    fn native_call();\n}").is_empty())?;
176
177        Ok(())
178    }
179
180    #[gtest]
181    fn patterns_inside_strings_do_not_count() -> Result<()> {
182        let source = "const SNIPPET: &str = \"#[no_mangle] pub extern \\\"C\\\" fn f() {}\";";
183
184        verify_true!(run_at(CORE_REL, source).is_empty())?;
185
186        Ok(())
187    }
188}