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>,
}
#[derive(Default)]
struct DefinePropsScan {
props_return_binding: Option<String>,
destructured_locals: FxHashSet<String>,
prop_aliases: FxHashMap<String, String>,
prop_names: Vec<(String, u32)>,
}
pub fn harvest_define_props(program: &Program<'_>) -> DefinePropsHarvest {
let mut harvest = DefinePropsHarvest::default();
let mut scan = DefinePropsScan::default();
for stmt in &program.body {
scan_define_props_statement(stmt, &mut scan, &mut harvest);
}
if scan.prop_names.is_empty() {
return harvest;
}
finalize_define_props(program, scan, &mut harvest);
harvest
}
fn scan_define_props_statement(
stmt: &Statement<'_>,
scan: &mut DefinePropsScan,
harvest: &mut DefinePropsHarvest,
) {
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, harvest);
}
let Some(call) = unwrap_define_props_call(init) else {
continue;
};
if scan.prop_names.is_empty() && !harvest.has_unharvestable_props {
collect_define_props_names(call, &mut scan.prop_names, harvest);
}
bind_define_props_target(
&declarator.id,
&mut scan.props_return_binding,
&mut scan.destructured_locals,
&mut scan.prop_aliases,
harvest,
);
}
}
Statement::ExpressionStatement(expr_stmt) => {
if let Expression::CallExpression(call) = &expr_stmt.expression {
inspect_macro_call(call, harvest);
if scan.prop_names.is_empty()
&& !harvest.has_unharvestable_props
&& let Some(inner) = unwrap_define_props_call(&expr_stmt.expression)
{
collect_define_props_names(inner, &mut scan.prop_names, harvest);
}
}
}
_ => {}
}
}
fn finalize_define_props(
program: &Program<'_>,
scan: DefinePropsScan,
harvest: &mut DefinePropsHarvest,
) {
let used_locals = resolve_used_locals(program, &scan.destructured_locals);
let (member_used, props_used_whole) = scan.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 scan.prop_names {
let local = scan
.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 = scan.props_return_binding;
}
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(Default)]
struct AstroPropsScan {
prop_names: Vec<(String, u32)>,
destructured_locals: FxHashSet<String>,
nested_consumed_props: FxHashSet<String>,
prop_aliases: FxHashMap<String, String>,
props_binding: Option<String>,
handled_astro_props_spans: FxHashSet<u32>,
}
pub fn harvest_astro_props(program: &Program<'_>) -> DefinePropsHarvest {
let mut harvest = DefinePropsHarvest::default();
let mut scan = AstroPropsScan::default();
for stmt in &program.body {
scan_astro_props_statement(stmt, &mut scan, &mut harvest);
}
if scan.prop_names.is_empty() {
return harvest;
}
let (member_used, whole_object_use) = {
let mut visitor = AstroPropsVisitor {
member_used: FxHashSet::default(),
whole_object_use: false,
handled_spans: &scan.handled_astro_props_spans,
};
oxc_ast_visit::Visit::visit_program(&mut visitor, program);
(visitor.member_used, visitor.whole_object_use)
};
if whole_object_use {
harvest.has_props_attrs_fallthrough = true;
}
let used_locals = resolve_used_locals(program, &scan.destructured_locals);
let (binding_member_used, binding_whole) = scan.props_binding.as_deref().map_or_else(
|| (FxHashSet::default(), false),
|binding| collect_prop_binding_usage(program, binding),
);
if binding_whole {
harvest.has_props_attrs_fallthrough = true;
}
for (name, span_start) in scan.prop_names {
let local = scan
.prop_aliases
.get(&name)
.cloned()
.unwrap_or_else(|| name.clone());
let used_in_script = used_locals.contains(&local)
|| member_used.contains(&name)
|| binding_member_used.contains(&name)
|| scan.nested_consumed_props.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
}
fn scan_astro_props_statement(
stmt: &Statement<'_>,
scan: &mut AstroPropsScan,
harvest: &mut DefinePropsHarvest,
) {
if let Some(decl) = props_interface_declaration(stmt) {
if !decl.extends.is_empty() {
harvest.has_unharvestable_props = true;
} else {
collect_ts_signature_props(&decl.body.body, &mut scan.prop_names);
}
return;
}
if let Some(alias) = props_type_alias_declaration(stmt) {
match &alias.type_annotation {
TSType::TSTypeLiteral(lit) => {
collect_ts_signature_props(&lit.members, &mut scan.prop_names);
}
_ => harvest.has_unharvestable_props = true,
}
return;
}
if let Statement::VariableDeclaration(decl) = stmt {
for declarator in &decl.declarations {
let Some(init) = &declarator.init else {
continue;
};
let Some(astro_props) = unwrap_astro_props_expr(init) else {
continue;
};
scan.handled_astro_props_spans
.insert(astro_props.span.start);
bind_astro_props_target(&declarator.id, scan, harvest);
}
}
}
fn props_interface_declaration<'a, 'b>(
stmt: &'b Statement<'a>,
) -> Option<&'b TSInterfaceDeclaration<'a>> {
let decl = match stmt {
Statement::TSInterfaceDeclaration(decl) => decl.as_ref(),
Statement::ExportNamedDeclaration(export) => match export.declaration.as_ref()? {
Declaration::TSInterfaceDeclaration(decl) => decl.as_ref(),
_ => return None,
},
_ => return None,
};
(decl.id.name.as_str() == "Props").then_some(decl)
}
fn props_type_alias_declaration<'a, 'b>(
stmt: &'b Statement<'a>,
) -> Option<&'b TSTypeAliasDeclaration<'a>> {
let alias = match stmt {
Statement::TSTypeAliasDeclaration(alias) => alias.as_ref(),
Statement::ExportNamedDeclaration(export) => match export.declaration.as_ref()? {
Declaration::TSTypeAliasDeclaration(alias) => alias.as_ref(),
_ => return None,
},
_ => return None,
};
(alias.id.name.as_str() == "Props").then_some(alias)
}
fn collect_ts_signature_props(members: &[TSSignature<'_>], prop_names: &mut Vec<(String, u32)>) {
for member in members {
if let TSSignature::TSPropertySignature(sig) = member
&& let Some(name) = property_key_name(&sig.key)
{
prop_names.push((name, sig.span.start));
}
}
}
fn is_astro_props(member: &StaticMemberExpression<'_>) -> bool {
member.property.name.as_str() == "props"
&& matches!(&member.object, Expression::Identifier(id) if id.name.as_str() == "Astro")
}
fn unwrap_astro_props_expr<'a, 'b>(
expr: &'b Expression<'a>,
) -> Option<&'b StaticMemberExpression<'a>> {
match expr {
Expression::StaticMemberExpression(member) if is_astro_props(member) => Some(member),
Expression::TSAsExpression(as_expr) => unwrap_astro_props_expr(&as_expr.expression),
Expression::TSSatisfiesExpression(sat) => unwrap_astro_props_expr(&sat.expression),
Expression::ParenthesizedExpression(paren) => unwrap_astro_props_expr(&paren.expression),
_ => None,
}
}
fn bind_astro_props_target(
id: &BindingPattern<'_>,
scan: &mut AstroPropsScan,
harvest: &mut DefinePropsHarvest,
) {
match id {
BindingPattern::BindingIdentifier(ident) => {
scan.props_binding = Some(ident.name.to_string());
}
BindingPattern::ObjectPattern(pattern) => {
for prop in &pattern.properties {
if let Some(local) = binding_local_name(&prop.value) {
scan.destructured_locals.insert(local.to_string());
if let Some(prop_name) = property_key_name(&prop.key) {
scan.prop_aliases.insert(prop_name, local.to_string());
}
} else if let Some(prop_name) = property_key_name(&prop.key) {
scan.nested_consumed_props.insert(prop_name);
} else {
harvest.has_unharvestable_props = true;
}
}
if pattern.rest.is_some() {
harvest.has_props_attrs_fallthrough = true;
}
}
BindingPattern::ArrayPattern(_) | BindingPattern::AssignmentPattern(_) => {
harvest.has_unharvestable_props = true;
}
}
}
struct AstroPropsVisitor<'h> {
member_used: FxHashSet<String>,
whole_object_use: bool,
handled_spans: &'h FxHashSet<u32>,
}
impl<'a> oxc_ast_visit::Visit<'a> for AstroPropsVisitor<'_> {
fn visit_static_member_expression(&mut self, expr: &StaticMemberExpression<'a>) {
if let Expression::StaticMemberExpression(inner) = &expr.object
&& is_astro_props(inner)
{
self.member_used.insert(expr.property.name.to_string());
return;
}
if is_astro_props(expr) {
if !self.handled_spans.contains(&expr.span.start) {
self.whole_object_use = true;
}
return;
}
oxc_ast_visit::walk::walk_static_member_expression(self, expr);
}
}
#[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 {
scan_define_emits_statement(
stmt,
&mut emit_names,
&mut emit_return_binding,
&mut harvest,
);
}
if emit_names.is_empty() {
return harvest;
}
let Some(binding) = emit_return_binding else {
harvest.has_unharvestable_emits = true;
return harvest;
};
finalize_define_emits(program, emit_names, binding, &mut harvest);
harvest
}
fn scan_define_emits_statement(
stmt: &Statement<'_>,
emit_names: &mut Vec<(String, u32)>,
emit_return_binding: &mut Option<String>,
harvest: &mut DefineEmitsHarvest,
) {
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, emit_names, 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, emit_names, harvest);
}
harvest.has_unharvestable_emits = true;
}
}
_ => {}
}
}
fn finalize_define_emits(
program: &Program<'_>,
emit_names: Vec<(String, u32)>,
binding: String,
harvest: &mut DefineEmitsHarvest,
) {
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);
}
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);
}
}
#[cfg(test)]
mod tests {
use oxc_allocator::Allocator;
use oxc_parser::Parser;
use oxc_span::SourceType;
use super::*;
fn with_ts_program<F, R>(source: &str, f: F) -> R
where
F: for<'a> FnOnce(&oxc_ast::ast::Program<'a>) -> R,
{
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, source, SourceType::ts()).parse();
f(&parser_return.program)
}
#[test]
fn define_props_runtime_object_harvests_names() {
with_ts_program(
"const props = defineProps({ foo: String, bar: Number })",
|prog| {
let h = harvest_define_props(prog);
assert!(
h.props.iter().any(|p| p.name == "foo"),
"expected foo in props"
);
assert!(
h.props.iter().any(|p| p.name == "bar"),
"expected bar in props"
);
assert!(!h.has_unharvestable_props);
},
);
}
#[test]
fn define_props_runtime_array_harvests_names() {
with_ts_program("const props = defineProps(['title', 'count'])", |prog| {
let h = harvest_define_props(prog);
assert!(h.props.iter().any(|p| p.name == "title"));
assert!(h.props.iter().any(|p| p.name == "count"));
assert!(!h.has_unharvestable_props);
});
}
#[test]
fn define_props_type_literal_harvests_names() {
with_ts_program(
"const props = defineProps<{ foo: string; bar?: number }>()",
|prog| {
let h = harvest_define_props(prog);
assert!(h.props.iter().any(|p| p.name == "foo"));
assert!(h.props.iter().any(|p| p.name == "bar"));
assert!(!h.has_unharvestable_props);
},
);
}
#[test]
fn define_props_type_reference_sets_unharvestable() {
with_ts_program("const props = defineProps<MyProps>()", |prog| {
let h = harvest_define_props(prog);
assert!(h.props.is_empty());
assert!(h.has_unharvestable_props);
});
}
#[test]
fn define_props_runtime_object_spread_sets_unharvestable() {
with_ts_program("const props = defineProps({ ...baseProps })", |prog| {
let h = harvest_define_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn define_props_runtime_non_object_arg_sets_unharvestable() {
with_ts_program("const props = defineProps(sharedProps)", |prog| {
let h = harvest_define_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn define_props_array_non_literal_element_sets_unharvestable() {
with_ts_program("const props = defineProps([computedName])", |prog| {
let h = harvest_define_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn with_defaults_unwraps_define_props() {
with_ts_program(
"const props = withDefaults(defineProps<{ size: string }>(), { size: 'md' })",
|prog| {
let h = harvest_define_props(prog);
assert!(h.props.iter().any(|p| p.name == "size"));
assert!(!h.has_unharvestable_props);
},
);
}
#[test]
fn define_model_assigned_form_sets_flag() {
with_ts_program(
"const props = defineProps(['x']); const m = defineModel()",
|prog| {
let h = harvest_define_props(prog);
assert!(h.has_define_model);
},
);
}
#[test]
fn define_expose_assigned_form_sets_flag() {
with_ts_program(
"const props = defineProps(['x']); const e = defineExpose({ count: 1 })",
|prog| {
let h = harvest_define_props(prog);
assert!(h.has_define_expose);
},
);
}
#[test]
fn unknown_macro_callee_does_not_set_flags() {
with_ts_program(
"const props = defineProps(['x']); const x = someFn()",
|prog| {
let h = harvest_define_props(prog);
assert!(!h.has_define_model);
assert!(!h.has_define_expose);
},
);
}
#[test]
fn bare_define_props_expression_statement_harvests_names() {
with_ts_program("defineProps(['alpha', 'beta'])", |prog| {
let h = harvest_define_props(prog);
assert!(h.props.iter().any(|p| p.name == "alpha"));
assert!(h.props.iter().any(|p| p.name == "beta"));
});
}
#[test]
fn non_define_props_expression_statement_ignored() {
with_ts_program("console.log('hello')", |prog| {
let h = harvest_define_props(prog);
assert!(h.props.is_empty());
assert!(!h.has_unharvestable_props);
});
}
#[test]
fn destructure_rest_element_sets_fallthrough() {
with_ts_program("const { foo, ...rest } = defineProps(['foo'])", |prog| {
let h = harvest_define_props(prog);
assert!(h.has_props_attrs_fallthrough);
});
}
#[test]
fn destructure_plain_props_no_fallthrough() {
with_ts_program("const { foo } = defineProps(['foo'])", |prog| {
let h = harvest_define_props(prog);
assert!(!h.has_props_attrs_fallthrough);
});
}
#[test]
fn props_binding_member_access_marks_used_in_script() {
with_ts_program(
"
const props = defineProps(['label', 'disabled'])
console.log(props.label)
",
|prog| {
let h = harvest_define_props(prog);
let label = h.props.iter().find(|p| p.name == "label");
let disabled = h.props.iter().find(|p| p.name == "disabled");
assert!(
label.is_some_and(|p| p.used_in_script),
"label should be used"
);
assert!(
disabled.is_some_and(|p| !p.used_in_script),
"disabled should be unused"
);
},
);
}
#[test]
fn props_binding_whole_object_use_sets_fallthrough() {
with_ts_program(
"
const props = defineProps(['x'])
return props
",
|prog| {
let h = harvest_define_props(prog);
assert!(h.has_props_attrs_fallthrough);
},
);
}
#[test]
fn props_whole_object_use_sets_fallthrough_via_to_refs() {
with_ts_program(
"
const props = defineProps(['a'])
const r = toRefs(props)
",
|prog| {
let h = harvest_define_props(prog);
assert!(
h.has_props_attrs_fallthrough,
"toRefs(props) is a whole-object use"
);
},
);
}
#[test]
fn destructure_alias_prop_used_via_local() {
with_ts_program(
"
const { label: myLabel } = defineProps(['label'])
console.log(myLabel)
",
|prog| {
let h = harvest_define_props(prog);
let prop = h
.props
.iter()
.find(|p| p.name == "label")
.expect("label prop");
assert_eq!(prop.local, "myLabel");
assert!(prop.used_in_script);
},
);
}
#[test]
fn define_emits_runtime_array_harvests_events() {
with_ts_program("const emit = defineEmits(['save', 'cancel'])", |prog| {
let h = harvest_define_emits(prog);
assert!(h.emits.iter().any(|e| e.name == "save"));
assert!(h.emits.iter().any(|e| e.name == "cancel"));
assert!(!h.has_unharvestable_emits);
});
}
#[test]
fn define_emits_marks_used_event_called_with_string_literal() {
with_ts_program(
"
const emit = defineEmits(['save', 'cancel'])
emit('save')
",
|prog| {
let h = harvest_define_emits(prog);
let save = h.emits.iter().find(|e| e.name == "save");
let cancel = h.emits.iter().find(|e| e.name == "cancel");
assert!(save.is_some_and(|e| e.used), "save should be used");
assert!(cancel.is_some_and(|e| !e.used), "cancel should be unused");
},
);
}
#[test]
fn define_emits_type_literal_tuple_form_harvests_events() {
with_ts_program(
"const emit = defineEmits<{ (e: 'click'): void; (e: 'change', val: string): void }>()",
|prog| {
let h = harvest_define_emits(prog);
assert!(h.emits.iter().any(|e| e.name == "click"));
assert!(h.emits.iter().any(|e| e.name == "change"));
assert!(!h.has_unharvestable_emits);
},
);
}
#[test]
fn define_emits_type_object_form_harvests_events() {
with_ts_program(
"const emit = defineEmits<{ update: [val: string]; reset: [] }>()",
|prog| {
let h = harvest_define_emits(prog);
assert!(h.emits.iter().any(|e| e.name == "update"));
assert!(h.emits.iter().any(|e| e.name == "reset"));
assert!(!h.has_unharvestable_emits);
},
);
}
#[test]
fn define_emits_type_reference_sets_unharvestable() {
with_ts_program("const emit = defineEmits<MyEmits>()", |prog| {
let h = harvest_define_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn define_emits_runtime_non_array_arg_sets_unharvestable() {
with_ts_program("const emit = defineEmits(sharedEmits)", |prog| {
let h = harvest_define_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn define_emits_non_string_array_element_sets_unharvestable() {
with_ts_program("const emit = defineEmits([computedEvent])", |prog| {
let h = harvest_define_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn define_emits_no_binding_sets_unharvestable() {
with_ts_program("defineEmits(['save'])", |prog| {
let h = harvest_define_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn define_emits_destructured_binding_sets_unharvestable() {
with_ts_program("const { save } = defineEmits(['save'])", |prog| {
let h = harvest_define_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn define_emits_dynamic_call_sets_has_dynamic_emit() {
with_ts_program(
"
const emit = defineEmits(['save'])
emit(eventName)
",
|prog| {
let h = harvest_define_emits(prog);
assert!(h.has_dynamic_emit);
},
);
}
#[test]
fn define_emits_whole_object_use_sets_flag() {
with_ts_program(
"
const emit = defineEmits(['save'])
someWrapper(emit)
",
|prog| {
let h = harvest_define_emits(prog);
assert!(h.has_emit_whole_object_use);
},
);
}
#[test]
fn options_api_props_object_form_harvests_names() {
with_ts_program(
"export default { props: { title: String, count: Number } }",
|prog| {
let h = harvest_options_api_props(prog);
assert!(h.props.iter().any(|p| p.name == "title"));
assert!(h.props.iter().any(|p| p.name == "count"));
assert!(!h.has_unharvestable_props);
},
);
}
#[test]
fn options_api_props_array_form_harvests_names() {
with_ts_program("export default { props: ['label', 'disabled'] }", |prog| {
let h = harvest_options_api_props(prog);
assert!(h.props.iter().any(|p| p.name == "label"));
assert!(h.props.iter().any(|p| p.name == "disabled"));
assert!(!h.has_unharvestable_props);
});
}
#[test]
fn options_api_props_identifier_sets_unharvestable() {
with_ts_program("export default { props: sharedProps }", |prog| {
let h = harvest_options_api_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn options_api_props_spread_in_object_sets_unharvestable() {
with_ts_program("export default { props: { ...base } }", |prog| {
let h = harvest_options_api_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn options_api_props_marks_used_via_this() {
with_ts_program(
"
export default {
props: { title: String, count: Number },
mounted() { console.log(this.title) }
}
",
|prog| {
let h = harvest_options_api_props(prog);
let title = h.props.iter().find(|p| p.name == "title");
let count = h.props.iter().find(|p| p.name == "count");
assert!(
title.is_some_and(|p| p.used_in_script),
"title should be used"
);
assert!(
count.is_some_and(|p| !p.used_in_script),
"count should be unused"
);
},
);
}
#[test]
fn options_api_define_component_harvests_props() {
with_ts_program(
"export default defineComponent({ props: ['name'] })",
|prog| {
let h = harvest_options_api_props(prog);
assert!(h.props.iter().any(|p| p.name == "name"));
},
);
}
#[test]
fn options_api_define_component_type_generic_sets_unharvestable() {
with_ts_program("export default defineComponent<MyProps>()", |prog| {
let h = harvest_options_api_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn options_api_mixin_sets_unharvestable() {
with_ts_program(
"export default { mixins: [BaseMixin], props: ['x'] }",
|prog| {
let h = harvest_options_api_props(prog);
assert!(h.has_unharvestable_props);
},
);
}
#[test]
fn options_api_extends_sets_unharvestable() {
with_ts_program(
"export default { extends: BaseComponent, props: ['x'] }",
|prog| {
let h = harvest_options_api_props(prog);
assert!(h.has_unharvestable_props);
},
);
}
#[test]
fn options_api_setup_method_sets_fallthrough() {
with_ts_program(
"export default { props: ['x'], setup(props) { return {} } }",
|prog| {
let h = harvest_options_api_props(prog);
assert!(h.has_props_attrs_fallthrough);
},
);
}
#[test]
fn options_api_dynamic_this_access_sets_fallthrough() {
with_ts_program(
"
export default {
props: ['x'],
mounted() { const k = 'x'; return this[k] }
}
",
|prog| {
let h = harvest_options_api_props(prog);
assert!(h.has_props_attrs_fallthrough);
},
);
}
#[test]
fn options_api_emits_array_form_harvests_events() {
with_ts_program("export default { emits: ['save', 'cancel'] }", |prog| {
let h = harvest_options_api_emits(prog);
assert!(h.emits.iter().any(|e| e.name == "save"));
assert!(h.emits.iter().any(|e| e.name == "cancel"));
assert!(!h.has_unharvestable_emits);
});
}
#[test]
fn options_api_emits_object_form_harvests_events() {
with_ts_program(
"export default { emits: { save: null, cancel: null } }",
|prog| {
let h = harvest_options_api_emits(prog);
assert!(h.emits.iter().any(|e| e.name == "save"));
assert!(h.emits.iter().any(|e| e.name == "cancel"));
},
);
}
#[test]
fn options_api_emits_marks_used_via_this_emit() {
with_ts_program(
"
export default {
emits: ['save', 'cancel'],
methods: { onSave() { this.$emit('save') } }
}
",
|prog| {
let h = harvest_options_api_emits(prog);
let save = h.emits.iter().find(|e| e.name == "save");
let cancel = h.emits.iter().find(|e| e.name == "cancel");
assert!(save.is_some_and(|e| e.used), "save should be used");
assert!(cancel.is_some_and(|e| !e.used), "cancel should be unused");
},
);
}
#[test]
fn options_api_emits_dynamic_this_emit_sets_has_dynamic_emit() {
with_ts_program(
"
export default {
emits: ['save'],
methods: { onSave() { this.$emit(this.eventName) } }
}
",
|prog| {
let h = harvest_options_api_emits(prog);
assert!(h.has_dynamic_emit);
},
);
}
#[test]
fn options_api_emits_identifier_value_sets_unharvestable() {
with_ts_program("export default { emits: sharedEmits }", |prog| {
let h = harvest_options_api_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn options_api_emits_spread_in_object_sets_unharvestable() {
with_ts_program("export default { emits: { ...base } }", |prog| {
let h = harvest_options_api_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn options_api_emits_non_string_array_element_sets_unharvestable() {
with_ts_program("export default { emits: [dynamicEvent] }", |prog| {
let h = harvest_options_api_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn options_api_emits_mixin_sets_unharvestable() {
with_ts_program(
"export default { mixins: [Base], emits: ['save'] }",
|prog| {
let h = harvest_options_api_emits(prog);
assert!(h.has_unharvestable_emits);
},
);
}
#[test]
fn options_api_emits_setup_sets_dynamic_emit_flag() {
with_ts_program(
"export default { emits: ['save'], setup(props, ctx) { ctx.emit('save') } }",
|prog| {
let h = harvest_options_api_emits(prog);
assert!(h.has_dynamic_emit);
},
);
}
#[test]
fn options_api_define_component_type_generic_sets_unharvestable_emits() {
with_ts_program("export default defineComponent<MyOpts>()", |prog| {
let h = harvest_options_api_emits(prog);
assert!(h.has_unharvestable_emits);
});
}
#[test]
fn svelte_props_object_destructure_harvests_names() {
with_ts_program("let { label, count } = $props()", |prog| {
let h = harvest_svelte_props(prog);
assert!(h.props.iter().any(|p| p.name == "label"));
assert!(h.props.iter().any(|p| p.name == "count"));
assert!(!h.has_unharvestable_props);
});
}
#[test]
fn svelte_props_bare_identifier_sets_unharvestable() {
with_ts_program("let p = $props()", |prog| {
let h = harvest_svelte_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn svelte_props_rest_element_sets_fallthrough() {
with_ts_program("let { label, ...rest } = $props()", |prog| {
let h = harvest_svelte_props(prog);
assert!(h.has_props_attrs_fallthrough);
});
}
#[test]
fn svelte_props_used_in_script_via_local() {
with_ts_program(
"
let { label, count } = $props()
console.log(label)
",
|prog| {
let h = harvest_svelte_props(prog);
let label_prop = h.props.iter().find(|p| p.name == "label");
let count_prop = h.props.iter().find(|p| p.name == "count");
assert!(
label_prop.is_some_and(|p| p.used_in_script),
"label should be used"
);
assert!(
count_prop.is_some_and(|p| !p.used_in_script),
"count should be unused"
);
},
);
}
#[test]
fn svelte_props_renamed_alias_stored_correctly() {
with_ts_program("let { title: myTitle } = $props()", |prog| {
let h = harvest_svelte_props(prog);
let prop = h
.props
.iter()
.find(|p| p.name == "title")
.expect("title prop");
assert_eq!(prop.local, "myTitle");
});
}
#[test]
fn svelte_props_no_dollar_props_call_returns_empty() {
with_ts_program("let x = someOtherFn()", |prog| {
let h = harvest_svelte_props(prog);
assert!(h.props.is_empty());
assert!(!h.has_unharvestable_props);
});
}
#[test]
fn svelte_props_array_pattern_sets_unharvestable() {
with_ts_program("let [a, b] = $props()", |prog| {
let h = harvest_svelte_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn svelte_props_nested_object_destructure_sets_unharvestable() {
with_ts_program("let { a: { x } } = $props()", |prog| {
let h = harvest_svelte_props(prog);
assert!(h.has_unharvestable_props);
});
}
#[test]
fn options_without_setup_no_fallthrough() {
with_ts_program("export default { props: ['x'], mounted() {} }", |prog| {
let h = harvest_options_api_props(prog);
assert!(!h.has_props_attrs_fallthrough);
});
}
#[test]
fn no_export_default_returns_empty_options_props() {
with_ts_program("const x = 1", |prog| {
let h = harvest_options_api_props(prog);
assert!(h.props.is_empty());
assert!(!h.has_unharvestable_props);
});
}
#[test]
fn no_export_default_returns_empty_options_emits() {
with_ts_program("const x = 1", |prog| {
let h = harvest_options_api_emits(prog);
assert!(h.emits.is_empty());
assert!(!h.has_unharvestable_emits);
});
}
}