use crate::core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
use heck::ToLowerCamelCase;
use std::collections::BTreeSet;
use super::render_type::render_type;
use crate::backends::dart::ident::dart_safe_ident;
use crate::backends::dart::template_env;
fn dart_trait_ident(rust_name: &str) -> String {
dart_safe_ident(&rust_name.to_lower_camel_case())
}
pub(super) fn emit_dart_traits(api: &ApiSurface, trait_names: &[&str]) -> (String, BTreeSet<String>) {
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
for &trait_name in trait_names {
if let Some(trait_def) = api.types.iter().find(|t| t.name == trait_name && t.is_trait) {
emit_trait_abstract_class(trait_def, &api.excluded_type_paths, &mut body, &mut imports);
body.push('\n');
}
}
(body, imports)
}
fn emit_trait_abstract_class(
trait_def: &TypeDef,
excluded_type_paths: &std::collections::HashMap<String, String>,
out: &mut String,
imports: &mut BTreeSet<String>,
) {
let trait_name = &trait_def.name;
let own_methods: Vec<&MethodDef> = trait_def.methods.iter().filter(|m| m.trait_source.is_none()).collect();
out.push_str(&template_env::render(
"abstract_class_doc_comment.jinja",
minijinja::context! {
trait_name => trait_name.as_str(),
},
));
out.push_str(&template_env::render(
"abstract_class_doc_code_start.jinja",
minijinja::context! {},
));
out.push_str(&template_env::render(
"abstract_class_doc_code_impl.jinja",
minijinja::context! {
trait_name => trait_name.as_str(),
},
));
for method in &own_methods {
let method_camel = dart_trait_ident(&method.name);
out.push_str("/// @override\n");
out.push_str(&template_env::render(
"abstract_class_method_doc_line.jinja",
minijinja::context! {
return_type => substitute_excluded_named_types(
&dart_return_type_str(&method.return_type, imports),
excluded_type_paths,
),
method_camel => method_camel.as_str(),
},
));
}
out.push_str("/// }\n");
out.push_str("///\n");
out.push_str(&template_env::render(
"abstract_class_doc_code_create.jinja",
minijinja::context! {
trait_name => trait_name.as_str(),
},
));
for method in &own_methods {
let method_camel = dart_trait_ident(&method.name);
out.push_str(&template_env::render(
"trait_method_doc_field.jinja",
minijinja::context! {
method_camel => method_camel.as_str(),
},
));
}
out.push_str(&template_env::render(
"abstract_class_doc_code_end.jinja",
minijinja::context! {},
));
out.push_str(&template_env::render(
"abstract_class_header.jinja",
minijinja::context! {
trait_name => trait_name.as_str(),
},
));
for method in &own_methods {
emit_abstract_method(method, excluded_type_paths, out, imports);
}
out.push_str("}\n");
}
fn emit_abstract_method(
method: &MethodDef,
excluded_type_paths: &std::collections::HashMap<String, String>,
out: &mut String,
imports: &mut BTreeSet<String>,
) {
if !method.doc.is_empty() {
let doc_lines: Vec<String> = method.doc.lines().map(ToString::to_string).collect();
out.push_str(&template_env::render(
"doc_comment.jinja",
minijinja::context! {
indent => " ",
lines => doc_lines,
},
));
}
if let Some(ref error_ty) = method.error_type {
out.push_str(&template_env::render(
"function_throws_annotation.jinja",
minijinja::context! {
error_ty => error_ty.as_str(),
},
));
}
let method_camel = dart_trait_ident(&method.name);
let inner_ret =
substitute_excluded_named_types(&dart_return_type_str(&method.return_type, imports), excluded_type_paths);
let return_ty = if matches!(method.return_type, TypeRef::Unit) {
"Future<void>".to_string()
} else {
format!("Future<{inner_ret}>")
};
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let rendered = render_type(&p.ty, imports);
let mapped = substitute_excluded_named_types(&rendered, excluded_type_paths);
let ty = if p.optional { format!("{mapped}?") } else { mapped };
format!("{ty} {}", dart_trait_ident(&p.name))
})
.collect();
out.push_str(&template_env::render(
"abstract_method_declaration.jinja",
minijinja::context! {
return_ty => return_ty,
method_camel => method_camel.as_str(),
params => params.join(", "),
},
));
}
fn dart_return_type_str(ty: &TypeRef, imports: &mut BTreeSet<String>) -> String {
match ty {
TypeRef::Unit => "void".to_string(),
_ => render_type(ty, imports),
}
}
fn substitute_excluded_named_types(
rendered: &str,
excluded_type_paths: &std::collections::HashMap<String, String>,
) -> String {
let mut mapped = rendered.to_string();
for name in excluded_type_paths.keys() {
mapped = replace_token(&mapped, name, &format!("{name}Bridge"));
}
mapped
}
fn replace_token(input: &str, needle: &str, replacement: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut rest = input;
while let Some(index) = rest.find(needle) {
let (before, after_start) = rest.split_at(index);
out.push_str(before);
let after = &after_start[needle.len()..];
let before_ok = out.chars().last().is_none_or(|c| !c.is_alphanumeric() && c != '_');
let after_ok = after.chars().next().is_none_or(|c| !c.is_alphanumeric() && c != '_');
if before_ok && after_ok {
out.push_str(replacement);
} else {
out.push_str(needle);
}
rest = after;
}
out.push_str(rest);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{ParamDef, PrimitiveType, ReceiverKind};
use std::collections::HashMap;
fn trait_with_method(method: MethodDef) -> TypeDef {
TypeDef {
name: "SampleBackend".to_string(),
is_trait: true,
methods: vec![method],
..Default::default()
}
}
fn method(name: &str, params: Vec<ParamDef>) -> MethodDef {
MethodDef {
name: name.to_string(),
params,
return_type: TypeRef::Primitive(PrimitiveType::Bool),
is_async: true,
receiver: Some(ReceiverKind::Ref),
..Default::default()
}
}
fn param(name: &str) -> ParamDef {
ParamDef {
name: name.to_string(),
ty: TypeRef::String,
..Default::default()
}
}
fn emit(trait_def: &TypeDef) -> String {
let mut out = String::new();
let mut imports = BTreeSet::new();
emit_trait_abstract_class(trait_def, &HashMap::new(), &mut out, &mut imports);
out
}
#[test]
fn trait_method_named_new_is_renamed_in_the_declaration() {
let body = emit(&trait_with_method(method("new", vec![])));
assert!(
body.contains("Future<bool> new_();"),
"reserved word `new` must be renamed in the abstract method declaration:\n{body}"
);
assert!(
!body.contains("Future<bool> new("),
"`new` must never be emitted as a bare Dart identifier:\n{body}"
);
}
#[test]
fn trait_method_named_get_is_renamed_in_the_declaration() {
let body = emit(&trait_with_method(method("get", vec![param("key")])));
assert!(
body.contains("Future<bool> get_(String key);"),
"built-in identifier `get` must be renamed in member position:\n{body}"
);
assert!(
!body.contains("Future<bool> get("),
"`get` must never open a Dart method declaration:\n{body}"
);
}
#[test]
fn trait_method_named_set_is_renamed_in_the_declaration() {
let body = emit(&trait_with_method(method("set", vec![param("key")])));
assert!(
body.contains("Future<bool> set_(String key);"),
"built-in identifier `set` must be renamed in member position:\n{body}"
);
}
#[test]
fn trait_method_parameter_named_new_is_renamed() {
let body = emit(&trait_with_method(method("apply", vec![param("new")])));
assert!(
body.contains("Future<bool> apply(String new_);"),
"reserved word `new` must be renamed in parameter position:\n{body}"
);
}
#[test]
fn renamed_method_name_is_consistent_across_doc_block_and_declaration() {
let body = emit(&trait_with_method(method("get", vec![])));
assert!(
body.contains("/// Future<bool> get_(...) async { ... }"),
"@override doc line must show the renamed method:\n{body}"
);
assert!(
body.contains("/// get_: (...) => myInstance.get_(...),"),
"create<Trait>DartImpl doc line must show the renamed method:\n{body}"
);
assert!(
body.contains("Future<bool> get_();"),
"declaration must show the renamed method:\n{body}"
);
}
#[test]
fn ordinary_method_and_parameter_names_are_left_alone() {
let body = emit(&trait_with_method(method("process_image", vec![param("mime_type")])));
assert!(
body.contains("Future<bool> processImage(String mimeType);"),
"non-colliding names must pass through unchanged:\n{body}"
);
}
}