Skip to main content

apple_bindgen/deps/
isolation.rs

1//! Framework dependency isolation module.
2//!
3//! Provides symbol filtering to avoid duplicate definitions when
4//! frameworks depend on each other.
5
6use std::collections::{HashMap, HashSet};
7use std::path::PathBuf;
8use std::process::Command;
9use syn::{Item, Visibility};
10
11/// Get the type prefixes for a given framework.
12/// Only types with these prefixes are considered "owned" by the framework.
13fn framework_prefixes(framework: &str) -> Option<&'static [&'static str]> {
14    match framework {
15        // Root framework - owns everything (system constants, etc.)
16        "CoreFoundation" => None,
17        "Foundation" => Some(&["NS", "__NS"]),
18        // AppKit/CoreData use NS* but don't own them (Foundation does)
19        "AppKit" => Some(&[]),
20        "CoreData" => Some(&[]),
21        "CoreGraphics" => Some(&["CG", "__CG"]),
22        "CoreText" => Some(&["CT", "__CT"]),
23        "QuartzCore" => Some(&["CA", "__CA"]),
24        "CoreServices" => Some(&["LS", "UT", "MDItem", "FSEvent", "AE"]),
25        "CoreImage" => Some(&["CI", "__CI"]),
26        "CoreMedia" => Some(&["CM", "__CM"]),
27        "CoreVideo" => Some(&["CV", "__CV"]),
28        "CoreAudio" => Some(&["Audio", "kAudio"]),
29        "AVFoundation" => Some(&["AV", "__AV"]),
30        "Metal" => Some(&["MTL", "__MTL"]),
31        "IOKit" => Some(&["IO", "io_", "kIO"]),
32        "Security" => Some(&["Sec", "CSSM", "kSec"]),
33        "SystemConfiguration" => Some(&["SC", "kSC"]),
34        "ImageIO" => Some(&["CGImage", "kCGImage"]),
35        "ColorSync" => Some(&["ColorSync"]),
36        "Cocoa" => Some(&[]), // Umbrella framework, no unique types
37        _ => None,
38    }
39}
40
41/// Common bindgen symbols that should only be defined once
42const BINDGEN_COMMON_SYMBOLS: &[&str] = &[
43    "__BindgenBitfieldUnit",
44    "__BindgenComplex",
45    "__BindgenFloat16",
46    "__IncompleteArrayField",
47    "id",
48];
49
50/// Check if a symbol is a bindgen common symbol
51fn is_bindgen_common_symbol(symbol: &str) -> bool {
52    BINDGEN_COMMON_SYMBOLS.contains(&symbol)
53}
54
55/// Check if a symbol belongs to a framework (has framework-specific prefix)
56fn is_framework_owned_symbol(symbol: &str, framework: &str) -> bool {
57    // Bindgen common symbols should be defined in every framework
58    // because generated code uses fully qualified paths
59    if is_bindgen_common_symbol(symbol) {
60        return false;
61    }
62    match framework_prefixes(framework) {
63        Some(prefixes) if prefixes.is_empty() => false,
64        Some(prefixes) => prefixes.iter().any(|p| symbol.starts_with(p)),
65        None => true, // No prefix info = root framework, owns everything
66    }
67}
68
69/// Filter dep_symbols to only include framework-owned symbols
70pub fn get_filterable_dep_symbols(
71    dep_symbols: &HashSet<String>,
72    dep_framework: &str,
73) -> HashSet<String> {
74    dep_symbols
75        .iter()
76        .filter(|s| is_framework_owned_symbol(s, dep_framework))
77        .cloned()
78        .collect()
79}
80
81/// Get SDK version from xcrun
82pub fn get_sdk_version() -> String {
83    Command::new("xcrun")
84        .args(["--show-sdk-version"])
85        .output()
86        .ok()
87        .and_then(|o| String::from_utf8(o.stdout).ok())
88        .map(|s| s.trim().to_string())
89        .unwrap_or_else(|| "unknown".to_string())
90}
91
92/// Cache key for symbol cache validation
93#[derive(Debug, PartialEq)]
94pub struct CacheKey {
95    pub sdk_version: String,
96    pub bindgen_version: String,
97    pub apple_bindgen_version: String,
98}
99
100impl CacheKey {
101    pub fn current() -> Self {
102        Self {
103            sdk_version: get_sdk_version(),
104            bindgen_version: "0.72".to_string(), // bindgen major.minor
105            apple_bindgen_version: env!("CARGO_PKG_VERSION").to_string(),
106        }
107    }
108
109    /// Get the cache subdirectory name for this key
110    pub fn cache_subdir(&self) -> String {
111        format!(
112            "MacOSX{}-bindgen{}-apple_bindgen{}",
113            self.sdk_version, self.bindgen_version, self.apple_bindgen_version
114        )
115    }
116}
117
118/// Load cached symbols for a framework
119pub fn load_cached_symbols(
120    cache_dir: &PathBuf,
121    framework: &str,
122    current_key: &CacheKey,
123) -> Option<HashSet<String>> {
124    load_cached_framework(cache_dir, framework, current_key).map(|(syms, _)| syms)
125}
126
127/// Load cached symbols and dependencies for a framework
128pub fn load_cached_framework(
129    cache_dir: &PathBuf,
130    framework: &str,
131    current_key: &CacheKey,
132) -> Option<(HashSet<String>, Vec<String>)> {
133    let versioned_dir = cache_dir.join(current_key.cache_subdir());
134    let cache_file = versioned_dir.join(format!("{}.toml", framework));
135    let content = std::fs::read_to_string(&cache_file).ok()?;
136
137    let mut symbols = Vec::new();
138    let mut dependencies = Vec::new();
139    let mut section = "";
140
141    for line in content.lines() {
142        let line = line.trim();
143        if line.starts_with("symbols") {
144            section = "symbols";
145        } else if line.starts_with("dependencies") {
146            section = "dependencies";
147        } else if line == "]" {
148            section = "";
149        } else if !section.is_empty() {
150            let val = line.trim_matches(|c| c == '"' || c == ',' || c == ' ');
151            if !val.is_empty() {
152                match section {
153                    "symbols" => symbols.push(val.to_string()),
154                    "dependencies" => dependencies.push(val.to_string()),
155                    _ => {}
156                }
157            }
158        }
159    }
160
161    Some((symbols.into_iter().collect(), dependencies))
162}
163
164/// Save unique symbols to cache file (symbols not found in dependencies)
165pub fn save_cached_symbols(
166    cache_dir: &PathBuf,
167    framework: &str,
168    key: &CacheKey,
169    unique_symbols: &HashSet<String>,
170    dependencies: &[String],
171) {
172    let versioned_dir = cache_dir.join(key.cache_subdir());
173    let _ = std::fs::create_dir_all(&versioned_dir);
174    let cache_file = versioned_dir.join(format!("{}.toml", framework));
175
176    let mut sorted_symbols: Vec<_> = unique_symbols.iter().collect();
177    sorted_symbols.sort();
178
179    let mut sorted_deps: Vec<_> = dependencies.to_vec();
180    sorted_deps.sort();
181
182    let mut content = String::new();
183    content.push_str("dependencies = [\n");
184    for dep in &sorted_deps {
185        content.push_str(&format!("  \"{}\",\n", dep));
186    }
187    content.push_str("]\n\n");
188    content.push_str("symbols = [\n");
189    for sym in sorted_symbols {
190        content.push_str(&format!("  \"{}\",\n", sym));
191    }
192    content.push_str("]\n");
193
194    if let Err(e) = std::fs::write(&cache_file, content) {
195        eprintln!(
196            "Warning: Failed to write cache file {}: {}",
197            cache_file.display(),
198            e
199        );
200    }
201}
202
203/// Load framework dependencies from deps.toml
204pub fn load_deps() -> HashMap<String, Vec<String>> {
205    let deps_content = include_str!("../../deps.toml");
206    let mut deps = HashMap::new();
207
208    for line in deps_content.lines() {
209        let line = line.trim();
210        if line.is_empty() || line.starts_with('#') {
211            continue;
212        }
213        if let Some((name, rest)) = line.split_once(" = ") {
214            let deps_str = rest.trim_matches(|c| c == '[' || c == ']');
215            let dep_list: Vec<String> = deps_str
216                .split(',')
217                .map(|s| s.trim().trim_matches('"').to_string())
218                .filter(|s| !s.is_empty())
219                .collect();
220            if !dep_list.is_empty() {
221                deps.insert(name.to_string(), dep_list);
222            }
223        }
224    }
225    deps
226}
227
228/// Collect all transitive dependencies for a framework
229pub fn collect_all_deps(
230    framework: &str,
231    deps: &HashMap<String, Vec<String>>,
232    result: &mut HashSet<String>,
233) {
234    if let Some(direct_deps) = deps.get(framework) {
235        for dep in direct_deps {
236            if result.insert(dep.clone()) {
237                collect_all_deps(dep, deps, result);
238            }
239        }
240    }
241}
242
243/// Topological sort of frameworks based on dependencies
244pub fn topological_sort(frameworks: &[&str], deps: &HashMap<String, Vec<String>>) -> Vec<String> {
245    let framework_set: HashSet<&str> = frameworks.iter().copied().collect();
246    let mut result = Vec::new();
247    let mut visited = HashSet::new();
248    let mut temp_mark = HashSet::new();
249
250    fn visit(
251        node: &str,
252        deps: &HashMap<String, Vec<String>>,
253        framework_set: &HashSet<&str>,
254        visited: &mut HashSet<String>,
255        temp_mark: &mut HashSet<String>,
256        result: &mut Vec<String>,
257    ) {
258        if visited.contains(node) {
259            return;
260        }
261        if temp_mark.contains(node) {
262            return;
263        }
264        temp_mark.insert(node.to_string());
265
266        if let Some(node_deps) = deps.get(node) {
267            for dep in node_deps {
268                if framework_set.contains(dep.as_str()) {
269                    visit(dep, deps, framework_set, visited, temp_mark, result);
270                }
271            }
272        }
273
274        temp_mark.remove(node);
275        visited.insert(node.to_string());
276        result.push(node.to_string());
277    }
278
279    for &framework in frameworks {
280        visit(
281            framework,
282            deps,
283            &framework_set,
284            &mut visited,
285            &mut temp_mark,
286            &mut result,
287        );
288    }
289
290    result
291}
292
293/// Extract public symbols from generated Rust code using syn
294pub fn extract_symbols(code: &str) -> HashSet<String> {
295    let mut symbols = HashSet::new();
296
297    let file = match syn::parse_file(code) {
298        Ok(f) => f,
299        Err(e) => {
300            eprintln!("Warning: Failed to parse generated code: {}", e);
301            return symbols;
302        }
303    };
304
305    for item in file.items {
306        if let Some(name) = extract_item_name(&item) {
307            // Skip bindgen anonymous types: each independent bindgen run assigns
308            // sequential numbers, so _bindgen_ty_42 in framework A is a different
309            // type than _bindgen_ty_42 in framework B.
310            if !name.starts_with("_bindgen_ty_") {
311                symbols.insert(name);
312            }
313        }
314    }
315
316    symbols
317}
318
319/// Extract the name of a public item
320fn extract_item_name(item: &Item) -> Option<String> {
321    match item {
322        Item::Struct(s) if matches!(s.vis, Visibility::Public(_)) => Some(s.ident.to_string()),
323        Item::Enum(e) if matches!(e.vis, Visibility::Public(_)) => Some(e.ident.to_string()),
324        Item::Type(t) if matches!(t.vis, Visibility::Public(_)) => Some(t.ident.to_string()),
325        Item::Fn(f) if matches!(f.vis, Visibility::Public(_)) => Some(f.sig.ident.to_string()),
326        Item::Const(c) if matches!(c.vis, Visibility::Public(_)) => Some(c.ident.to_string()),
327        Item::Static(s) if matches!(s.vis, Visibility::Public(_)) => Some(s.ident.to_string()),
328        Item::Trait(t) if matches!(t.vis, Visibility::Public(_)) => Some(t.ident.to_string()),
329        Item::Union(u) if matches!(u.vis, Visibility::Public(_)) => Some(u.ident.to_string()),
330        _ => None,
331    }
332}
333
334/// Get the type name from an impl block's self_ty
335fn get_impl_type_name(impl_item: &syn::ItemImpl) -> Option<String> {
336    match impl_item.self_ty.as_ref() {
337        syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
338        _ => None,
339    }
340}
341
342/// Get the trait name from an impl block if it's a trait impl
343fn get_impl_trait_name(impl_item: &syn::ItemImpl) -> Option<String> {
344    impl_item
345        .trait_
346        .as_ref()
347        .and_then(|(_, path, _)| path.segments.last().map(|s| s.ident.to_string()))
348}
349
350/// Check if a use tree references any symbol in dep_symbols
351fn use_references_dep_symbol(tree: &syn::UseTree, dep_symbols: &HashSet<String>) -> bool {
352    match tree {
353        syn::UseTree::Path(path) => use_references_dep_symbol(&path.tree, dep_symbols),
354        syn::UseTree::Name(name) => dep_symbols.contains(&name.ident.to_string()),
355        syn::UseTree::Rename(rename) => dep_symbols.contains(&rename.ident.to_string()),
356        syn::UseTree::Glob(_) => false,
357        syn::UseTree::Group(group) => group
358            .items
359            .iter()
360            .any(|t| use_references_dep_symbol(t, dep_symbols)),
361    }
362}
363
364/// Check if a use tree references a symbol NOT in the reachable set.
365/// For `use self::EnumName::*;`, checks if EnumName is reachable.
366fn use_references_unreachable(tree: &syn::UseTree, reachable: &HashSet<String>) -> bool {
367    match tree {
368        syn::UseTree::Path(path) => {
369            let segment = path.ident.to_string();
370            // `use self::X::*` → check if X is reachable
371            if segment == "self" {
372                return use_references_unreachable(&path.tree, reachable);
373            }
374            // External paths (crate::, std::, etc.) are always kept
375            if matches!(
376                segment.as_str(),
377                "crate" | "super" | "std" | "core" | "alloc" | "objc" | "objc2"
378            ) {
379                return false;
380            }
381            // The path segment itself is a symbol name — check if it's reachable
382            if !reachable.contains(&segment) {
383                return true;
384            }
385            use_references_unreachable(&path.tree, reachable)
386        }
387        syn::UseTree::Name(name) => {
388            let n = name.ident.to_string();
389            !reachable.contains(&n)
390        }
391        syn::UseTree::Rename(rename) => {
392            let n = rename.ident.to_string();
393            !reachable.contains(&n)
394        }
395        syn::UseTree::Glob(_) => false,
396        syn::UseTree::Group(group) => {
397            // If ALL items in the group are unreachable, remove the entire use
398            group
399                .items
400                .iter()
401                .all(|t| use_references_unreachable(t, reachable))
402        }
403    }
404}
405
406/// Extract source and alias from a `use self::Source as Alias;` tree.
407fn extract_use_rename(tree: &syn::UseTree) -> Option<(String, String)> {
408    match tree {
409        syn::UseTree::Path(path) if path.ident == "self" => extract_use_rename(&path.tree),
410        syn::UseTree::Rename(rename) => {
411            Some((rename.ident.to_string(), rename.rename.to_string()))
412        }
413        _ => None,
414    }
415}
416
417/// Filter generated bindgen code to only include symbols owned by this framework.
418///
419/// This function performs three passes:
420///
421/// ## Pass 1: Primary filtering (strict)
422///
423/// Iterates over all items in the parsed code and keeps only:
424/// - **Named items** (struct, enum, trait, type, const, static, fn, union):
425///   kept if the name is in `reachable` (the framework's unique symbol set).
426/// - **`use` statements**: kept if all referenced names are in `reachable`.
427///   Removes imports that reference types owned by other frameworks.
428/// - **`extern "C"` blocks**: individual foreign items are kept/dropped by name.
429/// - **Impl blocks**: kept if the implementing type is in `reachable` AND all
430///   type references within the block resolve to `reachable ∪ available ∪ builtin`.
431///   This prevents emitting impl blocks that reference types from frameworks
432///   not in the dependency chain (e.g., ObjC category extensions on NSString
433///   that reference AppKit types would be dropped when filtering Foundation).
434///
435/// ## Pass 2: Restore dropped trait definitions
436///
437/// The dependency closure in `compute_ownership` may remove trait definitions
438/// from `reachable` if their method signatures reference types that aren't
439/// resolved (e.g., `IEKCalendar` uses `CGColorRef` which may not be available
440/// during closure computation). This cascades: if the trait is removed, all
441/// `impl Trait for Struct` blocks also fail the deps check in Pass 1.
442///
443/// This pass scans the original source for impl blocks whose implementing
444/// struct survived Pass 1 but whose trait did not. Those traits are "needed"
445/// and their definitions are restored from the original source.
446///
447/// ## Pass 3: Restore dropped impl blocks
448///
449/// With the needed traits now in the output, re-scans the original source for
450/// impl blocks where the implementing type is in the surviving set and the
451/// trait is either surviving, needed, or available from a dependency framework.
452/// These impl blocks are added back.
453///
454/// ## Arguments
455///
456/// - `code`: Raw generated Rust source code (from bindgen).
457/// - `reachable`: Unique symbols owned by this framework.
458/// - `dep_frameworks`: List of dependency framework names (for `use crate::X::*`).
459/// - `available`: Combined symbol set of this framework + all dependencies.
460///   Used to validate impl block type references.
461/// Build a dedup key for an impl block that distinguishes different impl blocks
462/// for the same type. Trait impls use the trait name; plain impls use the first
463/// method/item name (e.g., `__BindgenBitfieldUnit` has two plain impls: one
464/// with `new`, another with `get`/`set`).
465fn impl_dedup_key(impl_item: &syn::ItemImpl) -> String {
466    let type_name = get_impl_type_name(impl_item);
467    let trait_name = get_impl_trait_name(impl_item);
468    let discriminant = trait_name
469        .as_deref()
470        .map(|t| t.to_string())
471        .unwrap_or_else(|| {
472            impl_item
473                .items
474                .first()
475                .and_then(|it| match it {
476                    syn::ImplItem::Fn(f) => Some(f.sig.ident.to_string()),
477                    syn::ImplItem::Const(c) => Some(c.ident.to_string()),
478                    syn::ImplItem::Type(t) => Some(t.ident.to_string()),
479                    _ => None,
480                })
481                .unwrap_or_default()
482        });
483    format!(
484        "impl_{}_{}",
485        type_name.as_deref().unwrap_or(""),
486        discriminant
487    )
488}
489
490pub fn filter_to_reachable(
491    code: &str,
492    reachable: &HashSet<String>,
493    dep_frameworks: &[String],
494    available: Option<&HashSet<String>>,
495) -> String {
496    let file = match syn::parse_file(code) {
497        Ok(f) => f,
498        Err(_) => return code.to_string(),
499    };
500
501    let mut filtered_items = Vec::new();
502    let mut extern_blocks = Vec::new();
503    let mut emitted_symbols: HashSet<String> = HashSet::new();
504
505    for item in file.items {
506        match &item {
507            Item::ForeignMod(fm) => {
508                let mut filtered_foreign_items = Vec::new();
509                for foreign_item in &fm.items {
510                    let name = match foreign_item {
511                        syn::ForeignItem::Fn(f) => Some(f.sig.ident.to_string()),
512                        syn::ForeignItem::Static(s) => Some(s.ident.to_string()),
513                        syn::ForeignItem::Type(t) => Some(t.ident.to_string()),
514                        _ => None,
515                    };
516                    if let Some(n) = name {
517                        if reachable.contains(&n) && emitted_symbols.insert(n) {
518                            filtered_foreign_items.push(foreign_item.clone());
519                        }
520                    } else {
521                        filtered_foreign_items.push(foreign_item.clone());
522                    }
523                }
524                if !filtered_foreign_items.is_empty() {
525                    let mut new_fm = fm.clone();
526                    new_fm.items = filtered_foreign_items;
527                    extern_blocks.push(Item::ForeignMod(new_fm));
528                }
529            }
530            Item::Impl(impl_item) => {
531                let type_name = get_impl_type_name(impl_item);
532                let type_reachable = type_name.as_ref().map_or(false, |n| reachable.contains(n));
533
534                if type_reachable {
535                    // If available set is provided, check that all type refs
536                    // in this impl block resolve to available symbols.
537                    let deps_satisfied = if let Some(avail) = available {
538                        use super::depgraph::{impl_block_deps, is_builtin};
539                        let deps = impl_block_deps(impl_item);
540                        deps.iter().all(|dep| {
541                            is_builtin(dep) || reachable.contains(dep) || avail.contains(dep)
542                        })
543                    } else {
544                        true
545                    };
546
547                    if deps_satisfied {
548                        if emitted_symbols.insert(impl_dedup_key(impl_item)) {
549                            filtered_items.push(item);
550                        }
551                    }
552                }
553            }
554            Item::Use(use_item) => {
555                if !use_references_unreachable(&use_item.tree, reachable) {
556                    filtered_items.push(item);
557                } else if let Some(avail) = available {
558                    // `pub use self::X as Y;` where Y is reachable but X is owned by
559                    // another framework. Convert to `pub type Y = X;` so it compiles
560                    // when X is imported via `use crate::OtherFramework::*;`.
561                    if let Some((source, alias)) = extract_use_rename(&use_item.tree) {
562                        if reachable.contains(&alias) && avail.contains(&source) {
563                            let type_alias: Item = syn::parse_str(&format!(
564                                "pub type {alias} = {source};"
565                            ))
566                            .expect("failed to parse type alias");
567                            filtered_items.push(type_alias);
568                        }
569                    }
570                }
571            }
572            _ => {
573                if let Some(name) = extract_item_name(&item) {
574                    if reachable.contains(&name) && emitted_symbols.insert(name) {
575                        filtered_items.push(item);
576                    }
577                } else {
578                    filtered_items.push(item);
579                }
580            }
581        }
582    }
583
584    // Pass 2 & 3: Restore dropped traits and impl blocks.
585    // When a trait is removed by dependency closure but its implementing
586    // struct survives, both the trait and impl block should be restored.
587    if let Ok(file2) = syn::parse_file(code) {
588        let surviving_names: HashSet<String> = filtered_items
589            .iter()
590            .filter_map(|item| extract_item_name(item))
591            .collect();
592
593        let available_set: HashSet<&str> = available
594            .iter()
595            .flat_map(|a| a.iter().map(|s| s.as_str()))
596            .collect();
597
598        // Find candidate traits for restoration: traits referenced by impl blocks
599        // whose implementing struct survived but the trait itself was not emitted.
600        let mut candidate_traits: HashSet<String> = HashSet::new();
601        for item in &file2.items {
602            if let Item::Impl(impl_item) = item {
603                let type_name = get_impl_type_name(impl_item);
604                let trait_name = get_impl_trait_name(impl_item);
605                if let (Some(tn), Some(trn)) = (&type_name, &trait_name) {
606                    if surviving_names.contains(tn)
607                        && !surviving_names.contains(trn)
608                        && !available_set.contains(trn.as_str())
609                    {
610                        candidate_traits.insert(trn.clone());
611                    }
612                }
613            }
614        }
615
616        // Validate candidates: only restore traits whose type dependencies
617        // are all satisfied (in surviving_names or available). This prevents
618        // restoring traits whose methods reference types from unavailable
619        // frameworks (e.g., INSObject in the objc module references instancetype
620        // from CoreFoundation).
621        use super::depgraph::is_builtin;
622        let mut needed_traits: HashSet<String> = HashSet::new();
623        for item in &file2.items {
624            if let Item::Trait(trait_item) = item {
625                let name = trait_item.ident.to_string();
626                if candidate_traits.contains(&name) {
627                    let mut collector = super::depgraph::TypeRefCollector::new();
628                    syn::visit::Visit::visit_item_trait(&mut collector, trait_item);
629                    let deps_ok = collector.types.iter().all(|dep| {
630                        dep == &name
631                            || is_builtin(dep)
632                            || surviving_names.contains(dep)
633                            || available_set.contains(dep.as_str())
634                    });
635                    if deps_ok {
636                        needed_traits.insert(name);
637                    }
638                }
639            }
640        }
641
642        // Restore validated trait definitions
643        for item in &file2.items {
644            if let Some(name) = extract_item_name(item) {
645                if needed_traits.contains(&name) && emitted_symbols.insert(name) {
646                    filtered_items.push(item.clone());
647                }
648            }
649        }
650
651        // Restore impl blocks whose type survived and trait is now known
652        for item in &file2.items {
653            if let Item::Impl(impl_item) = item {
654                let type_name = get_impl_type_name(impl_item);
655                let trait_name = get_impl_trait_name(impl_item);
656
657                let type_ok = type_name
658                    .as_ref()
659                    .map_or(false, |n| surviving_names.contains(n));
660                let trait_ok = trait_name.as_ref().map_or(true, |n| {
661                    surviving_names.contains(n)
662                        || needed_traits.contains(n)
663                        || available_set.contains(n.as_str())
664                });
665
666                if type_ok && trait_ok {
667                    if emitted_symbols.insert(impl_dedup_key(impl_item)) {
668                        filtered_items.push(item.clone());
669                    }
670                }
671            }
672        }
673    }
674
675    let mut output = String::new();
676
677    for dep in dep_frameworks {
678        output.push_str(&format!(
679            "#[allow(unused_imports)]\nuse crate::{}::*;\n",
680            dep
681        ));
682    }
683    if !dep_frameworks.is_empty() {
684        output.push('\n');
685    }
686
687    use quote::ToTokens;
688    for item in filtered_items {
689        output.push_str(&item.to_token_stream().to_string());
690        output.push('\n');
691    }
692    for item in extern_blocks {
693        output.push_str(&item.to_token_stream().to_string());
694        output.push('\n');
695    }
696
697    output
698}
699
700/// Filter out symbols that exist in dependencies and add use statements
701pub fn filter_symbols(
702    code: &str,
703    dep_symbols: &HashSet<String>,
704    dep_frameworks: &[String],
705) -> String {
706    let file = match syn::parse_file(code) {
707        Ok(f) => f,
708        Err(_) => return code.to_string(),
709    };
710
711    let mut filtered_items = Vec::new();
712    let mut extern_blocks = Vec::new();
713    // Track already emitted symbols to avoid duplicates from bindgen
714    let mut emitted_symbols: HashSet<String> = HashSet::new();
715
716    for item in file.items {
717        match &item {
718            Item::ForeignMod(fm) => {
719                let mut filtered_foreign_items = Vec::new();
720                for foreign_item in &fm.items {
721                    let name = match foreign_item {
722                        syn::ForeignItem::Fn(f) => Some(f.sig.ident.to_string()),
723                        syn::ForeignItem::Static(s) => Some(s.ident.to_string()),
724                        syn::ForeignItem::Type(t) => Some(t.ident.to_string()),
725                        _ => None,
726                    };
727                    if let Some(n) = name {
728                        if !dep_symbols.contains(&n) && emitted_symbols.insert(n) {
729                            filtered_foreign_items.push(foreign_item.clone());
730                        }
731                    } else {
732                        filtered_foreign_items.push(foreign_item.clone());
733                    }
734                }
735                if !filtered_foreign_items.is_empty() {
736                    let mut new_fm = fm.clone();
737                    new_fm.items = filtered_foreign_items;
738                    extern_blocks.push(Item::ForeignMod(new_fm));
739                }
740            }
741            Item::Impl(impl_item) => {
742                let type_name = get_impl_type_name(impl_item);
743                let trait_name = get_impl_trait_name(impl_item);
744
745                let type_in_deps = type_name
746                    .as_ref()
747                    .map_or(false, |n| dep_symbols.contains(n));
748                let trait_in_deps = trait_name
749                    .as_ref()
750                    .map_or(false, |n| dep_symbols.contains(n));
751
752                // For impl blocks, check if both type and trait have been emitted
753                let impl_key = format!(
754                    "impl_{}_{}",
755                    type_name.as_deref().unwrap_or(""),
756                    trait_name.as_deref().unwrap_or("")
757                );
758
759                if !type_in_deps && !trait_in_deps && emitted_symbols.insert(impl_key) {
760                    filtered_items.push(item);
761                }
762            }
763            Item::Use(use_item) => {
764                if !use_references_dep_symbol(&use_item.tree, dep_symbols) {
765                    filtered_items.push(item);
766                }
767            }
768            _ => {
769                if let Some(name) = extract_item_name(&item) {
770                    if !dep_symbols.contains(&name) && emitted_symbols.insert(name) {
771                        filtered_items.push(item);
772                    }
773                } else {
774                    filtered_items.push(item);
775                }
776            }
777        }
778    }
779
780    let mut output = String::new();
781
782    // Add private use statements for dependencies (not pub, to avoid diamond problem)
783    for dep in dep_frameworks {
784        output.push_str(&format!(
785            "#[allow(unused_imports)]\nuse crate::{}::*;\n",
786            dep
787        ));
788    }
789    if !dep_frameworks.is_empty() {
790        output.push('\n');
791    }
792
793    use quote::ToTokens;
794    for item in filtered_items {
795        output.push_str(&item.to_token_stream().to_string());
796        output.push('\n');
797    }
798    for item in extern_blocks {
799        output.push_str(&item.to_token_stream().to_string());
800        output.push('\n');
801    }
802
803    output
804}