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 FabricViewResolver;
pub const FABRIC_VIEW_RESOLVER: FabricViewResolver = FabricViewResolver;
impl FrameworkResolver for FabricViewResolver {
fn name(&self) -> &'static str {
"fabric-view"
}
fn languages(&self) -> Option<&'static [Language]> {
Some(&[
Language::TypeScript,
Language::Tsx,
Language::ObjC,
Language::Java,
Language::Kotlin,
])
}
fn detect(&self, context: &mut dyn ResolutionContext) -> bool {
if package_has_react_native("package.json", context) {
return true;
}
for root in ["packages", "apps", "modules", "libraries"] {
for sub in context.list_directories(root) {
if package_has_react_native(&format!("{root}/{sub}/package.json"), context) {
return true;
}
}
}
false
}
fn extract(&self, file_path: &str, source: &str) -> FrameworkExtractionResult {
let nodes = if file_path.ends_with(".ts") || file_path.ends_with(".tsx") {
extract_fabric_nodes(file_path, source)
} else if file_path.ends_with(".m") || file_path.ends_with(".mm") {
extract_legacy_view_manager_nodes(file_path, source)
} else if file_path.ends_with(".java") || file_path.ends_with(".kt") {
extract_jvm_view_manager_nodes(file_path, source)
} else {
Vec::new()
};
FrameworkExtractionResult {
nodes,
references: Vec::new(),
}
}
fn resolve(
&self,
_reference: &crate::resolution::types::UnresolvedRef,
_context: &mut dyn ResolutionContext,
) -> Option<crate::resolution::types::ResolvedRef> {
None
}
}
fn package_has_react_native(path: &str, context: &mut dyn ResolutionContext) -> bool {
context
.read_file(path)
.is_some_and(|pkg| pkg.contains("\"react-native\"") || pkg.contains("'react-native'"))
}
fn derive_component_name_from_manager(class_name: &str) -> String {
let mut name = class_name
.strip_prefix("RCT")
.unwrap_or(class_name)
.to_string();
if let Some(stripped) = name.strip_suffix("ViewManager") {
name = stripped.to_string();
} else if let Some(stripped) = name.strip_suffix("Manager") {
name = stripped.to_string();
}
name
}
fn extract_fabric_nodes(file_path: &str, source: &str) -> Vec<crate::types::Node> {
if !source.contains("codegenNativeComponent") {
return Vec::new();
}
let mut nodes = Vec::new();
let decl_re = Regex::new(
r#"codegenNativeComponent\s*(?:<[^>]+>)?\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]"#,
)
.unwrap();
let lang = if file_path.ends_with(".tsx") {
Language::Tsx
} else {
Language::TypeScript
};
for caps in decl_re.captures_iter(source) {
let whole = caps.get(0).unwrap();
let component_name = caps.get(1).unwrap().as_str();
let line = line_for_offset(source, whole.start());
let mut node = make_node(
format!("fabric-component:{file_path}:{component_name}:{line}"),
NodeKind::Component,
component_name,
format!("{file_path}::{component_name}"),
file_path,
lang,
line,
Some(format!(
"codegenNativeComponent<NativeProps>('{component_name}')"
)),
Some(format!(
"Fabric/Codegen native component '{component_name}'"
)),
);
node.is_exported = Some(true);
nodes.push(node);
}
if let Some(body) = find_native_props_body(source) {
for prop_name in extract_prop_names(&body) {
let offset = source.find(&prop_name).unwrap_or(0);
let line = line_for_offset(source, offset);
let mut node = make_node(
format!("fabric-prop:{file_path}:{prop_name}:{line}"),
NodeKind::Property,
prop_name.clone(),
format!("{file_path}::NativeProps.{prop_name}"),
file_path,
lang,
line,
None,
Some(format!("Fabric NativeProps prop '{prop_name}'")),
);
node.is_exported = Some(true);
nodes.push(node);
}
}
nodes
}
fn find_native_props_body(source: &str) -> Option<String> {
Regex::new(r"(?s)export\s+interface\s+NativeProps\b[^{]*\{(.*?)\n\}")
.unwrap()
.captures(source)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
fn extract_prop_names(body: &str) -> Vec<String> {
let prop_re = Regex::new(r"(?m)^\s*([A-Za-z_][A-Za-z0-9_]*)\??\s*:").unwrap();
prop_re
.captures_iter(body)
.filter_map(|caps| caps.get(1).map(|m| m.as_str().to_string()))
.collect()
}
fn extract_legacy_view_manager_nodes(file_path: &str, source: &str) -> Vec<crate::types::Node> {
if !(source.contains("RCT_EXPORT_VIEW_PROPERTY")
|| source.contains("RCT_CUSTOM_VIEW_PROPERTY")
|| source.contains("RCT_REMAP_VIEW_PROPERTY"))
{
return Vec::new();
}
let Some(class_name) = Regex::new(r"@implementation\s+([A-Za-z_][A-Za-z0-9_]*)")
.unwrap()
.captures(source)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
else {
return Vec::new();
};
if !(class_name.ends_with("Manager") || class_name.ends_with("ViewManager")) {
return Vec::new();
}
let component_name = derive_component_name_from_manager(&class_name);
let mut nodes = vec![fabric_component_node(
file_path,
source,
&component_name,
&class_name,
Language::ObjC,
)];
let prop_re =
Regex::new(r"\bRCT_(?:EXPORT|CUSTOM|REMAP)_VIEW_PROPERTY\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)")
.unwrap();
push_fabric_props(
file_path,
source,
&component_name,
Language::ObjC,
&prop_re,
&mut nodes,
);
nodes
}
fn extract_jvm_view_manager_nodes(file_path: &str, source: &str) -> Vec<crate::types::Node> {
if !source.contains("@ReactProp") {
return Vec::new();
}
let Some(class_name) = Regex::new(r"\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b")
.unwrap()
.captures(source)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
else {
return Vec::new();
};
if !(class_name.ends_with("Manager") || class_name.ends_with("ViewManager")) {
return Vec::new();
}
let component_name = derive_component_name_from_manager(&class_name);
let language = if file_path.ends_with(".kt") {
Language::Kotlin
} else {
Language::Java
};
let mut nodes = vec![fabric_component_node(
file_path,
source,
&component_name,
&class_name,
language,
)];
let prop_re = Regex::new(r#"@ReactProp\s*\(\s*(?:name\s*=\s*)?"([^"]+)""#).unwrap();
push_fabric_props(
file_path,
source,
&component_name,
language,
&prop_re,
&mut nodes,
);
nodes
}
fn fabric_component_node(
file_path: &str,
source: &str,
component_name: &str,
class_name: &str,
language: Language,
) -> crate::types::Node {
let line = source
.find(class_name)
.map(|idx| line_for_offset(source, idx))
.unwrap_or(1);
let mut node = make_node(
format!("fabric-component:{file_path}:{component_name}:{line}"),
NodeKind::Component,
component_name,
format!("{file_path}::{component_name}"),
file_path,
language,
line,
Some(format!("class {class_name} : ViewManager")),
Some(format!(
"React Native view-manager component '{component_name}'"
)),
);
node.is_exported = Some(true);
node
}
fn push_fabric_props(
file_path: &str,
source: &str,
component_name: &str,
language: Language,
prop_re: &Regex,
nodes: &mut Vec<crate::types::Node>,
) {
let mut seen = Vec::<String>::new();
for caps in prop_re.captures_iter(source) {
let whole = caps.get(0).unwrap();
let prop_name = caps.get(1).unwrap().as_str();
if seen.iter().any(|item| item == prop_name) {
continue;
}
seen.push(prop_name.to_string());
let line = line_for_offset(source, whole.start());
let mut node = make_node(
format!("fabric-prop:{file_path}:{prop_name}:{line}"),
NodeKind::Property,
prop_name,
format!("{file_path}::{component_name}.{prop_name}"),
file_path,
language,
line,
None,
Some(format!(
"React Native view prop '{prop_name}' on {component_name}"
)),
);
node.is_exported = Some(true);
nodes.push(node);
}
}
fn line_for_offset(source: &str, offset: usize) -> u64 {
line_number_at_byte(source, offset)
}