Skip to main content

infigraph_core/bridges/
mod.rs

1use std::path::Path;
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6use crate::model::{Bridge, BridgeKind};
7
8/// Aggregated result of a bridge scan.
9#[derive(Debug, Default, Serialize, Deserialize)]
10pub struct BridgeScanResult {
11    pub bridges: Vec<Bridge>,
12}
13
14impl BridgeScanResult {
15    pub fn by_kind(&self, kind: &BridgeKind) -> Vec<&Bridge> {
16        self.bridges.iter().filter(|b| &b.kind == kind).collect()
17    }
18
19    pub fn ffi_count(&self) -> usize {
20        self.bridges
21            .iter()
22            .filter(|b| b.kind == BridgeKind::Ffi)
23            .count()
24    }
25    pub fn jni_count(&self) -> usize {
26        self.bridges
27            .iter()
28            .filter(|b| b.kind == BridgeKind::Jni)
29            .count()
30    }
31    pub fn grpc_count(&self) -> usize {
32        self.bridges
33            .iter()
34            .filter(|b| b.kind == BridgeKind::Grpc)
35            .count()
36    }
37    pub fn pinvoke_count(&self) -> usize {
38        self.bridges
39            .iter()
40            .filter(|b| b.kind == BridgeKind::PInvoke)
41            .count()
42    }
43    pub fn com_count(&self) -> usize {
44        self.bridges
45            .iter()
46            .filter(|b| b.kind == BridgeKind::Com)
47            .count()
48    }
49}
50
51// ---------------------------------------------------------------------------
52// Pattern rules
53// ---------------------------------------------------------------------------
54
55struct BridgeRule {
56    kind: fn() -> BridgeKind,
57    /// File extensions this rule applies to (empty = all)
58    extensions: &'static [&'static str],
59    /// Substring that must appear on the line (case-sensitive)
60    pattern: &'static str,
61    /// Source language label
62    source_language: &'static str,
63    /// Target language (None = unknown)
64    target_language: Option<&'static str>,
65    /// How to extract the foreign symbol name from the matching line
66    extract: fn(&str) -> String,
67}
68
69fn extract_after_last_space(line: &str) -> String {
70    line.split_whitespace()
71        .last()
72        .unwrap_or("")
73        .trim_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != ':')
74        .to_string()
75}
76
77fn extract_quoted(line: &str) -> String {
78    // Grab first double-quoted string
79    if let Some(start) = line.find('"') {
80        if let Some(end) = line[start + 1..].find('"') {
81            return line[start + 1..start + 1 + end].to_string();
82        }
83    }
84    line.trim().to_string()
85}
86
87fn extract_vb6_lib(line: &str) -> String {
88    let lower = line.to_ascii_lowercase();
89    if let Some(idx) = lower.find(" lib ") {
90        let after = line[idx + 5..].trim_start();
91        return extract_quoted(after);
92    }
93    extract_after_last_space(line)
94}
95
96fn extract_paren_arg(line: &str) -> String {
97    // Grab first argument inside parentheses (before first comma or close paren)
98    if let Some(open) = line.find('(') {
99        let after = line[open + 1..].trim_start();
100        let end = after.find([',', ')']).unwrap_or(after.len());
101        return after[..end].trim().trim_matches('"').to_string();
102    }
103    line.trim().to_string()
104}
105
106static BRIDGE_RULES: &[BridgeRule] = &[
107    // ── Rust FFI ──────────────────────────────────────────────────────────────
108    BridgeRule {
109        kind: || BridgeKind::Ffi,
110        extensions: &["rs"],
111        pattern: "extern \"C\"",
112        source_language: "rust",
113        target_language: Some("c"),
114        extract: extract_after_last_space,
115    },
116    BridgeRule {
117        kind: || BridgeKind::Ffi,
118        extensions: &["rs"],
119        pattern: "#[no_mangle]",
120        source_language: "rust",
121        target_language: Some("c"),
122        extract: |_| "exported_symbol".to_string(),
123    },
124    BridgeRule {
125        kind: || BridgeKind::Ffi,
126        extensions: &["rs"],
127        pattern: "extern \"system\"",
128        source_language: "rust",
129        target_language: Some("c"),
130        extract: extract_after_last_space,
131    },
132    // ── C/C++ FFI ─────────────────────────────────────────────────────────────
133    BridgeRule {
134        kind: || BridgeKind::Ffi,
135        extensions: &["c", "h", "cpp", "hpp", "cc"],
136        pattern: "JNIEXPORT",
137        source_language: "c",
138        target_language: Some("java"),
139        extract: extract_after_last_space,
140    },
141    BridgeRule {
142        kind: || BridgeKind::Jni,
143        extensions: &["c", "h", "cpp", "hpp", "cc"],
144        pattern: "JNIEnv",
145        source_language: "c",
146        target_language: Some("java"),
147        extract: extract_after_last_space,
148    },
149    // ── Java JNI ──────────────────────────────────────────────────────────────
150    BridgeRule {
151        kind: || BridgeKind::Jni,
152        extensions: &["java"],
153        pattern: "native ",
154        source_language: "java",
155        target_language: Some("c"),
156        extract: extract_after_last_space,
157    },
158    BridgeRule {
159        kind: || BridgeKind::Jni,
160        extensions: &["java"],
161        pattern: "System.loadLibrary(",
162        source_language: "java",
163        target_language: Some("c"),
164        extract: extract_paren_arg,
165    },
166    // ── Go cgo ───────────────────────────────────────────────────────────────
167    BridgeRule {
168        kind: || BridgeKind::Cgo,
169        extensions: &["go"],
170        pattern: "import \"C\"",
171        source_language: "go",
172        target_language: Some("c"),
173        extract: |_| "C".to_string(),
174    },
175    BridgeRule {
176        kind: || BridgeKind::Cgo,
177        extensions: &["go"],
178        pattern: "C.",
179        source_language: "go",
180        target_language: Some("c"),
181        extract: |line| {
182            // Extract C.FunctionName
183            if let Some(idx) = line.find("C.") {
184                let after = &line[idx + 2..];
185                let end = after
186                    .find(|c: char| !c.is_alphanumeric() && c != '_')
187                    .unwrap_or(after.len());
188                return format!("C.{}", &after[..end]);
189            }
190            "C.unknown".to_string()
191        },
192    },
193    // ── .NET P/Invoke ─────────────────────────────────────────────────────────
194    BridgeRule {
195        kind: || BridgeKind::PInvoke,
196        extensions: &["cs"],
197        pattern: "[DllImport(",
198        source_language: "csharp",
199        target_language: Some("c"),
200        extract: extract_paren_arg,
201    },
202    BridgeRule {
203        kind: || BridgeKind::PInvoke,
204        extensions: &["cs"],
205        pattern: "DllImport(",
206        source_language: "csharp",
207        target_language: Some("c"),
208        extract: extract_paren_arg,
209    },
210    // ── Python ctypes / cffi ──────────────────────────────────────────────────
211    BridgeRule {
212        kind: || BridgeKind::Ctypes,
213        extensions: &["py"],
214        pattern: "ctypes.CDLL(",
215        source_language: "python",
216        target_language: Some("c"),
217        extract: extract_paren_arg,
218    },
219    BridgeRule {
220        kind: || BridgeKind::Ctypes,
221        extensions: &["py"],
222        pattern: "ctypes.cdll.LoadLibrary(",
223        source_language: "python",
224        target_language: Some("c"),
225        extract: extract_paren_arg,
226    },
227    BridgeRule {
228        kind: || BridgeKind::Ctypes,
229        extensions: &["py"],
230        pattern: "ffi.cdef(",
231        source_language: "python",
232        target_language: Some("c"),
233        extract: |_| "cffi_binding".to_string(),
234    },
235    // ── gRPC ─────────────────────────────────────────────────────────────────
236    BridgeRule {
237        kind: || BridgeKind::Grpc,
238        extensions: &["proto"],
239        pattern: "service ",
240        source_language: "protobuf",
241        target_language: None,
242        extract: |line| {
243            line.trim()
244                .strip_prefix("service ")
245                .map(|s| s.split_whitespace().next().unwrap_or("").to_string())
246                .unwrap_or_default()
247        },
248    },
249    BridgeRule {
250        kind: || BridgeKind::Grpc,
251        extensions: &["py", "js", "ts", "go", "java", "rb", "cs", "rs"],
252        pattern: "_pb2_grpc",
253        source_language: "python",
254        target_language: None,
255        extract: extract_after_last_space,
256    },
257    BridgeRule {
258        kind: || BridgeKind::Grpc,
259        extensions: &["go"],
260        pattern: "grpc.Dial(",
261        source_language: "go",
262        target_language: None,
263        extract: extract_paren_arg,
264    },
265    BridgeRule {
266        kind: || BridgeKind::Grpc,
267        extensions: &["java"],
268        pattern: "ManagedChannelBuilder",
269        source_language: "java",
270        target_language: None,
271        extract: extract_after_last_space,
272    },
273    BridgeRule {
274        kind: || BridgeKind::Grpc,
275        extensions: &["rs"],
276        pattern: "tonic::transport::Channel",
277        source_language: "rust",
278        target_language: None,
279        extract: extract_after_last_space,
280    },
281    // ── WASM ─────────────────────────────────────────────────────────────────
282    BridgeRule {
283        kind: || BridgeKind::Wasm,
284        extensions: &["rs"],
285        pattern: "#[wasm_bindgen]",
286        source_language: "rust",
287        target_language: Some("javascript"),
288        extract: |_| "wasm_export".to_string(),
289    },
290    BridgeRule {
291        kind: || BridgeKind::Wasm,
292        extensions: &["rs"],
293        pattern: "wasm_bindgen::JsValue",
294        source_language: "rust",
295        target_language: Some("javascript"),
296        extract: extract_after_last_space,
297    },
298    BridgeRule {
299        kind: || BridgeKind::Wasm,
300        extensions: &["js", "ts"],
301        pattern: "WebAssembly.instantiate(",
302        source_language: "javascript",
303        target_language: Some("wasm"),
304        extract: extract_paren_arg,
305    },
306    BridgeRule {
307        kind: || BridgeKind::Wasm,
308        extensions: &["js", "ts"],
309        pattern: "WebAssembly.instantiateStreaming(",
310        source_language: "javascript",
311        target_language: Some("wasm"),
312        extract: extract_paren_arg,
313    },
314    // ── VB6 DLL / COM ───────────────────────────────────────────────────────
315    BridgeRule {
316        kind: || BridgeKind::Ffi,
317        extensions: &["bas", "cls", "frm", "ctl"],
318        pattern: "Declare Function",
319        source_language: "vb6",
320        target_language: Some("c"),
321        extract: extract_vb6_lib,
322    },
323    BridgeRule {
324        kind: || BridgeKind::Ffi,
325        extensions: &["bas", "cls", "frm", "ctl"],
326        pattern: "Declare Sub",
327        source_language: "vb6",
328        target_language: Some("c"),
329        extract: extract_vb6_lib,
330    },
331    BridgeRule {
332        kind: || BridgeKind::Com,
333        extensions: &["bas", "cls", "frm", "ctl"],
334        pattern: "CreateObject(",
335        source_language: "vb6",
336        target_language: Some("com"),
337        extract: extract_paren_arg,
338    },
339];
340
341// ---------------------------------------------------------------------------
342// Scanner
343// ---------------------------------------------------------------------------
344
345/// Scan a project directory for cross-language bridge points.
346pub fn detect_bridges(root: &Path) -> Result<BridgeScanResult> {
347    let mut result = BridgeScanResult::default();
348    scan_dir(root, root, &mut result)?;
349    Ok(result)
350}
351
352fn scan_dir(root: &Path, dir: &Path, result: &mut BridgeScanResult) -> Result<()> {
353    const SKIP_DIRS: &[&str] = &[
354        ".git",
355        ".infigraph",
356        "node_modules",
357        "__pycache__",
358        "target",
359        "build",
360        "dist",
361    ];
362    for entry in std::fs::read_dir(dir)? {
363        let entry = entry?;
364        let path = entry.path();
365        let name = entry.file_name();
366        let name_str = name.to_string_lossy();
367        if path.is_dir() {
368            if !SKIP_DIRS.contains(&name_str.as_ref()) && !name_str.starts_with('.') {
369                scan_dir(root, &path, result)?;
370            }
371        } else if path.is_file() {
372            scan_file(root, &path, result);
373        }
374    }
375    Ok(())
376}
377
378fn scan_file(root: &Path, path: &Path, result: &mut BridgeScanResult) {
379    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
380    let rel = path
381        .strip_prefix(root)
382        .unwrap_or(path)
383        .to_string_lossy()
384        .replace('\\', "/");
385
386    // Only scan rules whose extensions match
387    let applicable: Vec<&BridgeRule> = BRIDGE_RULES
388        .iter()
389        .filter(|r| r.extensions.is_empty() || r.extensions.contains(&ext))
390        .collect();
391
392    if applicable.is_empty() {
393        return;
394    }
395
396    let Ok(source) = std::fs::read_to_string(path) else {
397        return;
398    };
399
400    for (line_no, line) in source.lines().enumerate() {
401        for rule in &applicable {
402            if line.contains(rule.pattern) {
403                let foreign_symbol = (rule.extract)(line);
404                result.bridges.push(Bridge {
405                    file: rel.clone(),
406                    line: line_no as u32 + 1,
407                    kind: (rule.kind)(),
408                    foreign_symbol,
409                    source_language: rule.source_language.to_string(),
410                    target_language: rule.target_language.map(str::to_string),
411                    detail: line.trim().to_string(),
412                });
413            }
414        }
415    }
416}