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
98        && !super::is_ffi_crate(name)
99        && let Some(line) = usage.extern_fn_line
100    {
101        out.push(violation(
102                ctx.rel,
103                line,
104                format!("crate `{name}` exports C symbols but is not named `*-ffi` — FFI export crates follow the `-ffi` naming convention"),
105            ));
106    }
107
108    if !super::is_sys_crate(name)
109        && let Some(line) = usage.foreign_block_line
110    {
111        out.push(violation(
112                ctx.rel,
113                line,
114                format!("crate `{name}` links foreign C items but is not named `*-sys` — FFI import crates follow the `-sys` naming convention"),
115            ));
116    }
117
118    out
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    const CORE_REL: &str = "crates/engine/src/lib.rs";
126
127    fn run_at(rel: &str, source: &str) -> Vec<Violation> {
128        crate::test_support::check_source_at(rel, source, check_ffi_crate_naming)
129    }
130
131    #[gtest]
132    fn examples() -> Result<()> {
133        for ex in EXAMPLES {
134            let violations = run_at(CORE_REL, ex.code);
135
136            verify_eq!(violations.is_empty(), ex.pass)?;
137        }
138
139        Ok(())
140    }
141
142    #[gtest]
143    fn export_from_ffi_crate_passes() -> Result<()> {
144        let source = "#[no_mangle]\npub extern \"C\" fn engine_tick() {}";
145
146        verify_true!(run_at("crates/engine-ffi/src/lib.rs", source).is_empty())?;
147        verify_true!(run_at("crates/engine_ffi/src/lib.rs", source).is_empty())?;
148
149        Ok(())
150    }
151
152    #[gtest]
153    fn foreign_block_in_sys_crate_passes() -> Result<()> {
154        let source = "extern \"C\" {\n    fn native_call();\n}";
155
156        verify_true!(run_at("crates/native-sys/src/lib.rs", source).is_empty())?;
157        verify_true!(run_at("crates/native_sys/src/lib.rs", source).is_empty())?;
158
159        Ok(())
160    }
161
162    #[gtest]
163    fn export_direction_flags_ffi_naming_only() -> Result<()> {
164        let violations = run_at(
165            "crates/engine-sys/src/lib.rs",
166            "#[no_mangle]\npub extern \"C\" fn f() {}",
167        );
168
169        verify_eq!(violations.len(), 1)?;
170
171        Ok(())
172    }
173
174    #[gtest]
175    fn files_outside_crates_are_skipped() -> Result<()> {
176        verify_true!(run_at("clean.rs", "extern \"C\" {\n    fn native_call();\n}").is_empty())?;
177
178        Ok(())
179    }
180
181    #[gtest]
182    fn patterns_inside_strings_do_not_count() -> Result<()> {
183        let source = "const SNIPPET: &str = \"#[no_mangle] pub extern \\\"C\\\" fn f() {}\";";
184
185        verify_true!(run_at(CORE_REL, source).is_empty())?;
186
187        Ok(())
188    }
189}