1use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct FfiModule {
11 pub name: String,
13 pub lib_path: String,
15 pub binding_type: &'static str,
17 pub source_lang: &'static str,
19 pub target_lang: &'static str,
21}
22
23#[derive(Debug, Clone)]
25pub struct CrossRef {
26 pub source_file: String,
28 pub source_lang: &'static str,
30 pub target_module: String,
32 pub target_lang: &'static str,
34 pub ref_type: &'static str,
36 pub line: usize,
38}
39
40pub trait FfiBinding: Send + Sync {
45 fn name(&self) -> &'static str;
47
48 fn source_lang(&self) -> &'static str;
50
51 fn target_lang(&self) -> &'static str;
53
54 fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String>;
57
58 fn consumer_extensions(&self) -> &[&'static str];
60
61 fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool;
64}
65
66pub struct PyO3Binding;
72
73impl FfiBinding for PyO3Binding {
74 fn name(&self) -> &'static str {
75 "pyo3"
76 }
77
78 fn source_lang(&self) -> &'static str {
79 "rust"
80 }
81
82 fn target_lang(&self) -> &'static str {
83 "python"
84 }
85
86 fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
87 if path.file_name()? != "Cargo.toml" {
88 return None;
89 }
90 if !content.contains("pyo3") && !content.contains("PyO3") {
91 return None;
92 }
93 extract_cargo_crate_name(content)
94 }
95
96 fn consumer_extensions(&self) -> &[&'static str] {
97 &["py"]
98 }
99
100 fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool {
101 let module_name = known_module.replace('-', "_");
103 import_module == module_name
104 || import_module.starts_with(&format!("{}.", module_name))
105 || import_name == module_name
106 }
107}
108
109pub struct WasmBindgenBinding;
111
112impl FfiBinding for WasmBindgenBinding {
113 fn name(&self) -> &'static str {
114 "wasm-bindgen"
115 }
116
117 fn source_lang(&self) -> &'static str {
118 "rust"
119 }
120
121 fn target_lang(&self) -> &'static str {
122 "javascript"
123 }
124
125 fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
126 if path.file_name()? != "Cargo.toml" {
127 return None;
128 }
129 if !content.contains("wasm-bindgen") {
130 return None;
131 }
132 extract_cargo_crate_name(content)
133 }
134
135 fn consumer_extensions(&self) -> &[&'static str] {
136 &["js", "ts", "tsx", "mjs"]
137 }
138
139 fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool {
140 let module_name = known_module.replace('-', "_");
141 import_module == module_name
142 || import_module.starts_with(&format!("{}.", module_name))
143 || import_name == module_name
144 || import_module.contains(&format!("/{}", module_name))
145 }
146}
147
148pub struct NapiRsBinding;
150
151impl FfiBinding for NapiRsBinding {
152 fn name(&self) -> &'static str {
153 "napi-rs"
154 }
155
156 fn source_lang(&self) -> &'static str {
157 "rust"
158 }
159
160 fn target_lang(&self) -> &'static str {
161 "javascript"
162 }
163
164 fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
165 if path.file_name()? != "Cargo.toml" {
166 return None;
167 }
168 if !content.contains("napi") {
169 return None;
170 }
171 extract_cargo_crate_name(content)
172 }
173
174 fn consumer_extensions(&self) -> &[&'static str] {
175 &["js", "ts", "tsx", "mjs"]
176 }
177
178 fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool {
179 let module_name = known_module.replace('-', "_");
180 import_module == module_name
181 || import_module.starts_with(&format!("{}.", module_name))
182 || import_name == module_name
183 }
184}
185
186pub struct CdylibBinding;
188
189impl FfiBinding for CdylibBinding {
190 fn name(&self) -> &'static str {
191 "cdylib"
192 }
193
194 fn source_lang(&self) -> &'static str {
195 "rust"
196 }
197
198 fn target_lang(&self) -> &'static str {
199 "c"
200 }
201
202 fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
203 if path.file_name()? != "Cargo.toml" {
204 return None;
205 }
206 if !content.contains("cdylib") {
208 return None;
209 }
210 if content.contains("pyo3") || content.contains("wasm-bindgen") || content.contains("napi")
211 {
212 return None;
213 }
214 extract_cargo_crate_name(content)
215 }
216
217 fn consumer_extensions(&self) -> &[&'static str] {
218 &["c", "cpp", "h", "hpp", "py"] }
220
221 fn matches_import(
222 &self,
223 _import_module: &str,
224 _import_name: &str,
225 _known_module: &str,
226 ) -> bool {
227 false
229 }
230}
231
232pub struct CtypesBinding;
234
235impl FfiBinding for CtypesBinding {
236 fn name(&self) -> &'static str {
237 "ctypes"
238 }
239
240 fn source_lang(&self) -> &'static str {
241 "python"
242 }
243
244 fn target_lang(&self) -> &'static str {
245 "c"
246 }
247
248 fn detect_in_build_file(&self, _path: &Path, _content: &str) -> Option<String> {
249 None
251 }
252
253 fn consumer_extensions(&self) -> &[&'static str] {
254 &["py"]
255 }
256
257 fn matches_import(&self, import_module: &str, import_name: &str, _known_module: &str) -> bool {
258 import_module == "ctypes" || import_name == "ctypes" || import_name == "CDLL"
259 }
260}
261
262pub struct CffiBinding;
264
265impl FfiBinding for CffiBinding {
266 fn name(&self) -> &'static str {
267 "cffi"
268 }
269
270 fn source_lang(&self) -> &'static str {
271 "python"
272 }
273
274 fn target_lang(&self) -> &'static str {
275 "c"
276 }
277
278 fn detect_in_build_file(&self, _path: &Path, _content: &str) -> Option<String> {
279 None
280 }
281
282 fn consumer_extensions(&self) -> &[&'static str] {
283 &["py"]
284 }
285
286 fn matches_import(&self, import_module: &str, import_name: &str, _known_module: &str) -> bool {
287 import_module == "cffi" || import_name == "cffi" || import_name == "FFI"
288 }
289}
290
291pub struct FfiDetector {
297 bindings: Vec<Box<dyn FfiBinding>>,
298}
299
300impl Default for FfiDetector {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306impl FfiDetector {
307 pub fn new() -> Self {
309 Self {
310 bindings: vec![
311 Box::new(PyO3Binding),
312 Box::new(WasmBindgenBinding),
313 Box::new(NapiRsBinding),
314 Box::new(CdylibBinding),
315 Box::new(CtypesBinding),
316 Box::new(CffiBinding),
317 ],
318 }
319 }
320
321 pub fn add_binding(&mut self, binding: Box<dyn FfiBinding>) {
323 self.bindings.push(binding);
324 }
325
326 pub fn bindings(&self) -> &[Box<dyn FfiBinding>] {
328 &self.bindings
329 }
330
331 pub fn detect_modules(&self, path: &Path, content: &str) -> Vec<FfiModule> {
333 let mut modules = Vec::new();
334 let parent = path.parent().unwrap_or(Path::new(""));
335 let lib_path = parent.join("src").join("lib.rs");
336
337 for binding in &self.bindings {
338 if let Some(name) = binding.detect_in_build_file(path, content) {
339 modules.push(FfiModule {
340 name,
341 lib_path: lib_path.to_string_lossy().to_string(),
342 binding_type: binding.name(),
343 source_lang: binding.source_lang(),
344 target_lang: binding.target_lang(),
345 });
346 }
347 }
348
349 modules
350 }
351
352 pub fn match_import<'a>(
354 &self,
355 import_module: &str,
356 import_name: &str,
357 known_modules: &'a [FfiModule],
358 ) -> Option<(&'a FfiModule, &'static str)> {
359 for module in known_modules {
360 for binding in &self.bindings {
361 if binding.name() == module.binding_type
362 && binding.matches_import(import_module, import_name, &module.name)
363 {
364 return Some((module, binding.name()));
365 }
366 }
367 }
368 None
369 }
370
371 pub fn is_consumer_extension(&self, ext: &str) -> bool {
373 for binding in &self.bindings {
374 if binding.consumer_extensions().contains(&ext) {
375 return true;
376 }
377 }
378 false
379 }
380}
381
382fn extract_cargo_crate_name(content: &str) -> Option<String> {
388 let mut in_package = false;
389 for line in content.lines() {
390 let trimmed = line.trim();
391 if trimmed == "[package]" {
392 in_package = true;
393 continue;
394 }
395 if trimmed.starts_with('[') {
396 in_package = false;
397 continue;
398 }
399 if in_package && trimmed.starts_with("name") {
400 if let Some(eq_pos) = trimmed.find('=') {
401 let value = trimmed[eq_pos + 1..].trim();
402 let value = value.trim_matches('"').trim_matches('\'');
403 return Some(value.to_string());
404 }
405 }
406 }
407 None
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_pyo3_detection() {
416 let binding = PyO3Binding;
417 let content = r#"
418[package]
419name = "my-lib"
420version = "0.1.0"
421
422[dependencies]
423pyo3 = "0.20"
424"#;
425 let result = binding.detect_in_build_file(Path::new("Cargo.toml"), content);
426 assert_eq!(result, Some("my-lib".to_string()));
427 }
428
429 #[test]
430 fn test_pyo3_import_matching() {
431 let binding = PyO3Binding;
432 assert!(binding.matches_import("my_lib", "", "my-lib"));
433 assert!(binding.matches_import("my_lib.submodule", "", "my-lib"));
434 assert!(!binding.matches_import("other_lib", "", "my-lib"));
435 }
436
437 #[test]
438 fn test_detector_registry() {
439 let detector = FfiDetector::new();
440 assert!(detector.bindings().len() >= 6);
441
442 let content = r#"
443[package]
444name = "wasm-app"
445
446[dependencies]
447wasm-bindgen = "0.2"
448"#;
449 let modules = detector.detect_modules(Path::new("Cargo.toml"), content);
450 assert_eq!(modules.len(), 1);
451 assert_eq!(modules[0].name, "wasm-app");
452 assert_eq!(modules[0].binding_type, "wasm-bindgen");
453 }
454}