use vize_carton::{FxHashSet, String, ToCompactString};
use oxc_ast::ast::{CallExpression, Expression, TSSignature, TSType, TSTypeLiteral};
use oxc_span::GetSpan;
use super::context::ScriptCompileContext;
pub const DEFINE_EMITS: &str = "defineEmits";
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct DefineEmitsResult {
pub runtime_decl: Option<String>,
pub type_decl: Option<String>,
pub decl_id: Option<String>,
}
#[allow(dead_code)]
pub fn process_define_emits(
ctx: &mut ScriptCompileContext,
call: &CallExpression<'_>,
source: &str,
decl_id: Option<String>,
) -> bool {
if !is_call_of(call, DEFINE_EMITS) {
return false;
}
if ctx.has_define_emits_call {
eprintln!("duplicate {}() call", DEFINE_EMITS);
}
ctx.has_define_emits_call = true;
let runtime_decl = if !call.arguments.is_empty() {
let arg = &call.arguments[0];
let start = arg.span().start as usize;
let end = arg.span().end as usize;
Some(String::from(source[start..end].trim()))
} else {
None
};
let type_decl = call.type_arguments.as_ref().map(|params| {
let start = params.span.start as usize;
let end = params.span.end as usize;
let type_str = &source[start..end];
if type_str.starts_with('<') && type_str.ends_with('>') {
String::from(&type_str[1..type_str.len() - 1])
} else {
String::from(type_str)
}
});
if runtime_decl.is_some() && type_decl.is_some() {
eprintln!(
"{}() cannot accept both type and non-type arguments at the same time. Use one or the other.",
DEFINE_EMITS
);
}
ctx.emits_runtime_decl = runtime_decl;
ctx.emits_type_decl = type_decl;
ctx.emit_decl_id = decl_id;
true
}
#[allow(dead_code)]
pub fn gen_runtime_emits(ctx: &ScriptCompileContext, model_names: &[String]) -> Option<String> {
fn debug_string<T: std::fmt::Debug>(value: &T) -> String {
let mut out = String::default();
use std::fmt::Write as _;
let _ = write!(&mut out, "{:?}", value);
out
}
let mut emits_decl = String::default();
if let Some(ref runtime_decl) = ctx.emits_runtime_decl {
emits_decl = runtime_decl.trim().to_compact_string();
} else if ctx.emits_type_decl.is_some() {
let type_declared_emits = extract_runtime_emits(ctx);
if !type_declared_emits.is_empty() {
let emits: Vec<String> = type_declared_emits
.into_iter()
.map(|k| debug_string(&k)) .collect();
let joined = emits.join(", ");
let mut out = String::with_capacity(joined.len() + 2);
out.push('[');
out.push_str(&joined);
out.push(']');
emits_decl = out;
}
}
if !model_names.is_empty() {
let model_emits: Vec<String> = model_names
.iter()
.map(|n| {
let mut name = String::with_capacity(7 + n.len());
name.push_str("update:");
name.push_str(n);
debug_string(&name)
})
.collect();
let joined = model_emits.join(", ");
let mut model_emits_decl = String::with_capacity(joined.len() + 2);
model_emits_decl.push('[');
model_emits_decl.push_str(&joined);
model_emits_decl.push(']');
if emits_decl.is_empty() {
emits_decl = model_emits_decl;
} else {
let mut merged = String::with_capacity(emits_decl.len() + model_emits_decl.len() + 26);
merged.push_str("/*@__PURE__*/_mergeModels(");
merged.push_str(&emits_decl);
merged.push_str(", ");
merged.push_str(&model_emits_decl);
merged.push(')');
emits_decl = merged;
}
}
if emits_decl.is_empty() {
None
} else {
Some(emits_decl)
}
}
#[allow(dead_code)]
pub fn extract_runtime_emits(ctx: &ScriptCompileContext) -> FxHashSet<String> {
let mut emits = FxHashSet::default();
let type_decl = match &ctx.emits_type_decl {
Some(decl) => decl,
None => return emits,
};
if type_decl.contains("=>") && !type_decl.contains('{') {
if let Some(event_name) = extract_event_name_from_function_type(type_decl) {
emits.insert(event_name);
}
return emits;
}
extract_events_from_type_literal(type_decl, &mut emits);
emits
}
fn extract_event_name_from_function_type(type_str: &str) -> Option<String> {
let re = regex::Regex::new(r#"\(\s*\w+\s*:\s*['"]([^'"]+)['"]\s*[,)]"#).ok()?;
re.captures(type_str)
.and_then(|cap| cap.get(1).map(|m| m.as_str().to_compact_string()))
}
fn extract_events_from_type_literal(type_str: &str, emits: &mut FxHashSet<String>) {
let call_sig_re =
regex::Regex::new(r#"\(\s*\w+\s*:\s*['"]([^'"]+)['"]\s*(?:,\s*[^)]+)?\)\s*:"#).unwrap();
for cap in call_sig_re.captures_iter(type_str) {
if let Some(event_name) = cap.get(1) {
emits.insert(event_name.as_str().to_compact_string());
}
}
let prop_re = regex::Regex::new(r#"(?:^|[{;,])\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:"#).unwrap();
for cap in prop_re.captures_iter(type_str) {
if let Some(prop_name) = cap.get(1) {
let name = prop_name.as_str();
if !matches!(name, "type" | "required" | "default" | "validator") {
emits.insert(name.to_compact_string());
}
}
}
}
#[allow(dead_code)]
pub fn extract_event_names_from_ts_type(
ts_type: &TSType<'_>,
emits: &mut FxHashSet<String>,
#[allow(clippy::only_used_in_recursion)] source: &str,
) {
match ts_type {
TSType::TSFunctionType(func_type) => {
if let Some(first_param) = func_type.params.items.first() {
if let Some(type_ann) = &first_param.type_annotation {
extract_literal_values_from_ts_type(&type_ann.type_annotation, emits, source);
}
}
}
TSType::TSTypeLiteral(type_lit) => {
extract_from_ts_type_literal(type_lit, emits, source);
}
TSType::TSUnionType(union) => {
for member in union.types.iter() {
extract_event_names_from_ts_type(member, emits, source);
}
}
TSType::TSIntersectionType(intersection) => {
for member in intersection.types.iter() {
extract_event_names_from_ts_type(member, emits, source);
}
}
TSType::TSParenthesizedType(paren) => {
extract_event_names_from_ts_type(&paren.type_annotation, emits, source);
}
_ => {}
}
}
fn extract_from_ts_type_literal(
type_lit: &TSTypeLiteral<'_>,
emits: &mut FxHashSet<String>,
source: &str,
) {
let mut has_property = false;
let mut has_call_signature = false;
for member in type_lit.members.iter() {
match member {
TSSignature::TSPropertySignature(prop) => {
has_property = true;
if let Some(name) = get_property_key_name(&prop.key, source) {
emits.insert(name);
}
}
TSSignature::TSCallSignatureDeclaration(_call) => {
has_call_signature = true;
}
_ => {}
}
}
if has_property && has_call_signature {
eprintln!("defineEmits() type cannot mixed call signature and property syntax.");
}
if has_call_signature && !has_property {
for member in type_lit.members.iter() {
if let TSSignature::TSCallSignatureDeclaration(call) = member {
if let Some(first_param) = call.params.items.first() {
if let Some(type_ann) = &first_param.type_annotation {
extract_literal_values_from_ts_type(
&type_ann.type_annotation,
emits,
source,
);
}
}
}
}
}
}
fn extract_literal_values_from_ts_type(
ts_type: &TSType<'_>,
emits: &mut FxHashSet<String>,
#[allow(clippy::only_used_in_recursion)] source: &str,
) {
match ts_type {
TSType::TSLiteralType(lit_type) => {
match &lit_type.literal {
oxc_ast::ast::TSLiteral::StringLiteral(s) => {
emits.insert(s.value.to_compact_string());
}
oxc_ast::ast::TSLiteral::NumericLiteral(n) => {
emits.insert(n.value.to_compact_string());
}
_ => {}
}
}
TSType::TSUnionType(union) => {
for member in union.types.iter() {
extract_literal_values_from_ts_type(member, emits, source);
}
}
TSType::TSParenthesizedType(paren) => {
extract_literal_values_from_ts_type(&paren.type_annotation, emits, source);
}
_ => {}
}
}
fn get_property_key_name(key: &oxc_ast::ast::PropertyKey<'_>, _source: &str) -> Option<String> {
match key {
oxc_ast::ast::PropertyKey::StaticIdentifier(id) => Some(id.name.to_compact_string()),
oxc_ast::ast::PropertyKey::StringLiteral(s) => Some(s.value.to_compact_string()),
oxc_ast::ast::PropertyKey::NumericLiteral(n) => Some(n.value.to_compact_string()),
_ => None,
}
}
fn is_call_of(call: &CallExpression<'_>, name: &str) -> bool {
if let Expression::Identifier(id) = &call.callee {
return id.name.as_str() == name;
}
false
}
#[cfg(test)]
mod tests {
use vize_carton::{CompactString, FxHashSet, ToCompactString};
use super::{
extract_event_name_from_function_type, extract_events_from_type_literal, gen_runtime_emits,
ScriptCompileContext,
};
fn snapshot_emits(emits: &FxHashSet<CompactString>) -> Vec<&str> {
let mut emits: Vec<_> = emits.iter().map(|event| event.as_str()).collect();
emits.sort_unstable();
emits
}
#[test]
fn test_extract_event_name_from_function_type() {
let result = extract_event_name_from_function_type("(e: 'click') => void");
assert_eq!(result, Some("click".to_compact_string()));
let result = extract_event_name_from_function_type("(e: \"update\") => void");
assert_eq!(result, Some("update".to_compact_string()));
}
#[test]
fn test_extract_events_from_type_literal() {
let mut emits = FxHashSet::default();
extract_events_from_type_literal("{ (e: 'click'): void; (e: 'update'): void }", &mut emits);
insta::assert_debug_snapshot!(snapshot_emits(&emits));
}
#[test]
fn test_extract_events_call_signature_with_payload() {
let mut emits = FxHashSet::default();
extract_events_from_type_literal("{ (e: 'click', payload: MouseEvent): void }", &mut emits);
insta::assert_debug_snapshot!(snapshot_emits(&emits));
}
#[test]
fn test_gen_runtime_emits_empty() {
let ctx = ScriptCompileContext::new("");
let result = gen_runtime_emits(&ctx, &[]);
assert!(result.is_none());
}
#[test]
fn test_gen_runtime_emits_with_models() {
let ctx = ScriptCompileContext::new("");
let result = gen_runtime_emits(
&ctx,
&[
"modelValue".to_compact_string(),
"count".to_compact_string(),
],
);
assert!(result.is_some());
let emits = result.unwrap();
insta::assert_snapshot!(emits.as_str());
}
}