use std::collections::HashSet;
use proc_macro2::TokenStream;
use quote::quote;
use crate::codegen::typemap::{self, CodegenContext, TypePosition};
use crate::ir::{Param, TypeRef};
use crate::parse::scope::ScopeId;
use crate::util::naming::to_snake_case;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SignatureKind {
Constructor,
Method,
StaticMethod,
Function,
Setter,
StaticSetter,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ConcreteParam {
pub name: String,
pub type_ref: TypeRef,
pub variadic: bool,
}
#[derive(Clone, Debug)]
pub struct ExpandedSignature {
pub rust_name: String,
pub js_name: String,
pub params: Vec<ConcreteParam>,
pub catch: bool,
pub return_type: TypeRef,
pub doc: Option<String>,
pub kind: SignatureKind,
}
pub fn dedupe_name(candidate: &str, used_names: &mut HashSet<String>) -> String {
let mut name = candidate.to_string();
if !used_names.contains(&name) {
used_names.insert(name.clone());
return name;
}
let base = name.clone();
let mut counter = 1u32;
loop {
name = format!("{base}_{counter}");
if !used_names.contains(&name) {
used_names.insert(name.clone());
return name;
}
counter += 1;
}
}
#[allow(clippy::too_many_arguments)]
pub fn expand_signatures(
js_name: &str,
overloads: &[&[Param]],
return_type: &TypeRef,
kind: SignatureKind,
doc: &Option<String>,
used_names: &mut HashSet<String>,
cgctx: Option<&CodegenContext<'_>>,
scope: ScopeId,
) -> Vec<ExpandedSignature> {
let base_rust_name = match kind {
SignatureKind::Constructor => "new".to_string(),
SignatureKind::Setter | SignatureKind::StaticSetter => {
format!("set_{}", to_snake_case(js_name))
}
_ => to_snake_case(js_name),
};
let mut all_sigs: Vec<Vec<ConcreteParam>> = Vec::new();
for params in overloads {
let expanded = expand_single_overload(params, cgctx, scope);
all_sigs.extend(expanded);
}
let mut seen: Vec<&Vec<ConcreteParam>> = Vec::new();
let mut deduped: Vec<Vec<ConcreteParam>> = Vec::new();
for sig in &all_sigs {
if !seen.iter().any(|s| concrete_params_eq(s, sig)) {
seen.push(sig);
deduped.push(sig.clone());
}
}
let candidate_names = compute_rust_names(&base_rust_name, &deduped);
let is_constructor = kind == SignatureKind::Constructor;
let mut result = Vec::new();
for (i, (candidate, concrete_params)) in candidate_names.into_iter().zip(deduped).enumerate() {
let is_first = i == 0;
let rust_name = dedupe_name(&candidate, used_names);
result.push(ExpandedSignature {
rust_name: rust_name.clone(),
js_name: js_name.to_string(),
params: concrete_params.clone(),
catch: is_constructor,
return_type: return_type.clone(),
doc: if is_first { doc.clone() } else { None },
kind,
});
let emit_try = !matches!(
kind,
SignatureKind::Constructor | SignatureKind::Setter | SignatureKind::StaticSetter
);
if emit_try {
let try_candidate = format!("try_{rust_name}");
let try_name = dedupe_name(&try_candidate, used_names);
result.push(ExpandedSignature {
rust_name: try_name,
js_name: js_name.to_string(),
params: concrete_params,
catch: true,
return_type: return_type.clone(),
doc: None,
kind,
});
}
}
result
}
fn concrete_params_eq(a: &[ConcreteParam], b: &[ConcreteParam]) -> bool {
a.len() == b.len()
&& a.iter().zip(b.iter()).all(|(pa, pb)| {
pa.name == pb.name && pa.type_ref == pb.type_ref && pa.variadic == pb.variadic
})
}
fn expand_single_overload(
params: &[Param],
cgctx: Option<&CodegenContext<'_>>,
scope: ScopeId,
) -> Vec<Vec<ConcreteParam>> {
let (non_variadic, variadic_param) = if params.last().is_some_and(|p| p.variadic) {
(¶ms[..params.len() - 1], Some(¶ms[params.len() - 1]))
} else {
(params, None)
};
let mut sigs: Vec<Vec<ConcreteParam>> = vec![vec![]];
for (i, param) in non_variadic.iter().enumerate() {
let type_alternatives = flatten_type(¶m.type_ref, cgctx, scope);
if param.optional {
let frozen: Vec<Vec<ConcreteParam>> =
sigs.iter().filter(|s| s.len() < i).cloned().collect();
let mut extendable: Vec<Vec<ConcreteParam>> =
sigs.into_iter().filter(|s| s.len() == i).collect();
let snapshot = extendable.clone();
let cur = extendable.len();
for (j, alt) in type_alternatives.into_iter().enumerate() {
let concrete = ConcreteParam {
name: param.name.clone(),
type_ref: alt,
variadic: false,
};
if j == 0 {
for sig in extendable.iter_mut().take(cur) {
sig.push(concrete.clone());
}
} else {
for item in snapshot.iter().take(cur) {
let mut sig = item.clone();
sig.push(concrete.clone());
extendable.push(sig);
}
}
}
sigs = frozen;
sigs.extend(snapshot);
sigs.extend(extendable);
} else {
let cur = sigs.len();
for (j, alt) in type_alternatives.into_iter().enumerate() {
let concrete = ConcreteParam {
name: param.name.clone(),
type_ref: alt,
variadic: false,
};
if j == 0 {
for sig in sigs.iter_mut().take(cur) {
sig.push(concrete.clone());
}
} else {
for k in 0..cur {
let mut sig = sigs[k].clone();
sig.truncate(i);
sig.push(concrete.clone());
sigs.push(sig);
}
}
}
}
}
if let Some(vp) = variadic_param {
for sig in &mut sigs {
sig.push(ConcreteParam {
name: vp.name.clone(),
type_ref: vp.type_ref.clone(),
variadic: true,
});
}
}
sigs
}
fn flatten_type(ty: &TypeRef, cgctx: Option<&CodegenContext<'_>>, scope: ScopeId) -> Vec<TypeRef> {
match ty {
TypeRef::Union(members) => members
.iter()
.flat_map(|m| flatten_type(m, cgctx, scope))
.collect(),
TypeRef::Named(name) => {
if let Some(c) = cgctx {
if let Some(target) = c.resolve_alias(name, scope) {
let target = target.clone();
return flatten_type(&target, cgctx, scope);
}
}
vec![ty.clone()]
}
TypeRef::Nullable(inner) => flatten_type(inner, cgctx, scope)
.into_iter()
.map(|t| TypeRef::Nullable(Box::new(t)))
.collect(),
TypeRef::Promise(inner) => flatten_type(inner, cgctx, scope)
.into_iter()
.map(|t| TypeRef::Promise(Box::new(t)))
.collect(),
TypeRef::Array(inner) => flatten_type(inner, cgctx, scope)
.into_iter()
.map(|t| TypeRef::Array(Box::new(t)))
.collect(),
TypeRef::Set(inner) => flatten_type(inner, cgctx, scope)
.into_iter()
.map(|t| TypeRef::Set(Box::new(t)))
.collect(),
TypeRef::Record(k, v) => {
let ks = flatten_type(k, cgctx, scope);
let vs = flatten_type(v, cgctx, scope);
let mut result = Vec::new();
for k in &ks {
for v in &vs {
result.push(TypeRef::Record(Box::new(k.clone()), Box::new(v.clone())));
}
}
result
}
TypeRef::Map(k, v) => {
let ks = flatten_type(k, cgctx, scope);
let vs = flatten_type(v, cgctx, scope);
let mut result = Vec::new();
for k in &ks {
for v in &vs {
result.push(TypeRef::Map(Box::new(k.clone()), Box::new(v.clone())));
}
}
result
}
_ => vec![ty.clone()],
}
}
fn compute_rust_names(base_name: &str, signatures: &[Vec<ConcreteParam>]) -> Vec<String> {
if signatures.len() == 1 {
return vec![base_name.to_string()];
}
let (trim_start, trim_end) = compute_trim(signatures);
let mut names = Vec::new();
for (sig_idx, sig) in signatures.iter().enumerate() {
if sig_idx == 0 {
names.push(base_name.to_string());
continue;
}
let mut name = base_name.to_string();
let mut first_suffix = true;
let end = if sig.len() >= trim_end {
sig.len() - trim_end
} else {
sig.len()
};
let start = trim_start.min(end);
for (param_idx, param) in sig[start..end].iter().enumerate() {
let abs_idx = start + param_idx;
let mut any_different = false;
let mut any_same_name_different_type = false;
for (other_idx, other) in signatures.iter().enumerate() {
if other_idx == sig_idx {
continue;
}
match other.get(abs_idx) {
Some(other_param) => {
if other_param.name == param.name && other_param.type_ref != param.type_ref
{
any_same_name_different_type = true;
any_different = true;
} else if other_param.name != param.name {
any_different = true;
}
}
None => {
any_different = true;
}
}
}
if !any_different {
continue;
}
if first_suffix {
name.push_str("_with_");
first_suffix = false;
} else {
name.push_str("_and_");
}
if any_same_name_different_type {
name.push_str(&type_snake_name(¶m.type_ref));
} else {
name.push_str(&to_snake_case(¶m.name));
}
}
names.push(name);
}
names
}
fn compute_trim(signatures: &[Vec<ConcreteParam>]) -> (usize, usize) {
let min_len = signatures.iter().map(|s| s.len()).min().unwrap_or(0);
let mut trim_start = 0;
for i in 0..min_len {
let first = &signatures[0][i];
if signatures[1..].iter().all(|sig| sig[i] == *first) {
trim_start += 1;
} else {
break;
}
}
let mut trim_end = 0;
for i in 0..min_len {
let first = &signatures[0][signatures[0].len() - 1 - i];
if signatures[1..]
.iter()
.all(|sig| sig[sig.len() - 1 - i] == *first)
{
trim_end += 1;
} else {
break;
}
}
if trim_start + trim_end > min_len {
trim_end = min_len - trim_start;
}
(trim_start, trim_end)
}
fn type_snake_name(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "str".to_string(),
TypeRef::Number => "f64".to_string(),
TypeRef::Boolean => "bool".to_string(),
TypeRef::BigInt => "big_int".to_string(),
TypeRef::Void | TypeRef::Undefined => "undefined".to_string(),
TypeRef::Null => "null".to_string(),
TypeRef::Any | TypeRef::Unknown => "js_value".to_string(),
TypeRef::Object => "object".to_string(),
TypeRef::Named(n) => to_snake_case(n),
TypeRef::ArrayBuffer => "array_buffer".to_string(),
TypeRef::Uint8Array => "uint8_array".to_string(),
TypeRef::Int8Array => "int8_array".to_string(),
TypeRef::Float32Array => "float32_array".to_string(),
TypeRef::Float64Array => "float64_array".to_string(),
TypeRef::Array(_) => "array".to_string(),
TypeRef::Promise(_) => "promise".to_string(),
TypeRef::Nullable(inner) => type_snake_name(inner),
TypeRef::Function(_) => "function".to_string(),
TypeRef::Date => "date".to_string(),
TypeRef::RegExp => "reg_exp".to_string(),
TypeRef::Error => "error".to_string(),
TypeRef::Map(_, _) => "map".to_string(),
TypeRef::Set(_) => "set".to_string(),
TypeRef::Record(_, _) => "record".to_string(),
_ => "js_value".to_string(),
}
}
pub fn generate_concrete_params(
params: &[ConcreteParam],
cgctx: Option<&CodegenContext<'_>>,
scope: ScopeId,
) -> TokenStream {
let items: Vec<_> = params
.iter()
.map(|p| {
let name = typemap::make_ident(&p.name);
let ty = if p.variadic {
quote! { &[JsValue] }
} else {
typemap::to_syn_type(&p.type_ref, TypePosition::ARGUMENT, cgctx, scope)
};
quote! { #name: #ty }
})
.collect();
quote! { #(#items),* }
}
pub fn is_void_return(ty: &TypeRef) -> bool {
matches!(ty, TypeRef::Void | TypeRef::Undefined)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::typemap::CodegenContext;
use crate::context::GlobalContext;
use crate::ir::TypeRef;
fn no_used() -> HashSet<String> {
HashSet::new()
}
fn test_ctx() -> (GlobalContext, ScopeId) {
let mut gctx = GlobalContext::new();
let scope = gctx.create_root_scope();
(gctx, scope)
}
fn expand(
js: &str,
params: &[Param],
ret: &TypeRef,
kind: SignatureKind,
doc: &Option<String>,
used: &mut HashSet<String>,
) -> Vec<ExpandedSignature> {
let (gctx, scope) = test_ctx();
let cgctx = CodegenContext::empty(&gctx, scope);
expand_signatures(js, &[params], ret, kind, doc, used, Some(&cgctx), scope)
}
fn expand_overloads(
js: &str,
overloads: &[&[Param]],
ret: &TypeRef,
kind: SignatureKind,
doc: &Option<String>,
used: &mut HashSet<String>,
) -> Vec<ExpandedSignature> {
let (gctx, scope) = test_ctx();
let cgctx = CodegenContext::empty(&gctx, scope);
expand_signatures(js, overloads, ret, kind, doc, used, Some(&cgctx), scope)
}
fn param(name: &str) -> Param {
Param {
name: name.to_string(),
type_ref: TypeRef::Any,
optional: false,
variadic: false,
}
}
fn typed_param(name: &str, ty: TypeRef) -> Param {
Param {
name: name.to_string(),
type_ref: ty,
optional: false,
variadic: false,
}
}
fn opt_param(name: &str) -> Param {
Param {
name: name.to_string(),
type_ref: TypeRef::Any,
optional: true,
variadic: false,
}
}
fn opt_typed_param(name: &str, ty: TypeRef) -> Param {
Param {
name: name.to_string(),
type_ref: ty,
optional: true,
variadic: false,
}
}
fn variadic_param(name: &str) -> Param {
Param {
name: name.to_string(),
type_ref: TypeRef::Any,
optional: false,
variadic: true,
}
}
#[test]
fn test_no_optional_params() {
let mut used = no_used();
let sigs = expand(
"foo",
&[param("a"), param("b")],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
assert_eq!(sigs.len(), 2);
assert_eq!(sigs[0].rust_name, "foo");
assert!(!sigs[0].catch);
assert_eq!(sigs[0].params.len(), 2);
assert_eq!(sigs[1].rust_name, "try_foo");
assert!(sigs[1].catch);
}
#[test]
fn test_constructor_no_try_variant() {
let mut used = no_used();
let sigs = expand(
"Console",
&[param("stdout")],
&TypeRef::Named("Console".into()),
SignatureKind::Constructor,
&None,
&mut used,
);
assert_eq!(sigs.len(), 1);
assert_eq!(sigs[0].rust_name, "new");
assert!(sigs[0].catch);
}
#[test]
fn test_optional_expansion() {
let mut used = no_used();
let sigs = expand(
"Console",
&[
param("stdout"),
opt_param("stderr"),
opt_param("ignoreErrors"),
],
&TypeRef::Named("Console".into()),
SignatureKind::Constructor,
&None,
&mut used,
);
assert_eq!(sigs.len(), 3);
assert_eq!(sigs[0].rust_name, "new");
assert_eq!(sigs[0].params.len(), 1);
assert_eq!(sigs[1].rust_name, "new_with_stderr");
assert_eq!(sigs[1].params.len(), 2);
assert_eq!(sigs[2].rust_name, "new_with_stderr_and_ignore_errors");
assert_eq!(sigs[2].params.len(), 3);
}
#[test]
fn test_optional_method_expansion() {
let mut used = no_used();
let sigs = expand(
"count",
&[opt_param("label")],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
assert_eq!(sigs.len(), 4);
assert_eq!(sigs[0].rust_name, "count");
assert_eq!(sigs[0].params.len(), 0);
assert!(!sigs[0].catch);
assert_eq!(sigs[1].rust_name, "try_count");
assert!(sigs[1].catch);
assert_eq!(sigs[2].rust_name, "count_with_label");
assert_eq!(sigs[2].params.len(), 1);
assert_eq!(sigs[3].rust_name, "try_count_with_label");
}
#[test]
fn test_variadic_param() {
let mut used = no_used();
let sigs = expand(
"log",
&[variadic_param("data")],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
assert_eq!(sigs.len(), 2);
assert_eq!(sigs[0].rust_name, "log");
assert_eq!(sigs[0].params.len(), 1);
assert!(sigs[0].params[0].variadic);
assert_eq!(sigs[1].rust_name, "try_log");
}
#[test]
fn test_optional_then_variadic() {
let mut used = no_used();
let sigs = expand(
"timeLog",
&[opt_param("label"), variadic_param("data")],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
assert_eq!(sigs.len(), 4);
assert_eq!(sigs[0].rust_name, "time_log");
assert_eq!(sigs[0].params.len(), 1); assert!(sigs[0].params[0].variadic);
assert_eq!(sigs[1].rust_name, "try_time_log");
assert_eq!(sigs[2].rust_name, "time_log_with_label");
assert_eq!(sigs[2].params.len(), 2); assert!(!sigs[2].params[0].variadic);
assert!(sigs[2].params[1].variadic);
assert_eq!(sigs[3].rust_name, "try_time_log_with_label");
}
#[test]
fn test_doc_only_on_first() {
let doc = Some("Hello".to_string());
let mut used = no_used();
let sigs = expand(
"count",
&[opt_param("label")],
&TypeRef::Void,
SignatureKind::Method,
&doc,
&mut used,
);
assert_eq!(sigs[0].doc, Some("Hello".to_string()));
assert_eq!(sigs[1].doc, None); assert_eq!(sigs[2].doc, None); assert_eq!(sigs[3].doc, None); }
#[test]
fn test_try_collision_deduped() {
let mut used: HashSet<String> = ["try_count".to_string()].into_iter().collect();
let sigs = expand(
"count",
&[param("x")],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
assert_eq!(sigs.len(), 2);
assert_eq!(sigs[0].rust_name, "count");
assert!(!sigs[0].catch);
assert_eq!(sigs[1].rust_name, "try_count_1");
assert!(sigs[1].catch);
}
#[test]
fn test_name_collision_deduped() {
let mut used = no_used();
let sigs1 = expand(
"foo",
&[param("a")],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
let sigs2 = expand(
"foo",
&[param("a"), param("b")],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
assert_eq!(sigs1[0].rust_name, "foo");
assert_eq!(sigs2[0].rust_name, "foo_1");
}
#[test]
fn test_overloads_with_variadic() {
let mut used = no_used();
let overload1 = [
typed_param("callback", TypeRef::Any),
opt_typed_param("msDelay", TypeRef::Number),
];
let overload2 = [
typed_param("callback", TypeRef::Any),
opt_typed_param("msDelay", TypeRef::Number),
variadic_param("args"),
];
let sigs = expand_overloads(
"setTimeout",
&[&overload1, &overload2],
&TypeRef::Number,
SignatureKind::Method,
&None,
&mut used,
);
let non_try: Vec<_> = sigs.iter().filter(|s| !s.catch).collect();
assert_eq!(non_try.len(), 4);
assert_eq!(non_try[0].rust_name, "set_timeout");
assert_eq!(non_try[0].params.len(), 1);
assert_eq!(non_try[1].rust_name, "set_timeout_with_ms_delay");
assert_eq!(non_try[1].params.len(), 2);
assert_eq!(non_try[2].rust_name, "set_timeout_with_args");
assert_eq!(non_try[2].params.len(), 2);
assert!(non_try[2].params[1].variadic);
assert_eq!(non_try[3].rust_name, "set_timeout_with_ms_delay_and_args");
assert_eq!(non_try[3].params.len(), 3);
assert!(non_try[3].params[2].variadic);
}
#[test]
fn test_overloads_with_different_types() {
let mut used = no_used();
let overload1 = [typed_param("x", TypeRef::String)];
let overload2 = [typed_param(
"x",
TypeRef::Promise(Box::new(TypeRef::String)),
)];
let sigs = expand_overloads(
"foo",
&[&overload1, &overload2],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
let non_try: Vec<_> = sigs.iter().filter(|s| !s.catch).collect();
assert_eq!(non_try.len(), 2);
assert_eq!(non_try[0].rust_name, "foo");
assert_eq!(non_try[1].rust_name, "foo_with_promise");
}
#[test]
fn test_overloads_shared_truncation_deduped() {
let mut used = no_used();
let overload1 = [param("a"), opt_param("b")];
let overload2 = [param("a"), opt_param("c")];
let sigs = expand_overloads(
"foo",
&[&overload1, &overload2],
&TypeRef::Void,
SignatureKind::Method,
&None,
&mut used,
);
let non_try: Vec<_> = sigs.iter().filter(|s| !s.catch).collect();
assert_eq!(non_try.len(), 3);
assert_eq!(non_try[0].rust_name, "foo");
assert_eq!(non_try[0].params.len(), 1);
assert_eq!(non_try[1].rust_name, "foo_with_b");
assert_eq!(non_try[1].params.len(), 2);
assert_eq!(non_try[2].rust_name, "foo_with_c");
assert_eq!(non_try[2].params.len(), 2);
}
}