wowlab_tidy/languages/rust/rules/interop/
ffi_crate_naming.rs1#[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}