use oxc_ast::ast::*;
use oxc_semantic::SemanticBuilder;
use rustc_hash::{FxHashMap, FxHashSet};
use fallow_types::extract::{ComponentEmit, ComponentProp};
#[derive(Debug, Default)]
pub struct DefinePropsHarvest {
pub props: Vec<ComponentProp>,
pub has_unharvestable_props: bool,
pub has_props_attrs_fallthrough: bool,
pub has_define_expose: bool,
pub has_define_model: bool,
pub props_return_binding: Option<String>,
}
pub fn harvest_define_props(program: &Program<'_>) -> DefinePropsHarvest {
let mut harvest = DefinePropsHarvest::default();
let mut props_return_binding: Option<String> = None;
let mut destructured_locals: FxHashSet<String> = FxHashSet::default();
let mut prop_aliases: FxHashMap<String, String> = FxHashMap::default();
let mut prop_names: Vec<(String, u32)> = Vec::new();
for stmt in &program.body {
match stmt {
Statement::VariableDeclaration(decl) => {
for declarator in &decl.declarations {
let Some(init) = &declarator.init else {
continue;
};
if let Expression::CallExpression(call) = init {
inspect_macro_call(call, &mut harvest);
}
let Some(call) = unwrap_define_props_call(init) else {
continue;
};
if prop_names.is_empty() && !harvest.has_unharvestable_props {
collect_define_props_names(call, &mut prop_names, &mut harvest);
}
bind_define_props_target(
&declarator.id,
&mut props_return_binding,
&mut destructured_locals,
&mut prop_aliases,
&mut harvest,
);
}
}
Statement::ExpressionStatement(expr_stmt) => {
if let Expression::CallExpression(call) = &expr_stmt.expression {
inspect_macro_call(call, &mut harvest);
if prop_names.is_empty()
&& !harvest.has_unharvestable_props
&& let Some(inner) = unwrap_define_props_call(&expr_stmt.expression)
{
collect_define_props_names(inner, &mut prop_names, &mut harvest);
}
}
}
_ => {}
}
}
if prop_names.is_empty() {
return harvest;
}
let used_locals = resolve_used_locals(program, &destructured_locals);
let (member_used, props_used_whole) = props_return_binding.as_deref().map_or_else(
|| (FxHashSet::default(), false),
|binding| collect_prop_binding_usage(program, binding),
);
if props_used_whole {
harvest.has_props_attrs_fallthrough = true;
}
for (name, span_start) in prop_names {
let local = prop_aliases
.get(&name)
.cloned()
.unwrap_or_else(|| name.clone());
let used_in_script = used_locals.contains(&local) || member_used.contains(&name);
harvest.props.push(ComponentProp {
name,
local,
span_start,
used_in_script,
used_in_template: false,
component: String::new(),
used_outside_forward: false,
});
}
harvest.props_return_binding = props_return_binding;
harvest
}
fn unwrap_define_props_call<'a, 'b>(expr: &'b Expression<'a>) -> Option<&'b CallExpression<'a>> {
let Expression::CallExpression(call) = expr else {
return None;
};
let callee_name = simple_callee_name(&call.callee)?;
if callee_name == "defineProps" {
return Some(call);
}
if callee_name == "withDefaults" {
let first = call.arguments.first()?.as_expression()?;
return unwrap_define_props_call(first);
}
None
}
fn simple_callee_name<'a>(callee: &Expression<'a>) -> Option<&'a str> {
match callee {
Expression::Identifier(ident) => Some(ident.name.as_str()),
_ => None,
}
}
fn inspect_macro_call(call: &CallExpression<'_>, harvest: &mut DefinePropsHarvest) {
if let Some(name) = simple_callee_name(&call.callee) {
match name {
"defineExpose" => harvest.has_define_expose = true,
"defineModel" => harvest.has_define_model = true,
_ => {}
}
}
}
fn collect_define_props_names(
call: &CallExpression<'_>,
prop_names: &mut Vec<(String, u32)>,
harvest: &mut DefinePropsHarvest,
) {
if let Some(type_args) = &call.type_arguments {
if let Some(first) = type_args.params.first() {
match first {
TSType::TSTypeLiteral(lit) => {
for member in &lit.members {
if let TSSignature::TSPropertySignature(sig) = member
&& let Some(name) = property_key_name(&sig.key)
{
prop_names.push((name, sig.span.start));
}
}
}
_ => harvest.has_unharvestable_props = true,
}
}
return;
}
if let Some(first) = call.arguments.first().and_then(|arg| arg.as_expression()) {
match first {
Expression::ObjectExpression(obj) => {
for prop in &obj.properties {
match prop {
ObjectPropertyKind::ObjectProperty(p) => {
if let Some(name) = property_key_name(&p.key) {
prop_names.push((name, p.span.start));
}
}
ObjectPropertyKind::SpreadProperty(_) => {
harvest.has_unharvestable_props = true;
}
}
}
}
Expression::ArrayExpression(arr) => {
for element in &arr.elements {
if let ArrayExpressionElement::StringLiteral(lit) = element {
prop_names.push((lit.value.to_string(), lit.span.start));
} else if !matches!(element, ArrayExpressionElement::Elision(_)) {
harvest.has_unharvestable_props = true;
}
}
}
_ => harvest.has_unharvestable_props = true,
}
}
}
fn property_key_name(key: &PropertyKey<'_>) -> Option<String> {
key.static_name().map(|name| name.to_string())
}
fn binding_local_name<'a>(pattern: &'a BindingPattern<'a>) -> Option<&'a str> {
match pattern {
BindingPattern::BindingIdentifier(ident) => Some(ident.name.as_str()),
BindingPattern::AssignmentPattern(assign) => binding_local_name(&assign.left),
_ => None,
}
}
fn bind_define_props_target(
id: &BindingPattern<'_>,
props_return_binding: &mut Option<String>,
destructured_locals: &mut FxHashSet<String>,
prop_aliases: &mut FxHashMap<String, String>,
harvest: &mut DefinePropsHarvest,
) {
match id {
BindingPattern::BindingIdentifier(ident) => {
*props_return_binding = Some(ident.name.to_string());
}
BindingPattern::ObjectPattern(pattern) => {
for prop in &pattern.properties {
if let Some(local) = binding_local_name(&prop.value) {
destructured_locals.insert(local.to_string());
if let Some(prop_name) = property_key_name(&prop.key) {
prop_aliases.insert(prop_name, local.to_string());
}
}
}
if pattern.rest.is_some() {
harvest.has_props_attrs_fallthrough = true;
}
}
_ => {}
}
}
fn resolve_used_locals(
program: &Program<'_>,
destructured_locals: &FxHashSet<String>,
) -> FxHashSet<String> {
let mut used: FxHashSet<String> = FxHashSet::default();
if destructured_locals.is_empty() {
return used;
}
let semantic_ret = SemanticBuilder::new().build(program);
let scoping = semantic_ret.semantic.scoping();
let root_scope = scoping.root_scope_id();
for local in destructured_locals {
let name = oxc_str::Ident::from(local.as_str());
if let Some(symbol_id) = scoping.get_binding(root_scope, name)
&& scoping.get_resolved_references(symbol_id).next().is_some()
{
used.insert(local.clone());
}
}
used
}
fn collect_prop_binding_usage(program: &Program<'_>, binding: &str) -> (FxHashSet<String>, bool) {
let mut visitor = PropBindingVisitor {
binding,
accessed: FxHashSet::default(),
used_whole: false,
};
oxc_ast_visit::Visit::visit_program(&mut visitor, program);
(visitor.accessed, visitor.used_whole)
}
struct PropBindingVisitor<'a> {
binding: &'a str,
accessed: FxHashSet<String>,
used_whole: bool,
}
impl<'a> oxc_ast_visit::Visit<'a> for PropBindingVisitor<'a> {
fn visit_static_member_expression(&mut self, expr: &StaticMemberExpression<'a>) {
if let Expression::Identifier(ident) = &expr.object
&& ident.name.as_str() == self.binding
{
self.accessed.insert(expr.property.name.to_string());
return;
}
oxc_ast_visit::walk::walk_static_member_expression(self, expr);
}
fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
if ident.name.as_str() == self.binding {
self.used_whole = true;
}
}
}
#[derive(Debug, Default)]
pub struct DefineEmitsHarvest {
pub emits: Vec<ComponentEmit>,
pub has_unharvestable_emits: bool,
pub has_dynamic_emit: bool,
pub has_emit_whole_object_use: bool,
pub emit_binding: Option<String>,
}
pub fn harvest_define_emits(program: &Program<'_>) -> DefineEmitsHarvest {
let mut harvest = DefineEmitsHarvest::default();
let mut emit_return_binding: Option<String> = None;
let mut emit_names: Vec<(String, u32)> = Vec::new();
for stmt in &program.body {
match stmt {
Statement::VariableDeclaration(decl) => {
for declarator in &decl.declarations {
let Some(init) = &declarator.init else {
continue;
};
let Some(call) = unwrap_define_emits_call(init) else {
continue;
};
if emit_names.is_empty() && !harvest.has_unharvestable_emits {
collect_define_emits_names(call, &mut emit_names, &mut harvest);
}
if let BindingPattern::BindingIdentifier(ident) = &declarator.id {
emit_return_binding = Some(ident.name.to_string());
} else {
harvest.has_unharvestable_emits = true;
}
}
}
Statement::ExpressionStatement(expr_stmt) => {
if let Some(call) = unwrap_define_emits_call(&expr_stmt.expression) {
if emit_names.is_empty() && !harvest.has_unharvestable_emits {
collect_define_emits_names(call, &mut emit_names, &mut harvest);
}
harvest.has_unharvestable_emits = true;
}
}
_ => {}
}
}
if emit_names.is_empty() {
return harvest;
}
let Some(binding) = emit_return_binding else {
harvest.has_unharvestable_emits = true;
return harvest;
};
let mut visitor = EmitBindingVisitor {
binding: &binding,
emitted: FxHashSet::default(),
has_dynamic_emit: false,
used_whole: false,
};
oxc_ast_visit::Visit::visit_program(&mut visitor, program);
if visitor.has_dynamic_emit {
harvest.has_dynamic_emit = true;
}
if visitor.used_whole {
harvest.has_emit_whole_object_use = true;
}
for (name, span_start) in emit_names {
let used = visitor.emitted.contains(&name);
harvest.emits.push(ComponentEmit {
name,
span_start,
used,
});
}
harvest.emit_binding = Some(binding);
harvest
}
fn unwrap_define_emits_call<'a, 'b>(expr: &'b Expression<'a>) -> Option<&'b CallExpression<'a>> {
let Expression::CallExpression(call) = expr else {
return None;
};
let callee_name = simple_callee_name(&call.callee)?;
if callee_name == "defineEmits" {
return Some(call);
}
None
}
fn collect_define_emits_names(
call: &CallExpression<'_>,
emit_names: &mut Vec<(String, u32)>,
harvest: &mut DefineEmitsHarvest,
) {
if let Some(type_args) = &call.type_arguments {
if let Some(first) = type_args.params.first() {
match first {
TSType::TSTypeLiteral(lit) => {
for member in &lit.members {
match member {
TSSignature::TSCallSignatureDeclaration(sig) => {
if let Some((name, span_start)) = call_signature_event_name(sig) {
emit_names.push((name, span_start));
} else {
harvest.has_unharvestable_emits = true;
}
}
TSSignature::TSPropertySignature(sig) => {
if let Some(name) = property_key_name(&sig.key) {
emit_names.push((name, sig.span.start));
}
}
_ => harvest.has_unharvestable_emits = true,
}
}
}
_ => harvest.has_unharvestable_emits = true,
}
}
return;
}
if let Some(first) = call.arguments.first().and_then(|arg| arg.as_expression()) {
match first {
Expression::ArrayExpression(arr) => {
for element in &arr.elements {
if let ArrayExpressionElement::StringLiteral(lit) = element {
emit_names.push((lit.value.to_string(), lit.span.start));
} else if !matches!(element, ArrayExpressionElement::Elision(_)) {
harvest.has_unharvestable_emits = true;
}
}
}
_ => harvest.has_unharvestable_emits = true,
}
}
}
fn call_signature_event_name(sig: &TSCallSignatureDeclaration<'_>) -> Option<(String, u32)> {
let first = sig.params.items.first()?;
let type_annotation = first.type_annotation.as_deref()?;
if let TSType::TSLiteralType(lit) = &type_annotation.type_annotation
&& let TSLiteral::StringLiteral(str_lit) = &lit.literal
{
return Some((str_lit.value.to_string(), sig.span.start));
}
None
}
struct EmitBindingVisitor<'a> {
binding: &'a str,
emitted: FxHashSet<String>,
has_dynamic_emit: bool,
used_whole: bool,
}
impl<'a> oxc_ast_visit::Visit<'a> for EmitBindingVisitor<'a> {
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
if let Expression::Identifier(ident) = &call.callee
&& ident.name.as_str() == self.binding
{
match call.arguments.first().and_then(|arg| arg.as_expression()) {
Some(Expression::StringLiteral(lit)) => {
self.emitted.insert(lit.value.to_string());
}
_ => self.has_dynamic_emit = true,
}
for arg in &call.arguments {
if let Some(expr) = arg.as_expression() {
self.visit_expression(expr);
}
}
return;
}
oxc_ast_visit::walk::walk_call_expression(self, call);
}
fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
if ident.name.as_str() == self.binding {
self.used_whole = true;
}
}
}