use oxc_allocator::Allocator;
use oxc_ast::ast::BindingPattern;
use oxc_parser::Parser;
use oxc_span::SourceType;
use vize_carton::{String, is_builtin_directive};
use crate::errors::ErrorCode;
use crate::lane::TransformContext;
use crate::{
DirectiveNode, ElementNode, ElementType, ExpressionNode, PropNode, RuntimeHelper,
SourceLocation, TemplateChildNode,
};
pub fn has_v_slot(el: &ElementNode<'_>) -> bool {
el.props
.iter()
.any(|prop| matches!(prop, PropNode::Directive(dir) if dir.name == "slot"))
}
fn find_v_slot<'a, 'b>(el: &'b ElementNode<'a>) -> Option<&'b DirectiveNode<'a>> {
el.props.iter().find_map(|prop| match prop {
PropNode::Directive(dir) if dir.name == "slot" => Some(dir.as_ref()),
_ => None,
})
}
pub fn get_slot_name(dir: &DirectiveNode<'_>) -> String {
match dir.arg.as_ref() {
Some(ExpressionNode::Simple(exp)) if exp.is_static => {
static_slot_name_with_modifiers(exp.content.clone(), dir)
}
Some(ExpressionNode::Simple(exp)) => exp.loc.source.clone(),
Some(ExpressionNode::Compound(exp)) => exp.loc.source.clone(),
None => static_slot_name_with_modifiers(String::new("default"), dir),
}
}
fn static_slot_name_with_modifiers(mut name: String, dir: &DirectiveNode<'_>) -> String {
for modifier in dir.modifiers.iter() {
name.push('.');
name.push_str(modifier.content.as_str());
}
name
}
pub fn get_slot_props_string(dir: &DirectiveNode<'_>) -> Option<String> {
dir.exp.as_ref().map(|exp| match exp {
ExpressionNode::Simple(s) => s.content.clone(),
ExpressionNode::Compound(c) => c.loc.source.clone(),
})
}
pub fn get_slot_prop_names(dir: &DirectiveNode<'_>) -> Vec<String> {
get_slot_props_string(dir)
.map(|pattern| extract_slot_prop_names(pattern.as_str()))
.unwrap_or_default()
}
pub fn extract_slot_prop_names(pattern: &str) -> Vec<String> {
let trimmed = pattern.trim();
if trimmed.is_empty() {
return Vec::new();
}
let mut source = String::with_capacity(trimmed.len() + 18);
source.push_str("let ");
source.push_str(trimmed);
source.push_str(" = __slotProps");
let allocator = Allocator::default();
let source_type = SourceType::default().with_typescript(true);
let parsed = Parser::new(&allocator, source.as_str(), source_type).parse();
let Some(oxc_ast::ast::Statement::VariableDeclaration(var_decl)) = parsed.program.body.first()
else {
return Vec::new();
};
let Some(declarator) = var_decl.declarations.first() else {
return Vec::new();
};
let mut names = Vec::new();
collect_slot_binding_names(&declarator.id, &mut names);
names
}
fn collect_slot_binding_names(pattern: &BindingPattern<'_>, names: &mut Vec<String>) {
match pattern {
BindingPattern::BindingIdentifier(id) => {
names.push(String::new(id.name.as_str()));
}
BindingPattern::ObjectPattern(obj) => {
for prop in obj.properties.iter() {
collect_slot_binding_names(&prop.value, names);
}
if let Some(rest) = &obj.rest {
collect_slot_binding_names(&rest.argument, names);
}
}
BindingPattern::ArrayPattern(arr) => {
for elem in arr.elements.iter().flatten() {
collect_slot_binding_names(elem, names);
}
if let Some(rest) = &arr.rest {
collect_slot_binding_names(&rest.argument, names);
}
}
BindingPattern::AssignmentPattern(assign) => {
collect_slot_binding_names(&assign.left, names);
}
}
}
pub fn is_dynamic_slot(dir: &DirectiveNode<'_>) -> bool {
if let Some(arg) = &dir.arg {
match arg {
ExpressionNode::Simple(exp) => !exp.is_static,
ExpressionNode::Compound(_) => true,
}
} else {
false
}
}
fn is_slot_template(el: &ElementNode<'_>) -> bool {
el.tag.as_str() == "template" && has_v_slot(el)
}
fn has_structural_slot_directive(el: &ElementNode<'_>) -> bool {
el.props.iter().any(|prop| {
matches!(
prop,
PropNode::Directive(dir)
if matches!(dir.name.as_str(), "if" | "else-if" | "else" | "for")
)
})
}
fn has_meaningful_implicit_default_child(child: &TemplateChildNode<'_>) -> bool {
match child {
TemplateChildNode::Comment(_) => false,
TemplateChildNode::Text(text) => !text.content.trim().is_empty(),
TemplateChildNode::Element(el) if is_slot_template(el) => false,
_ => true,
}
}
fn slot_name_is_static(dir: &DirectiveNode<'_>) -> bool {
dir.arg.as_ref().is_none_or(|arg| match arg {
ExpressionNode::Simple(exp) => exp.is_static,
ExpressionNode::Compound(_) => false,
})
}
pub(crate) fn validate_v_slot_usage(ctx: &mut TransformContext<'_>, el: &ElementNode<'_>) {
let own_slot = find_v_slot(el);
if let Some(dir) = own_slot
&& el.tag_type != ElementType::Component
&& el.tag.as_str() != "template"
{
ctx.on_error(ErrorCode::VSlotMisplaced, Some(dir.loc.clone()));
}
if el.tag_type == ElementType::Slot || el.tag.as_str() == "slot" {
for prop in el.props.iter() {
if let PropNode::Directive(dir) = prop
&& !is_builtin_directive(dir.name.as_str())
{
ctx.on_error(
ErrorCode::VSlotUnexpectedDirectiveOnSlotOutlet,
Some(dir.loc.clone()),
);
}
}
}
if el.tag_type != ElementType::Component || el.children.is_empty() {
return;
}
let mut seen_static_slots: std::vec::Vec<String> = std::vec::Vec::new();
let mut has_template_slots = false;
let mut has_named_default_slot = false;
let mut first_implicit_default_loc: Option<SourceLocation> = None;
for child in el.children.iter() {
let TemplateChildNode::Element(child_el) = child else {
if first_implicit_default_loc.is_none() && has_meaningful_implicit_default_child(child)
{
first_implicit_default_loc = Some(child.loc().clone());
}
continue;
};
let Some(slot_dir) = find_v_slot(child_el) else {
if first_implicit_default_loc.is_none() && has_meaningful_implicit_default_child(child)
{
first_implicit_default_loc = Some(child.loc().clone());
}
continue;
};
if child_el.tag.as_str() != "template" {
continue;
}
if own_slot.is_some() {
ctx.on_error(ErrorCode::VSlotMixedSlotUsage, Some(slot_dir.loc.clone()));
break;
}
has_template_slots = true;
if !has_structural_slot_directive(child_el) && slot_name_is_static(slot_dir) {
let slot_name = get_slot_name(slot_dir);
if seen_static_slots
.iter()
.any(|seen| seen.as_str() == slot_name.as_str())
{
ctx.on_error(
ErrorCode::VSlotDuplicateSlotNames,
Some(slot_dir.loc.clone()),
);
continue;
}
if slot_name.as_str() == "default" {
has_named_default_slot = true;
}
seen_static_slots.push(slot_name);
}
}
if own_slot.is_none()
&& has_template_slots
&& has_named_default_slot
&& let Some(loc) = first_implicit_default_loc
{
ctx.on_error(ErrorCode::VSlotExtraneousDefaultSlotChildren, Some(loc));
}
}
#[derive(Debug)]
pub struct SlotOutletInfo {
pub name: String,
pub props_expr: Option<String>,
pub has_fallback: bool,
}
pub fn transform_slot_outlet<'a>(
ctx: &mut TransformContext<'a>,
dir: &DirectiveNode<'a>,
el: &ElementNode<'a>,
) -> Option<SlotOutletInfo> {
ctx.helper(RuntimeHelper::RenderSlot);
if el.tag != "slot" {
return None;
}
let slot_name = get_slot_name(dir);
let props_expr = get_slot_props_string(dir);
let has_fallback = !el.children.is_empty();
Some(SlotOutletInfo {
name: slot_name,
props_expr,
has_fallback,
})
}
#[derive(Debug)]
pub struct SlotInfo {
pub name: String,
pub params_expr: Option<String>,
pub is_dynamic: bool,
}
pub fn collect_slots<'a>(el: &ElementNode<'a>) -> Vec<SlotInfo> {
let mut slots = Vec::new();
let mut seen_static_slots: std::vec::Vec<String> = std::vec::Vec::new();
for child in el.children.iter() {
if let TemplateChildNode::Element(child_el) = child
&& child_el.tag == "template"
{
for prop in child_el.props.iter() {
if let PropNode::Directive(dir) = prop
&& dir.name == "slot"
{
let name = get_slot_name(dir);
let params_expr = get_slot_props_string(dir);
let is_dynamic = is_dynamic_slot(dir);
if !is_dynamic
&& seen_static_slots
.iter()
.any(|seen| seen.as_str() == name.as_str())
{
continue;
}
if !is_dynamic {
seen_static_slots.push(name.clone());
}
slots.push(SlotInfo {
name,
params_expr,
is_dynamic,
});
}
}
}
}
let has_non_slot_children = el.children.iter().any(|child| {
if let TemplateChildNode::Element(el) = child {
!(el.tag == "template" && has_v_slot(el))
} else {
true
}
});
if has_non_slot_children && !slots.iter().any(|s| s.name == "default") {
slots.push(SlotInfo {
name: String::new("default"),
params_expr: None,
is_dynamic: false,
});
}
slots
}
pub fn has_dynamic_slots<'a>(el: &ElementNode<'a>) -> bool {
for child in el.children.iter() {
if let TemplateChildNode::Element(child_el) = child
&& child_el.tag == "template"
{
for prop in child_el.props.iter() {
if let PropNode::Directive(dir) = prop
&& dir.name == "slot"
&& is_dynamic_slot(dir)
{
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::{
DirectiveNode, SourceLocation, TemplateChildNode, collect_slots, extract_slot_prop_names,
get_slot_name, get_slot_prop_names, has_v_slot,
};
use crate::errors::{CompilerError, ErrorCode};
use crate::lane::traverse::traverse_children;
use crate::lane::{ParentNode, TransformContext};
use crate::options::TransformOptions;
use crate::parser::parse;
use bumpalo::Bump;
fn transform_errors(source: &str) -> std::vec::Vec<CompilerError> {
let allocator = Bump::new();
let (mut root, errors) = parse(&allocator, source);
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
let mut ctx =
TransformContext::new(&allocator, root.source.clone(), TransformOptions::default());
traverse_children(&mut ctx, ParentNode::Root(&mut root as *mut _));
ctx.errors
}
#[test]
fn test_has_v_slot() {
let allocator = Bump::new();
let (root, _) = parse(&allocator, r#"<template v-slot:header>content</template>"#);
if let TemplateChildNode::Element(el) = &root.children[0] {
assert!(has_v_slot(el));
}
}
#[test]
fn test_default_slot_name() {
let allocator = Bump::new();
let dir = DirectiveNode::new(&allocator, "slot", SourceLocation::STUB);
assert_eq!(get_slot_name(&dir).as_str(), "default");
}
#[test]
fn test_collect_slots() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<Comp><template #header>H</template><template #footer>F</template></Comp>"#,
);
if let TemplateChildNode::Element(el) = &root.children[0] {
let slots = collect_slots(el);
assert_eq!(slots.len(), 2);
assert!(slots.iter().any(|s| s.name == "header"));
assert!(slots.iter().any(|s| s.name == "footer"));
}
}
#[test]
fn test_collect_slots_dedupes_static_duplicate_slot_names() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<Comp><template #header>H1</template><template #header>H2</template></Comp>"#,
);
if let TemplateChildNode::Element(el) = &root.children[0] {
let slots = collect_slots(el);
assert_eq!(slots.len(), 1);
assert_eq!(slots[0].name, "header");
}
}
#[test]
fn test_v_slot_on_plain_element_reports_misplaced() {
let errors = transform_errors(r#"<div v-slot="{ item }">Text</div>"#);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, ErrorCode::VSlotMisplaced);
}
#[test]
fn test_v_slot_on_empty_plain_element_reports_misplaced() {
let errors = transform_errors(r#"<div v-slot></div>"#);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, ErrorCode::VSlotMisplaced);
}
#[test]
fn test_duplicate_slot_names_report_error() {
let errors = transform_errors(
r#"<Comp><template #header>H1</template><template #header>H2</template></Comp>"#,
);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, ErrorCode::VSlotDuplicateSlotNames);
}
#[test]
fn test_mixed_component_and_template_slot_usage_reports_error() {
let errors = transform_errors(r#"<Comp v-slot><template #header>Header</template></Comp>"#);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, ErrorCode::VSlotMixedSlotUsage);
}
#[test]
fn test_explicit_default_slot_with_children_reports_error() {
let errors = transform_errors(
r#"<Comp><template #default>Default</template><span>Extra</span></Comp>"#,
);
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].code,
ErrorCode::VSlotExtraneousDefaultSlotChildren
);
}
#[test]
fn test_explicit_default_slot_allows_whitespace_children() {
let errors = transform_errors("<Comp><template #default>Default</template>\n \t\n</Comp>");
assert!(errors.is_empty(), "Unexpected errors: {:?}", errors);
}
#[test]
fn test_custom_directive_on_slot_outlet_reports_error() {
let errors = transform_errors(r#"<slot v-custom />"#);
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].code,
ErrorCode::VSlotUnexpectedDirectiveOnSlotOutlet
);
}
#[test]
fn test_extract_slot_prop_names_simple_destructure() {
let names = extract_slot_prop_names("{ item, index }");
let names: Vec<_> = names.iter().map(|name| name.as_str()).collect();
assert_eq!(names, vec!["item", "index"]);
}
#[test]
fn test_extract_slot_prop_names_nested_defaults_and_rest() {
let names = extract_slot_prop_names("{ item: { id }, index = 0, ...rest }");
let names: Vec<_> = names.iter().map(|name| name.as_str()).collect();
assert_eq!(names, vec!["id", "index", "rest"]);
}
#[test]
fn test_get_slot_prop_names_from_directive() {
let allocator = Bump::new();
let (root, _) = parse(
&allocator,
r#"<Comp><template #default="{ item, active }">{{ item.id }}{{ active }}</template></Comp>"#,
);
if let TemplateChildNode::Element(el) = &root.children[0] {
if let TemplateChildNode::Element(slot_template) = &el.children[0] {
let dir = slot_template
.props
.iter()
.find_map(|prop| match prop {
crate::PropNode::Directive(dir) if dir.name == "slot" => Some(dir),
_ => None,
})
.expect("expected v-slot directive");
let names = get_slot_prop_names(dir);
let names: Vec<_> = names.iter().map(|name| name.as_str()).collect();
assert_eq!(names, vec!["item", "active"]);
} else {
panic!("expected slot template element");
}
} else {
panic!("expected component root element");
}
}
}