use regex::Regex;
use crate::resolution::types::{
FrameworkExtractionResult, FrameworkResolver, ResolutionContext, make_node,
};
use crate::types::{Language, NodeKind};
use crate::utils::line_number_at_byte;
pub struct ExpoModulesResolver;
pub const EXPO_MODULES_RESOLVER: ExpoModulesResolver = ExpoModulesResolver;
impl FrameworkResolver for ExpoModulesResolver {
fn name(&self) -> &'static str {
"expo-modules"
}
fn languages(&self) -> Option<&'static [Language]> {
Some(&[Language::Swift, Language::Kotlin])
}
fn detect(&self, context: &mut dyn ResolutionContext) -> bool {
if context
.read_file("package.json")
.is_some_and(|pkg| pkg.contains("expo-modules-core"))
{
return true;
}
for file in context.get_all_files().into_iter().take(200) {
if (file.ends_with(".swift") || file.ends_with(".kt"))
&& context
.read_file(&file)
.is_some_and(|source| is_expo_module_source(&source))
{
return true;
}
}
false
}
fn extract(&self, file_path: &str, source: &str) -> FrameworkExtractionResult {
let language = if file_path.ends_with(".kt") {
Language::Kotlin
} else {
Language::Swift
};
FrameworkExtractionResult {
nodes: extract_expo_methods(file_path, source, language),
references: Vec::new(),
}
}
fn resolve(
&self,
_reference: &crate::resolution::types::UnresolvedRef,
_context: &mut dyn ResolutionContext,
) -> Option<crate::resolution::types::ResolvedRef> {
None
}
}
fn is_expo_module_source(source: &str) -> bool {
Regex::new(r"\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*Module\b")
.unwrap()
.is_match(source)
&& Regex::new(r#"\b(Function|AsyncFunction|Property|Constants)\s*(?:<[^(]*>)?\s*\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']"#)
.unwrap()
.is_match(source)
}
fn extract_expo_methods(
file_path: &str,
source: &str,
language: Language,
) -> Vec<crate::types::Node> {
if !is_expo_module_source(source) {
return Vec::new();
}
let module_name = Regex::new(r#"\bName\s*\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']"#)
.unwrap()
.captures(source)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
.or_else(|| {
Regex::new(r"\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*Module\b")
.unwrap()
.captures(source)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
})
.unwrap_or_else(|| "ExpoModule".to_string());
let decl_re = Regex::new(r#"\b(Function|AsyncFunction|Property|Constants)\s*(?:<[^(]*>)?\s*\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']"#).unwrap();
let mut nodes = Vec::new();
let mut seen = Vec::<String>::new();
for caps in decl_re.captures_iter(source) {
let whole = caps.get(0).unwrap();
let kind = caps.get(1).unwrap().as_str();
let method_name = caps.get(2).unwrap().as_str();
let line = line_for_offset(source, whole.start());
let key = format!("{method_name}:{line}");
if seen.iter().any(|item| item == &key) {
continue;
}
seen.push(key);
let mut node = make_node(
format!("expo-module:{file_path}:{module_name}:{method_name}:{line}"),
NodeKind::Method,
method_name,
format!("{file_path}::{module_name}.{method_name}"),
file_path,
language,
line,
Some(format!("{kind}(\"{method_name}\")")),
Some(format!(
"Expo Modules {kind}(\"{method_name}\") in {module_name}"
)),
);
node.is_exported = Some(true);
nodes.push(node);
}
nodes
}
fn line_for_offset(source: &str, offset: usize) -> u64 {
line_number_at_byte(source, offset)
}