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
}
pub fn harvest_svelte_props(program: &Program<'_>) -> DefinePropsHarvest {
let mut harvest = DefinePropsHarvest::default();
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 {
let Statement::VariableDeclaration(decl) = stmt else {
continue;
};
for declarator in &decl.declarations {
let Some(init) = &declarator.init else {
continue;
};
if !is_props_rune_call(init) {
continue;
}
bind_svelte_props_target(
&declarator.id,
&mut destructured_locals,
&mut prop_aliases,
&mut prop_names,
&mut harvest,
);
}
}
if prop_names.is_empty() {
return harvest;
}
let used_locals = resolve_used_locals(program, &destructured_locals);
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);
harvest.props.push(ComponentProp {
name,
local,
span_start,
used_in_script,
used_in_template: false,
component: String::new(),
used_outside_forward: false,
});
}
harvest
}
fn is_props_rune_call(expr: &Expression<'_>) -> bool {
let Expression::CallExpression(call) = expr else {
return false;
};
simple_callee_name(&call.callee) == Some("$props")
}
fn bind_svelte_props_target(
id: &BindingPattern<'_>,
destructured_locals: &mut FxHashSet<String>,
prop_aliases: &mut FxHashMap<String, String>,
prop_names: &mut Vec<(String, u32)>,
harvest: &mut DefinePropsHarvest,
) {
match id {
BindingPattern::BindingIdentifier(_) => {
harvest.has_unharvestable_props = true;
}
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_names.push((prop_name.clone(), prop.span.start));
prop_aliases.insert(prop_name, local.to_string());
} else {
harvest.has_unharvestable_props = true;
}
} else {
harvest.has_unharvestable_props = true;
}
}
if pattern.rest.is_some() {
harvest.has_props_attrs_fallthrough = true;
}
}
_ => harvest.has_unharvestable_props = true,
}
}
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;
}
}
}
fn find_options_object<'a, 'b>(
program: &'b Program<'a>,
has_type_generic: &mut bool,
) -> Option<&'b ObjectExpression<'a>> {
for stmt in &program.body {
let Statement::ExportDefaultDeclaration(export) = stmt else {
continue;
};
let Some(expr) = export.declaration.as_expression() else {
continue;
};
match expr {
Expression::ObjectExpression(obj) => return Some(obj),
Expression::CallExpression(call) => {
if simple_callee_name(&call.callee) != Some("defineComponent") {
return None;
}
if call.type_arguments.is_some()
&& !call
.arguments
.first()
.and_then(|arg| arg.as_expression())
.is_some_and(|e| matches!(e, Expression::ObjectExpression(_)))
{
*has_type_generic = true;
return None;
}
if let Some(Expression::ObjectExpression(obj)) =
call.arguments.first().and_then(|arg| arg.as_expression())
{
return Some(obj);
}
return None;
}
_ => return None,
}
}
None
}
fn options_property_value<'a, 'b>(
obj: &'b ObjectExpression<'a>,
key: &str,
) -> Option<&'b Expression<'a>> {
for prop in &obj.properties {
if let ObjectPropertyKind::ObjectProperty(p) = prop
&& property_key_name(&p.key).as_deref() == Some(key)
{
return Some(&p.value);
}
}
None
}
fn options_has_mixin_or_extends(obj: &ObjectExpression<'_>) -> bool {
obj.properties.iter().any(|prop| {
matches!(
prop,
ObjectPropertyKind::ObjectProperty(p)
if matches!(property_key_name(&p.key).as_deref(), Some("mixins" | "extends"))
)
})
}
fn options_has_setup_method(obj: &ObjectExpression<'_>) -> bool {
obj.properties.iter().any(|prop| match prop {
ObjectPropertyKind::ObjectProperty(p) => {
property_key_name(&p.key).as_deref() == Some("setup")
}
ObjectPropertyKind::SpreadProperty(_) => false,
})
}
pub fn harvest_options_api_props(program: &Program<'_>) -> DefinePropsHarvest {
let mut harvest = DefinePropsHarvest::default();
let mut has_type_generic = false;
let Some(obj) = find_options_object(program, &mut has_type_generic) else {
if has_type_generic {
harvest.has_unharvestable_props = true;
}
return harvest;
};
if options_has_mixin_or_extends(obj) {
harvest.has_unharvestable_props = true;
}
if options_has_setup_method(obj) {
harvest.has_props_attrs_fallthrough = true;
}
let mut prop_names: Vec<(String, u32)> = Vec::new();
if let Some(props_value) = options_property_value(obj, "props") {
collect_options_prop_names(props_value, &mut prop_names, &mut harvest);
}
if prop_names.is_empty() {
return harvest;
}
let usage = collect_this_member_usage(program);
if usage.has_dynamic_this {
harvest.has_props_attrs_fallthrough = true;
}
for (name, span_start) in prop_names {
let used_in_script = usage.read.contains(&name);
harvest.props.push(ComponentProp {
name: name.clone(),
local: name,
span_start,
used_in_script,
used_in_template: false,
component: String::new(),
used_outside_forward: false,
});
}
harvest
}
fn collect_options_prop_names(
value: &Expression<'_>,
prop_names: &mut Vec<(String, u32)>,
harvest: &mut DefinePropsHarvest,
) {
match value {
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));
} else {
harvest.has_unharvestable_props = true;
}
}
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,
}
}
pub fn harvest_options_api_emits(program: &Program<'_>) -> DefineEmitsHarvest {
let mut harvest = DefineEmitsHarvest::default();
let mut has_type_generic = false;
let Some(obj) = find_options_object(program, &mut has_type_generic) else {
if has_type_generic {
harvest.has_unharvestable_emits = true;
}
return harvest;
};
if options_has_mixin_or_extends(obj) {
harvest.has_unharvestable_emits = true;
}
if options_has_setup_method(obj) {
harvest.has_dynamic_emit = true;
}
let mut emit_names: Vec<(String, u32)> = Vec::new();
if let Some(emits_value) = options_property_value(obj, "emits") {
collect_options_emit_names(emits_value, &mut emit_names, &mut harvest);
}
if emit_names.is_empty() {
return harvest;
}
let usage = collect_this_member_usage(program);
if usage.has_dynamic_emit {
harvest.has_dynamic_emit = true;
}
for (name, span_start) in emit_names {
let used = usage.emitted.contains(&name);
harvest.emits.push(ComponentEmit {
name,
span_start,
used,
});
}
harvest
}
fn collect_options_emit_names(
value: &Expression<'_>,
emit_names: &mut Vec<(String, u32)>,
harvest: &mut DefineEmitsHarvest,
) {
match value {
Expression::ObjectExpression(obj) => {
for prop in &obj.properties {
match prop {
ObjectPropertyKind::ObjectProperty(p) => {
if let Some(name) = property_key_name(&p.key) {
emit_names.push((name, p.span.start));
} else {
harvest.has_unharvestable_emits = true;
}
}
ObjectPropertyKind::SpreadProperty(_) => {
harvest.has_unharvestable_emits = true;
}
}
}
}
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,
}
}
#[derive(Debug, Default)]
struct ThisMemberUsage {
read: FxHashSet<String>,
emitted: FxHashSet<String>,
has_dynamic_this: bool,
has_dynamic_emit: bool,
}
fn collect_this_member_usage(program: &Program<'_>) -> ThisMemberUsage {
let mut visitor = ThisMemberVisitor {
usage: ThisMemberUsage::default(),
};
oxc_ast_visit::Visit::visit_program(&mut visitor, program);
visitor.usage
}
struct ThisMemberVisitor {
usage: ThisMemberUsage,
}
impl<'a> oxc_ast_visit::Visit<'a> for ThisMemberVisitor {
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
if let Expression::StaticMemberExpression(member) = &call.callee
&& matches!(member.object, Expression::ThisExpression(_))
&& member.property.name.as_str() == "$emit"
{
match call.arguments.first().and_then(|arg| arg.as_expression()) {
Some(Expression::StringLiteral(lit)) => {
self.usage.emitted.insert(lit.value.to_string());
}
_ => self.usage.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_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
if matches!(member.object, Expression::ThisExpression(_)) {
self.usage.read.insert(member.property.name.to_string());
}
oxc_ast_visit::walk::walk_static_member_expression(self, member);
}
fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
if matches!(member.object, Expression::ThisExpression(_)) {
self.usage.has_dynamic_this = true;
}
oxc_ast_visit::walk::walk_computed_member_expression(self, member);
}
}