use crate::resolution::types::{FrameworkResolver, ResolutionContext, ResolvedRef, UnresolvedRef};
use crate::types::{Language, Node, NodeKind};
const GENERIC_NAMES: &[&str] = &[
"init",
"description",
"debugDescription",
"hash",
"isEqual",
"isEqualTo",
"copy",
"mutableCopy",
"class",
"self",
"count",
"length",
"value",
"name",
"data",
"string",
"object",
"add",
"remove",
"update",
"load",
"save",
"reload",
"cancel",
"start",
"stop",
"pause",
"resume",
"close",
"open",
"show",
"hide",
"toString",
"dealloc",
"release",
"retain",
"autorelease",
];
const SOURCE_PROBE_LINES: usize = 3;
pub struct SwiftObjcBridgeResolver;
pub const SWIFT_OBJC_BRIDGE_RESOLVER: SwiftObjcBridgeResolver = SwiftObjcBridgeResolver;
impl FrameworkResolver for SwiftObjcBridgeResolver {
fn name(&self) -> &'static str {
"swift-objc-bridge"
}
fn languages(&self) -> Option<&'static [Language]> {
Some(&[Language::Swift, Language::ObjC])
}
fn detect(&self, context: &mut dyn ResolutionContext) -> bool {
let mut has_swift = false;
let mut has_objc = false;
for file in context.get_all_files() {
if file.ends_with(".swift") {
has_swift = true;
} else if file.ends_with(".m") || file.ends_with(".mm") {
has_objc = true;
}
if has_swift && has_objc {
return true;
}
}
false
}
fn claims_reference(&self, name: &str) -> bool {
name.contains(':')
}
fn resolve(
&self,
reference: &UnresolvedRef,
context: &mut dyn ResolutionContext,
) -> Option<ResolvedRef> {
match reference.language {
Language::Swift => resolve_swift_call_to_objc(reference, context),
Language::ObjC => resolve_objc_call_to_swift(reference, context),
_ => None,
}
}
}
fn resolve_swift_call_to_objc(
reference: &UnresolvedRef,
context: &mut dyn ResolutionContext,
) -> Option<ResolvedRef> {
let raw_name = reference
.reference_name
.rsplit('.')
.next()
.unwrap_or(&reference.reference_name);
let target = context
.get_nodes_by_kind(NodeKind::Method)
.into_iter()
.filter(|node| node.language == Language::ObjC)
.find(|node| {
swift_base_names_for_objc_selector(&node.name)
.into_iter()
.any(|candidate| {
candidate == raw_name && !GENERIC_NAMES.contains(&candidate.as_str())
})
})?;
Some(ResolvedRef::framework(reference, target.id, 0.6))
}
fn resolve_objc_call_to_swift(
reference: &UnresolvedRef,
context: &mut dyn ResolutionContext,
) -> Option<ResolvedRef> {
let raw_selector = reference
.reference_name
.rsplit('.')
.next()
.unwrap_or(&reference.reference_name);
if !raw_selector.contains(':') {
return None;
}
for candidate in swift_base_names_for_objc_selector(raw_selector) {
for node in context.get_nodes_by_name(&candidate) {
if node.language == Language::Swift
&& matches!(node.kind, NodeKind::Method | NodeKind::Function)
&& is_objc_exposed(&declaration_source_window(&node, context))
{
return Some(ResolvedRef::framework(reference, node.id, 0.6));
}
}
}
None
}
fn swift_base_names_for_objc_selector(selector: &str) -> Vec<String> {
let selector = selector.trim_matches(':');
if selector.is_empty() {
return Vec::new();
}
let first = selector.split(':').next().unwrap_or(selector);
let mut candidates = vec![first.to_string()];
for suffix in ["With", "By", "For", "From", "To", "In", "On"] {
if let Some(idx) = first.find(suffix) {
let base = &first[..idx];
if !base.is_empty() {
candidates.push(base.to_string());
}
}
}
candidates.sort();
candidates.dedup();
candidates
}
fn declaration_source_window(node: &Node, context: &mut dyn ResolutionContext) -> String {
let Some(content) = context.read_file(&node.file_path) else {
return String::new();
};
let lines = content.lines().collect::<Vec<_>>();
let end = node.start_line as usize;
let start = end.saturating_sub(SOURCE_PROBE_LINES + 1);
lines[start..end.min(lines.len())].join("\n")
}
fn is_objc_exposed(source: &str) -> bool {
source.contains("@objc") && !source.contains("@nonobjc")
}