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 && !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}