use std::fs;
use super::{types::ImportSet, *};
fn unique_test_dir(prefix: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicUsize, Ordering};
static SEQ: AtomicUsize = AtomicUsize::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("{prefix}_{}_{}", std::process::id(), seq))
}
fn body_of(src: &str) -> &str {
let mut rest = src;
while let Some((line, tail)) = rest.split_once('\n') {
if line.starts_with("//")
|| line.starts_with("package ")
|| line.starts_with("import ")
|| line.is_empty()
{
rest = tail;
if line.is_empty() && !tail.starts_with("import ") && !tail.is_empty() {
break;
}
} else {
break;
}
}
rest
}
#[test]
fn enum_class_with_from_int_companion() {
let class = KtClass::enum_("Color")
.entry(KtEnumEntry::with_args("RED", "0"))
.entry(KtEnumEntry::with_args("GREEN", "5"))
.entry(KtEnumEntry::with_args("BLUE", "6"))
.vis(KtVis::Public)
.kdoc("JVM-side surface for the native Rust `Color` enum.")
.ctor_param(
KtCtorParam::new("value", KtType::int())
.val()
.vis(KtVis::Public),
)
.companion(
KtCompanion::new().vis(KtVis::Public).member(
KtFun::new("fromInt")
.vis(KtVis::Public)
.annotation("JvmStatic")
.param(KtParam::new("value", KtType::int()))
.returns(KtType::cls("Color"))
.expr_body(KtCode::new().line("entries.first { it.value == value }")),
),
);
let src = render::render_one(&class.into(), "io.test.jni");
assert_eq!(
body_of(&src),
"\
/** JVM-side surface for the native Rust `Color` enum. */
public enum class Color(public val value: Int) {
RED(0),
GREEN(5),
BLUE(6);
public companion object {
@JvmStatic
public fun fromInt(value: Int): Color = entries.first { it.value == value }
}
}
"
);
}
#[test]
fn jvm_inline_value_class() {
let class = KtClass::value(
"ZenohId",
KtCtorParam::new("bytes", KtType::byte_array())
.val()
.vis(KtVis::Public),
)
.vis(KtVis::Public);
let src = render::render_one(&class.into(), "io.test.jni");
assert_eq!(
body_of(&src),
"\
@JvmInline
public value class ZenohId(public val bytes: ByteArray)
"
);
}
#[test]
fn abstract_class_with_volatile_property_and_supertype() {
let class = KtClass::class_with(KtClassModifier::Abstract, "NativeHandle")
.vis(KtVis::Public)
.ctor_param(KtCtorParam::new("initialPtr", KtType::long()))
.implements(KtType::cls("AutoCloseable"))
.member(
KtProperty::var("ptr")
.ty(KtType::long())
.initializer("initialPtr")
.vis(KtVis::Internal)
.annotation("Volatile"),
)
.member(
KtFun::new("peek")
.vis(KtVis::Public)
.returns(KtType::long())
.expr_body(KtCode::new().line("ptr")),
);
let src = render::render_one(&class.into(), "io.test.jni");
assert_eq!(
body_of(&src),
"\
public abstract class NativeHandle(initialPtr: Long) : AutoCloseable {
@Volatile internal var ptr: Long = initialPtr
public fun peek(): Long = ptr
}
"
);
}
#[test]
fn typed_handle_subclass_with_ctor_args_supertype() {
let class = KtClass::class_("ZThing")
.vis(KtVis::Public)
.ctor_param(KtCtorParam::new("initialPtr", KtType::long()))
.extends(KtType::cls("io.test.jni.NativeHandle"), Some("initialPtr"))
.member(
KtFun::new("close")
.annotation("Synchronized")
.modifier("override")
.body(KtCode::new().blk("if (ptr != 0L) {", |c| {
c.line("freePtr(ptr)").line("ptr = ptr or 1L")
})),
)
.companion(
KtCompanion::new().member(
KtFun::new("freePtr")
.annotation("JvmStatic")
.external()
.param(KtParam::new("ptr", KtType::long())),
),
);
let src = render::render_one(&class.into(), "io.test.jni.thing");
assert_eq!(
body_of(&src),
"\
public class ZThing(initialPtr: Long) : NativeHandle(initialPtr) {
@Synchronized
override fun close() {
if (ptr != 0L) {
freePtr(ptr)
ptr = ptr or 1L
}
}
companion object {
@JvmStatic
external fun freePtr(ptr: Long)
}
}
"
);
assert!(src.contains("import io.test.jni.NativeHandle"), "{src}");
}
#[test]
fn object_with_external_funs() {
let obj = KtClass::object_("JNINative")
.vis(KtVis::Internal)
.member(
KtFun::new("zThingNew")
.external()
.param(KtParam::new("errorSink", KtType::any()))
.returns(KtType::long()),
)
.member(
KtFun::new("zThingFree")
.external()
.param(KtParam::new("ptr", KtType::long())),
);
let src = render::render_one(&obj.into(), "io.test.jni");
assert_eq!(
body_of(&src),
"\
internal object JNINative {
external fun zThingNew(errorSink: Any): Long
external fun zThingFree(ptr: Long)
}
"
);
}
#[test]
fn top_level_fun_with_generics_named_lambda_and_default() {
let f = KtFun::new("zThingSub")
.vis(KtVis::Public)
.annotation("Suppress(\"UNCHECKED_CAST\")")
.generic("R")
.param(KtParam::new(
"thing",
KtType::cls("io.test.jni.thing.ZThing"),
))
.param(
KtParam::new(
"onError",
KtType::lambda(
[
("je".to_string(), KtType::string().nullable()),
("message".to_string(), KtType::string()),
],
KtType::var_r(),
),
)
.default("{ __de_je, __de_z0 -> error(__de_je ?: __de_z0) }"),
)
.param(KtParam::new(
"build",
KtType::lambda(
[
(
"handle".to_string(),
KtType::cls("io.test.jni.thing.ZThing"),
),
("name".to_string(), KtType::string()),
],
KtType::var_r(),
),
))
.returns(KtType::var_r())
.body(
KtCode::new()
.line("var __cap_failed = false")
.blk("val __ret = run {", |c| {
c.line("(JNINative.zThingSub(thing.ptr, build, __cap) as R)")
})
.line("if (__cap_failed) return onError(__cap_je, \"\")")
.line("return __ret"),
);
let src = render::render_one(&f.into(), "io.test.jni.thing");
assert_eq!(
body_of(&src),
"\
@Suppress(\"UNCHECKED_CAST\")
public fun <R> zThingSub(
thing: ZThing,
onError: (je: String?, message: String) -> R = { __de_je, __de_z0 -> error(__de_je ?: __de_z0) },
build: (handle: ZThing, name: String) -> R,
): R {
var __cap_failed = false
val __ret = run {
(JNINative.zThingSub(thing.ptr, build, __cap) as R)
}
if (__cap_failed) return onError(__cap_je, \"\")
return __ret
}
"
);
}
#[test]
fn unit_return_is_omitted() {
let f = KtFun::new("doIt")
.vis(KtVis::Public)
.returns(KtType::unit())
.body(KtCode::new().line("work()"));
let src = render::render_one(&f.into(), "p");
assert!(src.contains("public fun doIt() {"), "{src}");
assert!(!src.contains(": Unit"), "{src}");
}
#[test]
fn long_signature_wraps_params_one_per_line() {
let short = KtFun::new("short")
.vis(KtVis::Public)
.param(KtParam::new("a", KtType::int()))
.param(KtParam::new("b", KtType::int()))
.returns(KtType::int())
.body(KtCode::new().line("a + b"));
let src = render::render_one(&short.into(), "p");
assert!(
src.contains("public fun short(a: Int, b: Int): Int {"),
"{src}"
);
let long = KtFun::new("zSessionDeclareSubscriber")
.vis(KtVis::Public)
.param(KtParam::new("session", KtType::cls("ZSession")))
.param(KtParam::new("keyExprSel", KtType::int()))
.param(KtParam::new("keyExpr0", KtType::string().nullable()))
.param(KtParam::new("keyExpr1", KtType::cls("ZKeyExpr").nullable()))
.param(KtParam::new("onClose", KtType::lambda([], KtType::unit())))
.returns(KtType::cls("ZSubscriber"))
.body(KtCode::new().line("TODO()"));
let src = render::render_one(&long.into(), "p");
assert!(
src.contains(
"public fun zSessionDeclareSubscriber(\n \
session: ZSession,\n \
keyExprSel: Int,\n \
keyExpr0: String?,\n \
keyExpr1: ZKeyExpr?,\n \
onClose: () -> Unit,\n\
): ZSubscriber {"
),
"{src}"
);
}
#[test]
fn long_function_type_param_wraps_its_own_params() {
let cb = KtType::lambda(
[
("keyExpr".to_string(), KtType::cls("ZKeyExpr")),
("payloadToBytes".to_string(), KtType::byte_array()),
("encodingToString".to_string(), KtType::string()),
("kind".to_string(), KtType::int()),
("timestampNtp64".to_string(), KtType::long().nullable()),
("congestionControl".to_string(), KtType::int()),
(
"attachmentToBytes".to_string(),
KtType::byte_array().nullable(),
),
],
KtType::unit(),
);
let f = KtFun::new("declareSubscriber")
.vis(KtVis::Public)
.param(KtParam::new("session", KtType::cls("ZSession")))
.param(KtParam::new("callback", cb))
.param(
KtParam::new(
"onError",
KtType::lambda(
[("je".to_string(), KtType::string().nullable())],
KtType::cls("ZSubscriber"),
),
)
.default("{ __de_je -> error(__de_je ?: \"\") }"),
)
.returns(KtType::cls("ZSubscriber"))
.body(KtCode::new().line("TODO()"));
let src = render::render_one(&f.into(), "p");
assert!(
src.contains(
"public fun declareSubscriber(\n \
session: ZSession,\n \
callback: (\n \
keyExpr: ZKeyExpr,\n \
payloadToBytes: ByteArray,\n \
encodingToString: String,\n \
kind: Int,\n \
timestampNtp64: Long?,\n \
congestionControl: Int,\n \
attachmentToBytes: ByteArray?,\n \
) -> Unit,\n \
onError: (je: String?) -> ZSubscriber = { __de_je -> error(__de_je ?: \"\") },\n\
): ZSubscriber {"
),
"{src}"
);
}
#[test]
fn nested_function_type_params_wrap_recursively() {
let inner_cb = KtType::lambda(
[
("keyExpression".to_string(), KtType::cls("ZKeyExpr")),
("payloadToBytes".to_string(), KtType::byte_array()),
("encodingToString".to_string(), KtType::string()),
(
"attachmentToBytes".to_string(),
KtType::byte_array().nullable(),
),
],
KtType::unit(),
);
let register = KtType::lambda(
[
("callback".to_string(), inner_cb),
("onClose".to_string(), KtType::lambda([], KtType::unit())),
],
KtType::cls("ZSubscriber"),
);
let f = KtFun::new("declareWithNestedCallback")
.vis(KtVis::Public)
.param(KtParam::new("session", KtType::cls("ZSession")))
.param(KtParam::new("register", register))
.returns(KtType::cls("ZSubscriber"))
.body(KtCode::new().line("TODO()"));
let src = render::render_one(&f.into(), "p");
assert!(
src.contains(
"public fun declareWithNestedCallback(\n \
session: ZSession,\n \
register: (\n \
callback: (\n \
keyExpression: ZKeyExpr,\n \
payloadToBytes: ByteArray,\n \
encodingToString: String,\n \
attachmentToBytes: ByteArray?,\n \
) -> Unit,\n \
onClose: () -> Unit,\n \
) -> ZSubscriber,\n\
): ZSubscriber {"
),
"{src}"
);
}
#[test]
fn typealias_renders() {
let d = KtDecl::TypeAlias {
vis: KtVis::Public,
name: "OldName".into(),
target: KtType::cls("io.test.jni.NewName"),
};
let src = render::render_one(&d, "io.test.compat");
assert!(src.contains("public typealias OldName = NewName"), "{src}");
assert!(src.contains("import io.test.jni.NewName"), "{src}");
}
#[test]
fn import_collision_falls_back_to_fqn() {
let f = KtFun::new("f")
.param(KtParam::new("a", KtType::cls("io.a.Same")))
.param(KtParam::new("b", KtType::cls("io.b.Same")))
.body(KtCode::new());
let src = render::render_one(&f.into(), "p");
assert!(src.contains("import io.a.Same"), "{src}");
assert!(!src.contains("import io.b.Same"), "{src}");
assert!(src.contains("a: Same, b: io.b.Same"), "{src}");
}
#[test]
fn same_package_types_need_no_import() {
let f = KtFun::new("f")
.param(KtParam::new("a", KtType::cls("io.p.Local")))
.body(KtCode::new());
let src = render::render_one(&f.into(), "io.p");
assert!(!src.contains("import io.p.Local"), "{src}");
assert!(src.contains("a: Local"), "{src}");
}
#[test]
fn an_extension_receiver_renders_before_the_name() {
let f = KtFun::new("asRaw")
.generic("R")
.receiver(KtType::generic("io.other.Cb", [KtType::var_("R")]))
.returns(KtType::cls("io.p.CbRaw"))
.expr_body(KtCode::new().line("CbRaw { }"));
let src = render::render_one(&f.into(), "io.p");
assert!(src.contains("fun <R> Cb<R>.asRaw(): CbRaw"), "{src}");
assert!(src.contains("import io.other.Cb"), "{src}");
}
#[test]
fn an_extension_functions_name_is_still_a_plain_identifier() {
let file = KtFile::new("io.p").decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.p.Cb"))
.expr_body(KtCode::new().line("CbRaw { }")),
);
assert_eq!(file.validate(), vec![]);
}
#[test]
fn signature_keeps_the_extension_receiver() {
let f = KtFun::new("asRaw")
.receiver(KtType::cls("io.p.Cb"))
.body(KtCode::new());
assert!(f.signature().receiver.is_some());
}
#[test]
fn type_construction_covers_metadata_shapes() {
let mut imp = ImportSet::new("p");
for (ty, want) in [
(KtType::int(), "Int"),
(KtType::string().nullable(), "String?"),
(KtType::cls("io.zenoh.jni.keyexpr.ZKeyExpr"), "ZKeyExpr"),
(
KtType::cls("io.zenoh.jni.keyexpr.ZKeyExpr").nullable(),
"ZKeyExpr?",
),
(
KtType::generic("List", [KtType::cls("io.zenoh.jni.ZZenohId")]),
"List<ZZenohId>",
),
(
KtType::generic("List", [KtType::byte_array()]),
"List<ByteArray>",
),
(KtType::any().nullable(), "Any?"),
(KtType::var_r(), "R"),
] {
assert_eq!(ty.render(&mut imp), want);
}
}
#[test]
fn display_renders_types_verbatim() {
let fun = KtType::lambda(
[
("je".to_string(), KtType::string().nullable()),
("message".to_string(), KtType::string()),
],
KtType::cls("ZSubscriber"),
);
assert_eq!(
fun.to_string(),
"(je: String?, message: String) -> ZSubscriber"
);
let nullable_fun =
KtType::lambda([("a".to_string(), KtType::cls("X"))], KtType::cls("Y")).nullable();
assert_eq!(nullable_fun.to_string(), "((a: X) -> Y)?");
assert_eq!(
KtType::generic("List", [KtType::cls("io.zenoh.jni.ZZenohId")]).to_string(),
"List<io.zenoh.jni.ZZenohId>"
);
assert_eq!(
KtType::lambda(
[
(String::new(), KtType::int()),
(String::new(), KtType::string().nullable())
],
KtType::boolean()
)
.to_string(),
"(Int, String?) -> Boolean"
);
}
#[test]
fn merge_files_groups_by_package_and_rejects_duplicates() {
let a = KtFile::new("io.p").decl(KtClass::class_("A").vis(KtVis::Public));
let b = KtFile::new("io.p").decl(KtFun::new("f").body(KtCode::new()));
let c = KtFile::new("io.q").decl(KtClass::class_("C"));
let merged = merge_files(vec![a, b, c]).expect("merge");
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].package, "io.p");
assert_eq!(merged[0].decls.len(), 2);
let d1 = KtFile::new("io.p").decl(KtClass::class_("A"));
let d2 = KtFile::new("io.p").decl(KtClass::class_("A"));
assert!(merge_files(vec![d1, d2]).is_err());
}
#[test]
fn merged_file_path_is_flattened() {
let f = KtFile::new("io.zenoh.jni.bytes");
let p = merged_file_path(std::path::Path::new("/root"), &f, "X");
assert_eq!(p, std::path::PathBuf::from("/root/io/zenoh/jni/bytes.kt"));
let empty = KtFile::new("");
let p2 = merged_file_path(std::path::Path::new("/root"), &empty, "NativeHandle");
assert_eq!(p2, std::path::PathBuf::from("/root/NativeHandle.kt"));
}
#[test]
fn write_files_refuses_nonempty_unowned_root() {
let dir = unique_test_dir("kotlin_unowned_root");
let root = dir.join("generated");
fs::create_dir_all(&root).unwrap();
let handwritten = root.join("Main.kt");
fs::write(&handwritten, "fun main() = Unit\n").unwrap();
let err = write_files(&[KtFile::new("io.test")], &root).unwrap_err();
assert!(err.to_string().contains("ownership marker"), "{err}");
assert_eq!(
fs::read_to_string(&handwritten).unwrap(),
"fun main() = Unit\n"
);
let _ = fs::remove_dir_all(dir);
}
#[test]
fn write_files_accepts_crlf_marker() {
let dir = unique_test_dir("kotlin_crlf_marker");
let root = dir.join("generated");
fs::create_dir_all(&root).unwrap();
fs::write(
root.join(".kotlin-codegen-output"),
"kotlin-codegen output v1\r\n",
)
.unwrap();
fs::write(root.join("Stale.kt"), "package stale\n").unwrap();
let paths = write_files(&[KtFile::new("io.test")], &root)
.expect("CRLF marker must be accepted as an owned root");
assert!(paths.iter().all(|p| p.exists()));
assert!(
!root.join("Stale.kt").exists(),
"stale file wiped on rewrite"
);
let _ = fs::remove_dir_all(dir);
}
#[test]
fn write_files_replaces_marked_root_and_preserves_it_on_staging_failure() {
let dir = unique_test_dir("kotlin_owned_root");
let root = dir.join("generated");
let initial = KtFile::new("io.test").decl(KtFun::new("first").body(KtCode::new()));
write_files(&[initial], &root).unwrap();
let stale = root.join("stale.kt");
fs::write(&stale, "stale\n").unwrap();
let replacement = KtFile::new("io.test").decl(KtFun::new("second").body(KtCode::new()));
write_files(&[replacement], &root).unwrap();
assert!(!stale.exists());
assert!(root.join(".kotlin-codegen-output").exists());
assert!(root.join("io/test.kt").exists());
let escaping_output = KtFile::new("../outside").decl(KtFun::new("one").body(KtCode::new()));
assert!(write_files(&[escaping_output], &root).is_err());
assert!(root.join("io/test.kt").exists());
let _ = fs::remove_dir_all(dir);
}
#[test]
fn multiline_kdoc() {
let c = KtClass::class_("X").kdoc("First line.\n\nSecond paragraph.");
let src = render::render_one(&c.into(), "p");
assert!(
src.contains("/**\n * First line.\n *\n * Second paragraph.\n */\nclass X"),
"{src}"
);
}
#[test]
fn delegated_property_renders_by_clause() {
let p = KtProperty::val("MAGIC")
.ty(KtType::long())
.vis(KtVis::Public)
.delegate("lazy { constGetMagic(handler) }");
let src = render::render_one(&p.into(), "p");
assert!(
src.contains("public val MAGIC: Long by lazy { constGetMagic(handler) }"),
"{src}"
);
}
#[test]
fn every_kt_code_position_contributes_its_raw_imports() {
fn code(text: &str, fqn: &str) -> KtCode {
KtCode::new().line(text).import(fqn)
}
let mut f = KtFun::new("withDefault")
.vis(KtVis::Public)
.body(KtCode::new());
let mut p = KtParam::new("factory", KtType::cls("Any"));
p.default = Some(code("Factory.make()", "io.example.Factory"));
f = f.param(p);
let mut im = KtFunSig::new("run");
let mut ip = KtParam::new("codec", KtType::cls("Any"));
ip.default = Some(code("Codec.utf8()", "io.example.Codec"));
im = im.param(ip);
let iface = KtFunInterface::new("Handler", im).vis(KtVis::Public);
let mut cp = KtCtorParam::new("seed", KtType::long());
cp.default = Some(code("Seed.zero()", "io.example.Seed"));
let mut cls = KtClass::class_("Holder").vis(KtVis::Public).ctor_param(cp);
cls.supertypes.superclass = Some(KtSuperclass {
ty: KtType::cls("Base"),
args: Some(code("Anchor.of(1)", "io.example.Anchor")),
});
let enum_cls = KtClass::enum_("Kind")
.entry(KtEnumEntry {
name: "FIRST".into(),
args: Some(code("Weight.one()", "io.example.Weight")),
})
.vis(KtVis::Public);
let src = KtFile::new("io.test")
.decl(f)
.decl(iface)
.decl(cls)
.decl(enum_cls)
.render();
for fqn in [
"io.example.Factory",
"io.example.Codec",
"io.example.Seed",
"io.example.Anchor",
"io.example.Weight",
] {
assert!(
src.contains(&format!("import {fqn}")),
"missing `import {fqn}` — that slot's raw imports were dropped\n{src}"
);
}
}
#[test]
fn class_modifiers_render() {
for (modifier, keyword) in [
(KtClassModifier::Abstract, "abstract class"),
(KtClassModifier::Open, "open class"),
(KtClassModifier::Sealed, "sealed class"),
] {
let c = KtClass::class_with(modifier, "X");
let src = render::render_one(&c.into(), "io.test");
assert_eq!(body_of(&src), format!("{keyword} X\n"));
}
}
#[test]
fn value_class_holds_exactly_one_property() {
let c = KtClass::value("Id", KtCtorParam::new("bytes", KtType::byte_array()).val());
assert_eq!(c.ctor_params().len(), 1);
assert_eq!(c.ctor_params()[0].name, "bytes");
}
#[test]
fn data_class_always_has_at_least_one_property() {
let c = KtClass::data("P", KtCtorParam::new("x", KtType::int()).val())
.ctor_param(KtCtorParam::new("y", KtType::int()).val());
assert_eq!(c.ctor_params().len(), 2);
}
#[test]
#[should_panic(expected = "has no primary constructor")]
fn object_cannot_take_ctor_params() {
let _ = KtClass::object_("Obj").ctor_param(KtCtorParam::new("x", KtType::int()));
}
#[test]
#[should_panic(expected = "has no primary constructor")]
fn interface_cannot_take_ctor_params() {
let _ = KtClass::interface_("I").ctor_param(KtCtorParam::new("x", KtType::int()));
}
#[test]
#[should_panic(expected = "is not an enum class")]
fn non_enum_cannot_take_entries() {
let _ = KtClass::class_("C").entry(KtEnumEntry::new("A"));
}
#[test]
fn enum_with_primary_constructor_renders_entry_args() {
let c = KtClass::enum_("Kind")
.ctor_param(KtCtorParam::new("code", KtType::int()).val())
.entry(KtEnumEntry::with_args("A", "1"))
.entry(KtEnumEntry::with_args("B", "2"));
let src = render::render_one(&c.into(), "io.test");
assert_eq!(
body_of(&src),
"\
enum class Kind(val code: Int) {
A(1),
B(2);
}
"
);
}
#[test]
fn named_companion_object_renders_its_name() {
let c = KtClass::class_("Holder").companion(
KtCompanion::named("Factory")
.vis(KtVis::Public)
.member(KtFun::new("of").expr_body(KtCode::new().line("Holder()"))),
);
let src = render::render_one(&c.into(), "io.test");
assert_eq!(
body_of(&src),
"\
class Holder {
public companion object Factory {
fun of() = Holder()
}
}
"
);
}
#[test]
fn anonymous_companion_object_omits_the_name() {
let c = KtClass::class_("Holder")
.companion(KtCompanion::new().member(KtProperty::val("N").initializer("1")));
let src = render::render_one(&c.into(), "io.test");
assert_eq!(
body_of(&src),
"\
class Holder {
companion object {
val N = 1
}
}
"
);
}
#[test]
fn superclass_renders_before_interfaces() {
let c = KtClass::class_("ZThing")
.implements(KtType::cls("AutoCloseable"))
.extends(KtType::cls("Base"), Some("ptr"))
.implements(KtType::cls("Comparable"));
let src = render::render_one(&c.into(), "io.test");
assert_eq!(
body_of(&src),
"class ZThing : Base(ptr), AutoCloseable, Comparable\n"
);
}
#[test]
fn superclass_without_args_renders_bare() {
let c = KtClass::class_("Sub").extends(KtType::cls("Base"), None);
let src = render::render_one(&c.into(), "io.test");
assert_eq!(body_of(&src), "class Sub : Base\n");
}
#[test]
#[should_panic(expected = "Kotlin allows only one superclass")]
fn a_class_cannot_extend_twice() {
let _ = KtClass::class_("X")
.extends(KtType::cls("A"), Some("1"))
.extends(KtType::cls("B"), Some("2"));
}
#[test]
fn fun_interface_renders_its_single_abstract_method() {
let i = KtFunInterface::new(
"Handler",
KtFunSig::new("run")
.param(KtParam::new("value", KtType::long()))
.returns(KtType::boolean()),
)
.vis(KtVis::Public)
.type_param("out R");
let src = render::render_one(&i.into(), "io.test");
assert_eq!(
body_of(&src),
"\
public fun interface Handler<out R> {
fun run(value: Long): Boolean
}
"
);
}
#[test]
fn a_signature_becomes_a_bodyless_member() {
let c = KtClass::interface_("Codec").member(
KtFunSig::new("encode")
.param(KtParam::new("v", KtType::string()))
.returns(KtType::byte_array()),
);
let src = render::render_one(&c.into(), "io.test");
assert_eq!(
body_of(&src),
"\
interface Codec {
fun encode(v: String): ByteArray
}
"
);
}
#[test]
fn signature_of_a_concrete_function_drops_body_and_modifiers() {
let f = KtFun::new("close")
.modifier("override")
.param(KtParam::new("force", KtType::boolean()))
.returns(KtType::unit())
.body(KtCode::new().line("free()"));
let sig = f.signature();
assert_eq!(sig.name, "close");
assert_eq!(sig.params.len(), 1);
let back: KtFun = sig.into();
assert!(back.modifiers.is_empty());
assert!(matches!(back.body, KtBody::None));
}
#[test]
fn external_is_a_body_kind_not_a_modifier() {
let f = KtFun::new("nativeCall")
.vis(KtVis::Internal)
.modifier("inline")
.param(KtParam::new("ptr", KtType::long()))
.returns(KtType::boolean())
.external();
let src = render::render_one(&f.into(), "io.test");
assert_eq!(
body_of(&src),
"internal external inline fun nativeCall(ptr: Long): Boolean\n"
);
}
#[test]
fn external_and_a_body_are_mutually_exclusive() {
let f = KtFun::new("f")
.external()
.body(KtCode::new().line("work()"));
assert!(matches!(f.body, KtBody::Block(_)));
let g = KtFun::new("g")
.body(KtCode::new().line("work()"))
.external();
assert!(matches!(g.body, KtBody::External));
}
#[test]
#[should_panic(expected = "`external` is not a modifier here")]
fn external_cannot_be_passed_as_a_modifier_string() {
let _ = KtFun::new("f").modifier("external");
}
#[test]
fn banner_and_path_helper_are_reachable_from_the_crate_root() {
use crate as kotlin_codegen;
assert!(kotlin_codegen::KOTLIN_BANNER.starts_with("//"));
let f = KtFile::new("io.p");
assert_eq!(
kotlin_codegen::merged_file_path(std::path::Path::new("/root"), &f, "X"),
std::path::PathBuf::from("/root/io/p.kt")
);
assert!(f.render().starts_with(kotlin_codegen::KOTLIN_BANNER));
}
#[test]
fn validation_reports_every_problem_not_just_the_first() {
let f = KtFile::new("io.p")
.decl(KtClass::class_("A"))
.decl(KtClass::class_("A"))
.decl(KtClass::class_("B"))
.decl(KtClass::class_("B"))
.decl(KtProperty::val("c").initializer("1"))
.decl(KtProperty::val("c").initializer("2"));
let diags = f.validate();
assert_eq!(diags.len(), 3, "{diags:#?}");
assert!(diags.iter().all(|d| d.severity == Severity::Error));
assert!(diags.iter().all(|d| d.scope == "io.p"));
assert_eq!(
diags
.iter()
.filter(|d| d.check == Check::DuplicateType)
.count(),
2
);
assert_eq!(
diags
.iter()
.filter(|d| d.check == Check::DuplicateValue)
.count(),
1
);
}
#[test]
fn a_check_can_be_downgraded_or_switched_off() {
let f = KtFile::new("io.p")
.decl(KtClass::class_("A"))
.decl(KtClass::class_("A"));
let warned = f.validate_with(&ValidationPolicy::new().warn(Check::DuplicateType));
assert_eq!(warned.len(), 1);
assert_eq!(warned[0].severity, Severity::Warning);
let off = f.validate_with(&ValidationPolicy::new().allow(Check::DuplicateType));
assert!(off.is_empty());
}
#[test]
fn a_warning_does_not_stop_merging_but_is_returned() {
let policy = ValidationPolicy::new().warn(Check::DuplicateType);
let frags = vec![
KtFile::new("io.p").decl(KtClass::class_("A")),
KtFile::new("io.p").decl(KtClass::class_("A")),
];
let (merged, warnings) = merge_files_with(frags, &policy).expect("warnings do not stop merge");
assert_eq!(merged.len(), 1);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].severity, Severity::Warning);
let frags = vec![
KtFile::new("io.p").decl(KtClass::class_("A")),
KtFile::new("io.p").decl(KtClass::class_("A")),
];
assert!(merge_files(frags).is_err());
}
#[test]
fn validation_error_message_lists_each_diagnostic() {
let frags = vec![
KtFile::new("io.p").decl(KtClass::class_("A")),
KtFile::new("io.p").decl(KtClass::class_("A")),
KtFile::new("io.p").decl(KtClass::class_("B")),
KtFile::new("io.p").decl(KtClass::class_("B")),
];
let err = merge_files(frags).expect_err("duplicates");
let text = err.to_string();
assert!(text.contains("2 Kotlin validation error(s)"), "{text}");
assert!(text.contains("duplicate type `A`"), "{text}");
assert!(text.contains("duplicate type `B`"), "{text}");
assert!(text.contains("duplicate-type"), "{text}");
}
#[test]
fn write_files_refuses_to_write_an_invalid_file() {
let dir = unique_test_dir("kotlin_validate_write");
let root = dir.join("generated");
let bad = KtFile::new("io.p")
.decl(KtClass::class_("A"))
.decl(KtClass::class_("A"));
let err = write_files(&[bad], &root).expect_err("invalid file");
assert!(matches!(err, WriteKotlinError::Validation(_)));
assert!(!root.exists());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn invalid_identifiers_are_reported_with_their_location() {
let f = KtFile::new("io.p").decl(
KtClass::class_("My-Class")
.ctor_param(KtCtorParam::new("2fast", KtType::int()))
.member(
KtFun::new("object")
.param(KtParam::new("in", KtType::int()))
.body(KtCode::new()),
)
.companion(KtCompanion::named("val").member(KtProperty::val("ok").initializer("1"))),
);
let diags = f.validate();
let by_scope: Vec<(String, String)> = diags
.iter()
.map(|d| (d.scope.clone(), d.message.clone()))
.collect();
assert_eq!(diags.len(), 5, "{by_scope:#?}");
assert!(diags.iter().all(|d| d.check == Check::InvalidIdentifier));
assert!(by_scope.contains(&(
"io.p".to_string(),
"class name `My-Class` is not a valid Kotlin identifier".to_string()
)));
assert!(by_scope.contains(&(
"io.p/My-Class".to_string(),
"constructor parameter name `2fast` is not a valid Kotlin identifier".to_string()
)));
assert!(by_scope.contains(&(
"io.p/My-Class/object".to_string(),
"parameter name `in` is not a valid Kotlin identifier".to_string()
)));
}
#[test]
fn an_invalid_package_is_reported() {
assert!(KtFile::new("io..jni")
.validate()
.iter()
.any(|d| d.check == Check::InvalidPackage));
assert!(KtFile::new("io.object")
.validate()
.iter()
.any(|d| d.check == Check::InvalidPackage));
assert!(KtFile::new("").validate().is_empty());
}
#[test]
fn backticked_names_are_accepted() {
let f = KtFile::new("io.p")
.decl(KtClass::class_(escape_kotlin_ident("object")))
.decl(KtFun::new(escape_kotlin_ident("my name")).body(KtCode::new()));
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn free_form_generic_parameters_are_not_checked() {
let f = KtFile::new("io.p")
.decl(
KtFunInterface::new("Handler", KtFunSig::new("run"))
.type_param("out R")
.type_param("in T"),
)
.decl(
KtFun::new("pick")
.generic("T : Comparable<T>")
.body(KtCode::new()),
);
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn a_raw_blocks_name_is_an_identity_not_an_identifier() {
let f = KtFile::new("io.p").decl(KtDecl::Raw {
name: "__hoisted::singleton#1".to_string(),
code: KtCode::new().line("internal val x = 1"),
});
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn a_class_and_a_property_may_share_a_name() {
let f = KtFile::new("io.p")
.decl(KtClass::class_("Foo"))
.decl(KtProperty::val("Foo").initializer("1"))
.decl(KtFun::new("Foo").body(KtCode::new()));
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn functions_overload_on_parameter_types() {
let f = KtFile::new("io.p")
.decl(
KtFun::new("send")
.param(KtParam::new("v", KtType::int()))
.body(KtCode::new()),
)
.decl(
KtFun::new("send")
.param(KtParam::new("v", KtType::string()))
.body(KtCode::new()),
)
.decl(KtFun::new("send").body(KtCode::new()));
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn functions_with_identical_parameter_types_collide() {
let f = KtFile::new("io.p")
.decl(
KtFun::new("send")
.param(KtParam::new("v", KtType::int()))
.returns(KtType::boolean())
.body(KtCode::new()),
)
.decl(
KtFun::new("send")
.param(KtParam::new("other", KtType::int()))
.returns(KtType::long())
.body(KtCode::new()),
);
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::DuplicateFunction);
assert!(diags[0].message.contains("`send(Int)`"), "{diags:#?}");
}
#[test]
fn extensions_on_different_receivers_are_not_duplicates() {
let f = KtFile::new("io.p")
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.p.Cb"))
.body(KtCode::new()),
)
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.p.Other"))
.body(KtCode::new()),
);
assert_eq!(f.validate(), vec![]);
}
#[test]
fn two_extensions_on_the_same_receiver_are_duplicates() {
let f = KtFile::new("io.p")
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.p.Cb"))
.body(KtCode::new()),
)
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.p.Cb"))
.body(KtCode::new()),
);
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::DuplicateFunction);
assert!(diags[0].message.contains("`io.p.Cb.asRaw()`"), "{diags:#?}");
}
#[test]
fn an_extension_does_not_collide_with_a_member_of_the_same_name() {
let f = KtFile::new("io.p")
.decl(
KtFun::new("asRaw")
.receiver(KtType::cls("io.p.Cb"))
.body(KtCode::new()),
)
.decl(KtFun::new("asRaw").body(KtCode::new()));
assert_eq!(f.validate(), vec![]);
}
#[test]
fn duplicates_inside_a_class_body_are_found() {
let f = KtFile::new("io.p").decl(
KtClass::class_("Session")
.member(KtFun::new("close").body(KtCode::new()))
.member(KtFun::new("close").body(KtCode::new()))
.companion(
KtCompanion::new()
.member(KtFun::new("of").body(KtCode::new()))
.member(KtFun::new("of").body(KtCode::new())),
),
);
let diags = f.validate();
assert_eq!(diags.len(), 2, "{diags:#?}");
assert!(diags.iter().all(|d| d.check == Check::DuplicateFunction));
assert_eq!(diags[0].scope, "io.p/Session");
assert_eq!(diags[1].scope, "io.p/Session/Companion");
}
#[test]
fn a_ctor_property_collides_with_a_member_property() {
let f = KtFile::new("io.p").decl(
KtClass::class_("P")
.ctor_param(KtCtorParam::new("id", KtType::long()).val())
.member(KtProperty::val("id").initializer("0")),
);
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::DuplicateValue);
assert_eq!(diags[0].scope, "io.p/P");
}
#[test]
fn a_plain_ctor_parameter_declares_nothing() {
let f = KtFile::new("io.p").decl(
KtClass::class_("P")
.ctor_param(KtCtorParam::new("id", KtType::long()))
.member(KtProperty::val("id").initializer("0")),
);
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn duplicate_fun_interfaces_are_found() {
let f = KtFile::new("io.p")
.decl(KtFunInterface::new("Handler", KtFunSig::new("run")))
.decl(KtFunInterface::new("Handler", KtFunSig::new("run")));
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::DuplicateType);
}
#[test]
fn a_class_and_a_typealias_collide() {
let f = KtFile::new("io.p")
.decl(KtClass::class_("Foo"))
.decl(KtDecl::TypeAlias {
vis: KtVis::Public,
name: "Foo".to_string(),
target: KtType::int(),
});
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::DuplicateType);
}
#[test]
fn identical_raw_blocks_merge_but_differing_ones_are_an_error() {
let raw = |code: &str| KtDecl::Raw {
name: "__Builder".to_string(),
code: KtCode::new().line(code),
};
let merged = merge_files(vec![
KtFile::new("io.p").decl(raw("internal val __Builder = 1")),
KtFile::new("io.p").decl(raw("internal val __Builder = 1")),
])
.expect("identical raw blocks merge");
assert_eq!(merged[0].decls.len(), 1);
let err = merge_files(vec![
KtFile::new("io.p").decl(raw("internal val __Builder = 1")),
KtFile::new("io.p").decl(raw("internal val __Builder = 2")),
])
.expect_err("differing raw blocks collide");
assert!(err.to_string().contains("duplicate raw block"), "{err}");
}
#[test]
fn a_nested_class_is_its_own_scope() {
let f = KtFile::new("io.p").decl(
KtClass::class_("Outer")
.member(KtProperty::val("value").initializer("1"))
.member(KtClass::class_("Inner").member(KtProperty::val("value").initializer("2"))),
);
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn a_bare_property_is_reported() {
let f = KtFile::new("io.p").decl(KtProperty::val("x"));
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::PropertyWithoutTypeOrValue);
}
#[test]
fn a_property_with_any_one_of_the_three_is_accepted() {
let f = KtFile::new("io.p")
.decl(KtProperty::val("a").ty(KtType::int()))
.decl(KtProperty::val("b").initializer("1"))
.decl(KtProperty::val("c").delegate("lazy { 1 }"))
.decl(KtProperty::val("d").accessors(KtCode::new().line("get() = 1")));
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn enum_entries_must_call_a_declared_primary_constructor() {
let f = KtFile::new("io.p").decl(
KtClass::enum_("Kind")
.ctor_param(KtCtorParam::new("code", KtType::int()).val())
.entry(KtEnumEntry::with_args("A", "1"))
.entry(KtEnumEntry::new("B")),
);
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::EnumEntryMissingArguments);
assert_eq!(diags[0].scope, "io.p/Kind");
assert!(diags[0].message.contains("`B`"), "{diags:#?}");
}
#[test]
fn an_enum_without_a_constructor_needs_no_entry_arguments() {
let f = KtFile::new("io.p").decl(
KtClass::enum_("Kind")
.entry(KtEnumEntry::new("A"))
.entry(KtEnumEntry::new("B")),
);
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn a_bodyless_function_is_allowed_only_where_it_means_abstract() {
let ok_iface = KtFile::new("io.p")
.decl(KtClass::interface_("I").member(KtFunSig::new("run")))
.decl(KtClass::sealed_interface("S").member(KtFunSig::new("run")));
assert!(ok_iface.validate().is_empty(), "{:#?}", ok_iface.validate());
let ok_abstract = KtFile::new("io.p").decl(
KtClass::class_with(KtClassModifier::Abstract, "A")
.member(KtFun::new("run").modifier("abstract")),
);
assert!(
ok_abstract.validate().is_empty(),
"{:#?}",
ok_abstract.validate()
);
let missing_keyword = KtFile::new("io.p")
.decl(KtClass::class_with(KtClassModifier::Abstract, "A").member(KtFun::new("run")));
assert_eq!(
missing_keyword.validate()[0].check,
Check::FunctionWithoutBody
);
let top_level = KtFile::new("io.p").decl(KtFun::new("run"));
assert_eq!(top_level.validate()[0].check, Check::FunctionWithoutBody);
let concrete = KtFile::new("io.p").decl(KtClass::class_("C").member(KtFun::new("run")));
assert_eq!(concrete.validate()[0].check, Check::FunctionWithoutBody);
let ext = KtFile::new("io.p").decl(KtFun::new("run").external());
assert!(ext.validate().is_empty(), "{:#?}", ext.validate());
}
#[test]
fn a_companion_is_concrete_so_its_members_need_bodies() {
let f = KtFile::new("io.p")
.decl(KtClass::interface_("I").companion(KtCompanion::new().member(KtFun::new("of"))));
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::FunctionWithoutBody);
assert_eq!(diags[0].scope, "io.p/I/Companion");
}
#[test]
fn check_all_lists_every_variant() {
let names: std::collections::BTreeSet<&str> = Check::ALL.iter().map(|c| c.name()).collect();
assert_eq!(names.len(), Check::ALL.len());
assert!(names.iter().all(|n| !n.is_empty()));
}
#[test]
fn warn_all_lets_a_generator_adopt_validation_gradually() {
let f = KtFile::new("io.p")
.decl(KtClass::class_("A"))
.decl(KtClass::class_("A"))
.decl(KtProperty::val("bare"))
.decl(KtFun::new("nobody"));
assert!(f.validate().iter().all(|d| d.severity == Severity::Error));
let warned = f.validate_with(&ValidationPolicy::warn_all());
assert!(!warned.is_empty());
assert!(warned.iter().all(|d| d.severity == Severity::Warning));
let (merged, warnings) =
merge_files_with(vec![f], &ValidationPolicy::warn_all()).expect("warnings do not stop");
assert_eq!(merged.len(), 1);
assert_eq!(warnings.len(), warned.len());
}
#[test]
#[should_panic(expected = "must be a property")]
fn a_data_class_rejects_a_plain_constructor_parameter() {
let _ = KtClass::data("P", KtCtorParam::new("x", KtType::int()));
}
#[test]
#[should_panic(expected = "must be a property")]
fn a_data_class_rejects_a_plain_parameter_added_later() {
let _ = KtClass::data("P", KtCtorParam::new("x", KtType::int()).val())
.ctor_param(KtCtorParam::new("y", KtType::int()));
}
#[test]
fn a_data_class_accepts_val_and_var_properties() {
let c = KtClass::data("P", KtCtorParam::new("x", KtType::int()).val())
.ctor_param(KtCtorParam::new("y", KtType::int()).var());
assert_eq!(c.ctor_params().len(), 2);
}
#[test]
#[should_panic(expected = "single read-only property")]
fn a_value_class_rejects_a_var() {
let _ = KtClass::value("Id", KtCtorParam::new("v", KtType::long()).var());
}
#[test]
#[should_panic(expected = "single read-only property")]
fn a_value_class_rejects_a_plain_parameter() {
let _ = KtClass::value("Id", KtCtorParam::new("v", KtType::long()));
}
#[test]
#[should_panic(expected = "name cannot be empty")]
fn a_named_companion_rejects_an_empty_name() {
let _ = KtCompanion::named("");
}
#[test]
fn a_companion_object_can_extend_a_class() {
let c = KtClass::class_("Holder").companion(
KtCompanion::new()
.extends(KtType::cls("Base"), Some("1"))
.implements(KtType::cls("Marker"))
.member(KtProperty::val("N").initializer("1")),
);
let src = render::render_one(&c.into(), "io.test");
assert_eq!(
body_of(&src),
"\
class Holder {
companion object : Base(1), Marker {
val N = 1
}
}
"
);
}
#[test]
#[should_panic(expected = "only one superclass")]
fn a_companion_cannot_extend_twice() {
let _ = KtCompanion::new()
.extends(KtType::cls("A"), None)
.extends(KtType::cls("B"), None);
}
#[test]
#[should_panic(expected = "`external` is not a modifier here")]
fn external_with_trailing_space_is_still_rejected() {
let _ = KtFun::new("f").modifier("external ");
}
#[test]
#[should_panic(expected = "`external` is not a modifier here")]
fn external_inside_a_multi_keyword_modifier_is_rejected() {
let _ = KtFun::new("f").modifier("external inline");
}
#[test]
fn a_modifier_merely_containing_external_as_a_substring_is_fine() {
let f = KtFun::new("f").modifier("externalish").body(KtCode::new());
assert_eq!(f.modifiers, vec!["externalish".to_string()]);
}
#[test]
fn scope_paths_have_no_leading_slash_in_the_root_package() {
let f = KtFile::new("").decl(
KtClass::class_("Outer")
.member(KtProperty::val("dup").initializer("1"))
.member(KtProperty::val("dup").initializer("2")),
);
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].scope, "Outer");
}
#[test]
fn three_colliding_imports_all_report_against_the_one_owner() {
let f = KtFile::new("io.p")
.import("io.a.Same")
.import("io.b.Same")
.import("io.c.Same");
let diags = f.validate();
assert_eq!(diags.len(), 2, "{diags:#?}");
assert!(diags.iter().all(|d| d.check == Check::ImportCollision));
assert!(diags.iter().all(|d| d.message.contains("`io.a.Same`")));
}
#[test]
fn kt_decl_variants_stay_close_in_size() {
let class = std::mem::size_of::<KtClass>();
let next = std::mem::size_of::<KtFun>()
.max(std::mem::size_of::<KtProperty>())
.max(std::mem::size_of::<KtFunInterface>());
assert!(
class <= next + 200,
"KtClass is {class} bytes against a next-largest of {next}; \
box a field before clippy does it for you"
);
}
#[test]
fn a_named_companion_collides_with_a_nested_type_of_that_name() {
let f = KtFile::new("io.p").decl(
KtClass::class_("Outer")
.member(KtClass::class_("Factory"))
.companion(KtCompanion::named("Factory")),
);
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert_eq!(diags[0].check, Check::DuplicateType);
assert_eq!(diags[0].scope, "io.p/Outer");
assert!(diags[0].message.contains("companion object `Factory`"));
}
#[test]
fn a_named_companion_may_reuse_a_name_from_another_namespace() {
let f = KtFile::new("io.p").decl(
KtClass::class_("Outer")
.member(KtProperty::val("Factory").initializer("1"))
.member(KtFun::new("Factory").body(KtCode::new()))
.companion(KtCompanion::named("Factory")),
);
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn an_anonymous_companion_declares_no_name() {
let f = KtFile::new("io.p").decl(
KtClass::class_("Outer")
.member(KtClass::class_("Companion"))
.companion(KtCompanion::new()),
);
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn a_named_companion_does_not_collide_across_class_scopes() {
let f = KtFile::new("io.p")
.decl(KtClass::class_("A").companion(KtCompanion::named("Factory")))
.decl(KtClass::class_("B").companion(KtCompanion::named("Factory")))
.decl(KtClass::class_("Factory"));
assert!(f.validate().is_empty(), "{:#?}", f.validate());
}
#[test]
fn merging_carries_a_banner_override_into_the_merged_file() {
let merged = merge_files(vec![
KtFile::new("io.p").decl(KtFun::new("a").body(KtCode::new())),
KtFile::new("io.p")
.banner("// custom")
.decl(KtFun::new("b").body(KtCode::new())),
])
.expect("merge");
assert_eq!(merged[0].banner.as_deref(), Some("// custom"));
assert!(merged[0].render().starts_with("// custom\n"));
let merged = merge_files(vec![
KtFile::new("io.p").banner("// first"),
KtFile::new("io.p").banner("// second"),
])
.expect("merge");
assert_eq!(merged[0].banner.as_deref(), Some("// first"));
}
#[test]
fn a_function_type_receiver_is_parenthesized() {
let f = KtFun::new("asRaw")
.receiver(KtType::lambda(
[("value".to_string(), KtType::int())],
KtType::string(),
))
.body(KtCode::new());
let src = render::render_one(&f.into(), "io.p");
assert!(
src.contains("fun ((value: Int) -> String).asRaw()"),
"{src}"
);
}
#[test]
fn a_nullable_function_type_receiver_is_not_double_parenthesized() {
let f = KtFun::new("asRaw")
.receiver(
KtType::lambda([("value".to_string(), KtType::int())], KtType::string()).nullable(),
)
.body(KtCode::new());
let src = render::render_one(&f.into(), "io.p");
assert!(
src.contains("fun ((value: Int) -> String)?.asRaw()"),
"{src}"
);
}
#[test]
fn a_named_receiver_is_rendered_unchanged() {
let mut imports = ImportSet::new("io.p");
assert_eq!(
KtType::cls("io.other.Cb").render_receiver(&mut imports),
"Cb"
);
assert_eq!(
KtType::generic("List", [KtType::int()]).render_receiver(&mut imports),
"List<Int>"
);
assert_eq!(
KtType::cls("io.other.Cb")
.nullable()
.render_receiver(&mut imports),
"Cb?"
);
}
#[test]
fn a_duplicate_on_a_function_type_receiver_reads_as_kotlin_syntax() {
let ext = || {
KtFun::new("asRaw")
.receiver(KtType::lambda(
[("value".to_string(), KtType::int())],
KtType::unit(),
))
.body(KtCode::new())
};
let f = KtFile::new("io.p").decl(ext()).decl(ext());
let diags = f.validate();
assert_eq!(diags.len(), 1, "{diags:#?}");
assert!(
diags[0]
.message
.contains("`((value: Int) -> Unit).asRaw()`"),
"{diags:#?}"
);
}