#[allow(clippy::wildcard_imports, reason = "many AST types used")]
use oxc_ast::ast::*;
use rustc_hash::FxHashSet;
use fallow_types::extract::{
ComponentFunction, ComponentFunctionKind, ComponentProp, ForwardAttr, HookUse, HookUseKind,
RenderEdge,
};
use super::{ModuleInfoExtractor, PendingComponentArrow};
impl ModuleInfoExtractor {
pub(crate) fn react_prescan_variable_declaration(&mut self, decl: &VariableDeclaration<'_>) {
if !self.jsx_capable {
return;
}
for declarator in &decl.declarations {
let BindingPattern::BindingIdentifier(id) = &declarator.id else {
continue;
};
let name = id.name.as_str();
if !is_component_name(name) {
continue;
}
let Some(init) = &declarator.init else {
continue;
};
let is_exported = self.is_exported_binding(name);
if let Some((func_span, kind)) = classify_component_init(init) {
self.pending_component_arrows.insert(
func_span,
PendingComponentArrow {
name: name.to_string(),
kind,
is_exported,
},
);
}
}
}
pub(crate) fn react_enter_function(&mut self, func: &Function<'_>) -> bool {
if !self.jsx_capable {
return false;
}
if let Some(pending) = self.pending_component_arrows.remove(&func.span) {
let component = pending.name.clone();
self.begin_component(
pending.name,
func.span.start,
pending.kind,
pending.is_exported,
);
self.harvest_function_props(&component, &func.params, func.body.as_deref());
return true;
}
let Some(id) = func.id.as_ref() else {
return false;
};
let name = id.name.as_str();
if !is_component_name(name) || !function_body_returns_jsx(func.body.as_deref()) {
return false;
}
let is_exported = self.is_exported_binding(name);
self.begin_component(
name.to_string(),
func.span.start,
ComponentFunctionKind::FnDecl,
is_exported,
);
self.harvest_function_props(name, &func.params, func.body.as_deref());
true
}
pub(crate) fn react_enter_arrow(&mut self, expr: &ArrowFunctionExpression<'_>) -> bool {
if !self.jsx_capable {
return false;
}
let Some(pending) = self.pending_component_arrows.remove(&expr.span) else {
return false;
};
let component = pending.name.clone();
self.begin_component(
pending.name,
expr.span.start,
pending.kind,
pending.is_exported,
);
self.harvest_arrow_props(&component, &expr.params, &expr.body);
true
}
pub(crate) fn react_exit_component(&mut self, pushed: bool) {
if pushed {
self.component_stack.pop();
}
}
pub(crate) fn react_record_jsx_element(&mut self, element: &JSXElement<'_>) {
if !self.jsx_capable {
return;
}
let opening = &element.opening_element;
if jsx_is_provider_tag(&opening.name) {
self.react_mark_renders_provider();
}
if jsx_has_function_render_prop(opening) || jsx_children_has_function(&element.children) {
self.react_mark_children_as_function();
}
let Some(child_name) = jsx_component_tag_name(&opening.name) else {
return;
};
let (attr_names, has_spread, forward_attrs, has_complex_forward) =
collect_jsx_attributes(&opening.attributes);
let parent_component = self.component_stack.last().cloned().unwrap_or_default();
self.render_edges.push(RenderEdge {
parent_component,
child_component_name: child_name,
attr_names,
has_spread,
forward_attrs,
has_complex_forward,
});
}
fn react_mark_renders_provider(&mut self) {
if let Some(component) = self.component_functions.last_mut() {
component.renders_provider = true;
}
}
fn react_mark_children_as_function(&mut self) {
if let Some(component) = self.component_functions.last_mut() {
component.has_children_as_function = true;
}
}
pub(crate) fn react_record_hook_call(&mut self, call: &CallExpression<'_>) {
if !self.jsx_capable || self.component_stack.is_empty() {
return;
}
if is_clone_element_callee(&call.callee)
&& let Some(component) = self.component_functions.last_mut()
{
component.uses_clone_element = true;
}
let Expression::Identifier(callee) = &call.callee else {
return;
};
let Some(kind) = hook_kind(callee.name.as_str()) else {
return;
};
let dep_array_arity = hook_dep_array_arity(kind, &call.arguments);
self.hook_uses.push(HookUse {
kind,
dep_array_arity,
span_start: call.span.start,
});
}
fn begin_component(
&mut self,
name: String,
span_start: u32,
kind: ComponentFunctionKind,
is_exported: bool,
) {
self.component_functions.push(ComponentFunction {
name: name.clone(),
span_start,
kind,
is_exported,
has_unharvestable_props: false,
uses_clone_element: false,
renders_provider: false,
has_children_as_function: false,
is_pure_passthrough: false,
});
self.component_stack.push(name);
}
fn harvest_function_props(
&mut self,
component: &str,
params: &FormalParameters<'_>,
body: Option<&FunctionBody<'_>>,
) {
self.harvest_props_from_params(
component,
params.items.first().map(|p| &p.pattern),
params.rest.is_some(),
body,
);
}
fn harvest_arrow_props(
&mut self,
component: &str,
params: &FormalParameters<'_>,
body: &FunctionBody<'_>,
) {
self.harvest_props_from_params(
component,
params.items.first().map(|p| &p.pattern),
params.rest.is_some(),
Some(body),
);
}
fn harvest_props_from_params(
&mut self,
component: &str,
first: Option<&BindingPattern<'_>>,
has_rest_param: bool,
body: Option<&FunctionBody<'_>>,
) {
let mark_unharvestable = |this: &mut Self| {
if let Some(component) = this.component_functions.last_mut() {
component.has_unharvestable_props = true;
}
};
if let Some(props_root) = passthrough_spread_root(first)
&& body_is_pure_passthrough(body, &props_root)
&& let Some(component) = self.component_functions.last_mut()
{
component.is_pure_passthrough = true;
}
if has_rest_param {
mark_unharvestable(self);
return;
}
let Some(pattern) = first else {
return;
};
let BindingPattern::ObjectPattern(obj) = pattern else {
mark_unharvestable(self);
return;
};
if obj.rest.is_some() {
mark_unharvestable(self);
return;
}
let mut harvested: Vec<(String, String, u32)> = Vec::new();
for prop in &obj.properties {
let key_name = match &prop.key {
PropertyKey::StaticIdentifier(id) => id.name.to_string(),
PropertyKey::StringLiteral(s) => s.value.to_string(),
_ => {
mark_unharvestable(self);
return;
}
};
let Some(local) = binding_pattern_local_name(&prop.value) else {
mark_unharvestable(self);
return;
};
harvested.push((key_name, local, prop.span.start));
}
let local_refs = harvested
.iter()
.map(|(_, local, _)| local.as_str())
.collect::<Vec<_>>();
let usage = resolve_body_local_usage(body, &local_refs);
for (name, local, span_start) in harvested {
let used_in_script = usage.used.contains(local.as_str());
let used_outside_forward = usage.used_outside_forward.contains(local.as_str());
self.react_props.push(ComponentProp {
name,
local,
span_start,
used_in_script,
used_in_template: false,
component: component.to_string(),
used_outside_forward,
});
}
}
fn is_exported_binding(&self, name: &str) -> bool {
self.exports.iter().any(|export| {
export
.local_name
.as_deref()
.is_some_and(|local| local == name)
|| export.name.matches_str(name)
})
}
}
struct BodyLocalUsage {
used: FxHashSet<String>,
used_outside_forward: FxHashSet<String>,
}
fn resolve_body_local_usage(body: Option<&FunctionBody<'_>>, locals: &[&str]) -> BodyLocalUsage {
let mut usage = BodyLocalUsage {
used: FxHashSet::default(),
used_outside_forward: FxHashSet::default(),
};
let Some(body) = body else {
return usage;
};
if locals.is_empty() {
return usage;
}
let wanted: FxHashSet<&str> = locals.iter().copied().collect();
let mut visitor = BodyIdentVisitor {
wanted: &wanted,
used: &mut usage.used,
used_outside_forward: &mut usage.used_outside_forward,
attr_value_depth: 0,
};
for stmt in &body.statements {
oxc_ast_visit::Visit::visit_statement(&mut visitor, stmt);
}
usage
}
struct BodyIdentVisitor<'a, 'b> {
wanted: &'a FxHashSet<&'a str>,
used: &'b mut FxHashSet<String>,
used_outside_forward: &'b mut FxHashSet<String>,
attr_value_depth: u32,
}
impl<'a> oxc_ast_visit::Visit<'a> for BodyIdentVisitor<'_, '_> {
fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
let name = ident.name.as_str();
if self.wanted.contains(name) {
self.used.insert(name.to_string());
if self.attr_value_depth == 0 {
self.used_outside_forward.insert(name.to_string());
}
}
}
fn visit_jsx_attribute(&mut self, attr: &JSXAttribute<'a>) {
if let Some(value) = &attr.value {
self.attr_value_depth += 1;
oxc_ast_visit::walk::walk_jsx_attribute_value(self, value);
self.attr_value_depth -= 1;
}
}
}
fn is_component_name(name: &str) -> bool {
name.chars().next().is_some_and(char::is_uppercase)
}
fn classify_component_init(
init: &Expression<'_>,
) -> Option<(oxc_span::Span, ComponentFunctionKind)> {
match init {
Expression::ArrowFunctionExpression(arrow) if arrow_returns_jsx(arrow) => {
Some((arrow.span, ComponentFunctionKind::Arrow))
}
Expression::FunctionExpression(func) if function_body_returns_jsx(func.body.as_deref()) => {
Some((func.span, ComponentFunctionKind::Arrow))
}
Expression::CallExpression(call) => classify_wrapper_call(call),
Expression::ParenthesizedExpression(paren) => classify_component_init(&paren.expression),
Expression::TSAsExpression(ts_as) => classify_component_init(&ts_as.expression),
Expression::TSSatisfiesExpression(ts_sat) => classify_component_init(&ts_sat.expression),
_ => None,
}
}
fn classify_wrapper_call(
call: &CallExpression<'_>,
) -> Option<(oxc_span::Span, ComponentFunctionKind)> {
let wrapper = wrapper_callee_name(&call.callee)?;
let kind = match wrapper {
"forwardRef" => ComponentFunctionKind::ForwardRefWrapper,
"memo" => ComponentFunctionKind::MemoWrapper,
_ => return None,
};
let first = call.arguments.first()?.as_expression()?;
match first {
Expression::ArrowFunctionExpression(arrow) => Some((arrow.span, kind)),
Expression::FunctionExpression(func) => Some((func.span, kind)),
Expression::ParenthesizedExpression(paren) => match &paren.expression {
Expression::ArrowFunctionExpression(arrow) => Some((arrow.span, kind)),
Expression::FunctionExpression(func) => Some((func.span, kind)),
_ => None,
},
_ => None,
}
}
fn wrapper_callee_name<'a>(callee: &'a Expression<'_>) -> Option<&'a str> {
match callee {
Expression::Identifier(ident) => Some(ident.name.as_str()),
Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
_ => None,
}
}
fn arrow_returns_jsx(arrow: &ArrowFunctionExpression<'_>) -> bool {
if arrow.expression {
return arrow
.body
.statements
.first()
.is_some_and(|stmt| match stmt {
Statement::ExpressionStatement(expr_stmt) => {
is_jsx_expression(&expr_stmt.expression)
}
_ => false,
});
}
function_body_returns_jsx(Some(&arrow.body))
}
fn function_body_returns_jsx(body: Option<&FunctionBody<'_>>) -> bool {
let Some(body) = body else {
return false;
};
body.statements.iter().any(statement_returns_jsx)
}
fn statement_returns_jsx(stmt: &Statement<'_>) -> bool {
match stmt {
Statement::ReturnStatement(ret) => ret.argument.as_ref().is_some_and(is_jsx_expression),
Statement::IfStatement(if_stmt) => {
statement_returns_jsx(&if_stmt.consequent)
|| if_stmt
.alternate
.as_ref()
.is_some_and(statement_returns_jsx)
}
Statement::BlockStatement(block) => block.body.iter().any(statement_returns_jsx),
Statement::SwitchStatement(switch) => switch
.cases
.iter()
.any(|case| case.consequent.iter().any(statement_returns_jsx)),
Statement::TryStatement(try_stmt) => {
try_stmt.block.body.iter().any(statement_returns_jsx)
|| try_stmt
.handler
.as_ref()
.is_some_and(|h| h.body.body.iter().any(statement_returns_jsx))
|| try_stmt
.finalizer
.as_ref()
.is_some_and(|f| f.body.iter().any(statement_returns_jsx))
}
_ => false,
}
}
fn is_jsx_expression(expr: &Expression<'_>) -> bool {
match expr {
Expression::JSXElement(_) | Expression::JSXFragment(_) => true,
Expression::ParenthesizedExpression(paren) => is_jsx_expression(&paren.expression),
Expression::ConditionalExpression(cond) => {
is_jsx_expression(&cond.consequent) || is_jsx_expression(&cond.alternate)
}
Expression::LogicalExpression(logical) => is_jsx_expression(&logical.right),
Expression::TSAsExpression(ts_as) => is_jsx_expression(&ts_as.expression),
Expression::TSSatisfiesExpression(ts_sat) => is_jsx_expression(&ts_sat.expression),
Expression::TSNonNullExpression(ts_non_null) => is_jsx_expression(&ts_non_null.expression),
_ => false,
}
}
fn jsx_component_tag_name(name: &JSXElementName<'_>) -> Option<String> {
match name {
JSXElementName::Identifier(ident) => {
let n = ident.name.as_str();
is_component_name(n).then(|| n.to_string())
}
JSXElementName::IdentifierReference(ident) => {
let n = ident.name.as_str();
is_component_name(n).then(|| n.to_string())
}
JSXElementName::MemberExpression(member) => Some(jsx_member_path(member)),
JSXElementName::ThisExpression(_) | JSXElementName::NamespacedName(_) => None,
}
}
fn jsx_member_path(member: &JSXMemberExpression<'_>) -> String {
let object = match &member.object {
JSXMemberExpressionObject::IdentifierReference(ident) => ident.name.to_string(),
JSXMemberExpressionObject::MemberExpression(inner) => jsx_member_path(inner),
JSXMemberExpressionObject::ThisExpression(_) => "this".to_string(),
};
format!("{object}.{}", member.property.name)
}
fn collect_jsx_attributes(
attrs: &[JSXAttributeItem<'_>],
) -> (Vec<String>, bool, Vec<ForwardAttr>, bool) {
let mut names = Vec::new();
let mut has_spread = false;
let mut forward_attrs: Vec<ForwardAttr> = Vec::new();
let mut has_complex_forward = false;
for item in attrs {
match item {
JSXAttributeItem::Attribute(attr) => {
let attr_name = match &attr.name {
JSXAttributeName::Identifier(ident) => Some(ident.name.to_string()),
JSXAttributeName::NamespacedName(ns) => {
Some(format!("{}:{}", ns.namespace.name, ns.name.name))
}
};
if let Some(name) = &attr_name {
names.push(name.clone());
}
classify_attr_value(
attr_name.as_deref(),
attr.value.as_ref(),
&mut forward_attrs,
&mut has_complex_forward,
);
}
JSXAttributeItem::SpreadAttribute(_) => has_spread = true,
}
}
(names, has_spread, forward_attrs, has_complex_forward)
}
fn classify_attr_value(
attr_name: Option<&str>,
value: Option<&JSXAttributeValue<'_>>,
forward_attrs: &mut Vec<ForwardAttr>,
has_complex_forward: &mut bool,
) {
let Some(JSXAttributeValue::ExpressionContainer(container)) = value else {
if matches!(
value,
Some(JSXAttributeValue::Element(_) | JSXAttributeValue::Fragment(_))
) {
*has_complex_forward = true;
}
return;
};
let root = match &container.expression {
JSXExpression::Identifier(ident) => Some(ident.name.to_string()),
JSXExpression::StaticMemberExpression(_) | JSXExpression::ComputedMemberExpression(_) => {
member_expression_root(&container.expression).map(ToString::to_string)
}
JSXExpression::EmptyExpression(_) => return,
_ => {
*has_complex_forward = true;
return;
}
};
match (attr_name, root) {
(Some(attr), Some(root)) => forward_attrs.push(ForwardAttr {
attr: attr.to_string(),
root,
}),
_ => *has_complex_forward = true,
}
}
fn member_expression_root<'a>(expr: &'a JSXExpression<'a>) -> Option<&'a str> {
fn walk<'a>(expr: &'a Expression<'_>) -> Option<&'a str> {
match expr {
Expression::Identifier(ident) => Some(ident.name.as_str()),
Expression::StaticMemberExpression(member) => walk(&member.object),
Expression::ComputedMemberExpression(member) => walk(&member.object),
_ => None,
}
}
match expr {
JSXExpression::StaticMemberExpression(member) => walk(&member.object),
JSXExpression::ComputedMemberExpression(member) => walk(&member.object),
_ => None,
}
}
fn jsx_is_provider_tag(name: &JSXElementName<'_>) -> bool {
matches!(name, JSXElementName::MemberExpression(member) if member.property.name == "Provider")
}
fn jsx_has_function_render_prop(opening: &JSXOpeningElement<'_>) -> bool {
opening.attributes.iter().any(|item| {
let JSXAttributeItem::Attribute(attr) = item else {
return false;
};
let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value else {
return false;
};
matches!(
&container.expression,
JSXExpression::ArrowFunctionExpression(_) | JSXExpression::FunctionExpression(_)
)
})
}
fn jsx_children_has_function(children: &[JSXChild<'_>]) -> bool {
children.iter().any(|child| {
let JSXChild::ExpressionContainer(container) = child else {
return false;
};
matches!(
&container.expression,
JSXExpression::ArrowFunctionExpression(_) | JSXExpression::FunctionExpression(_)
)
})
}
fn is_clone_element_callee(callee: &Expression<'_>) -> bool {
match callee {
Expression::Identifier(ident) => ident.name == "cloneElement",
Expression::StaticMemberExpression(member) => member.property.name == "cloneElement",
_ => false,
}
}
fn passthrough_spread_root(first: Option<&BindingPattern<'_>>) -> Option<String> {
match first? {
BindingPattern::BindingIdentifier(id) => Some(id.name.to_string()),
BindingPattern::ObjectPattern(obj) => match &obj.rest {
Some(rest) => binding_pattern_local_name(&rest.argument),
None => None,
},
_ => None,
}
}
fn body_is_pure_passthrough(body: Option<&FunctionBody<'_>>, props_root: &str) -> bool {
let Some(body) = body else {
return false;
};
let [stmt] = body.statements.as_slice() else {
return false;
};
let returned = match stmt {
Statement::ReturnStatement(ret) => ret.argument.as_ref(),
Statement::ExpressionStatement(expr_stmt) => Some(&expr_stmt.expression),
_ => None,
};
let Some(returned) = returned else {
return false;
};
let Some(element) = unwrap_single_passthrough_element(returned) else {
return false;
};
jsx_element_is_bare_props_spread(element, props_root)
}
fn unwrap_single_passthrough_element<'a>(expr: &'a Expression<'a>) -> Option<&'a JSXElement<'a>> {
match expr {
Expression::JSXElement(element) => Some(element),
Expression::JSXFragment(fragment) => single_element_child(&fragment.children),
Expression::ParenthesizedExpression(paren) => {
unwrap_single_passthrough_element(&paren.expression)
}
Expression::TSAsExpression(ts_as) => unwrap_single_passthrough_element(&ts_as.expression),
Expression::TSSatisfiesExpression(ts_sat) => {
unwrap_single_passthrough_element(&ts_sat.expression)
}
Expression::TSNonNullExpression(ts_non_null) => {
unwrap_single_passthrough_element(&ts_non_null.expression)
}
_ => None,
}
}
fn single_element_child<'a>(children: &'a [JSXChild<'a>]) -> Option<&'a JSXElement<'a>> {
let mut found: Option<&'a JSXElement<'a>> = None;
for child in children {
match child {
JSXChild::Text(text) if text.value.trim().is_empty() => {}
JSXChild::Element(element) => {
if found.is_some() {
return None;
}
found = Some(element);
}
_ => return None,
}
}
found
}
fn jsx_element_is_bare_props_spread(element: &JSXElement<'_>, props_root: &str) -> bool {
let opening = &element.opening_element;
if jsx_component_tag_name(&opening.name).is_none() {
return false;
}
if !element.children.iter().all(jsx_child_is_whitespace) {
return false;
}
let mut has_props_spread = false;
for item in &opening.attributes {
match item {
JSXAttributeItem::SpreadAttribute(spread) => {
match spread_root_identifier(&spread.argument) {
Some(root) if root == props_root => has_props_spread = true,
_ => return false,
}
}
JSXAttributeItem::Attribute(_) => return false,
}
}
has_props_spread
}
fn jsx_child_is_whitespace(child: &JSXChild<'_>) -> bool {
matches!(child, JSXChild::Text(text) if text.value.trim().is_empty())
}
fn spread_root_identifier<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
match expr {
Expression::Identifier(ident) => Some(ident.name.as_str()),
Expression::StaticMemberExpression(member) => spread_root_identifier(&member.object),
Expression::ComputedMemberExpression(member) => spread_root_identifier(&member.object),
Expression::ParenthesizedExpression(paren) => spread_root_identifier(&paren.expression),
_ => None,
}
}
fn binding_pattern_local_name(pattern: &BindingPattern<'_>) -> Option<String> {
match pattern {
BindingPattern::BindingIdentifier(id) => Some(id.name.to_string()),
BindingPattern::AssignmentPattern(assign) => binding_pattern_local_name(&assign.left),
_ => None,
}
}
fn hook_kind(name: &str) -> Option<HookUseKind> {
match name {
"useState" => Some(HookUseKind::UseState),
"useEffect" => Some(HookUseKind::UseEffect),
"useMemo" => Some(HookUseKind::UseMemo),
"useCallback" => Some(HookUseKind::UseCallback),
_ if is_custom_hook_name(name) => Some(HookUseKind::Custom),
_ => None,
}
}
fn is_custom_hook_name(name: &str) -> bool {
name.strip_prefix("use")
.and_then(|rest| rest.chars().next())
.is_some_and(char::is_uppercase)
}
fn hook_dep_array_arity(kind: HookUseKind, args: &[Argument<'_>]) -> Option<u32> {
let dep_index = match kind {
HookUseKind::UseEffect | HookUseKind::UseMemo | HookUseKind::UseCallback => 1,
HookUseKind::UseState | HookUseKind::Custom => return None,
};
let arg = args.get(dep_index)?.as_expression()?;
let Expression::ArrayExpression(array) = arg.get_inner_expression() else {
return None;
};
u32::try_from(array.elements.len()).ok()
}