aboutsummaryrefslogtreecommitdiff
path: root/plugins/kotlin-as-java/src
diff options
context:
space:
mode:
authorSzymon Świstun <sswistun@virtuslab.com>2020-01-20 14:55:42 +0100
committerPaweł Marks <Kordyjan@users.noreply.github.com>2020-02-12 13:13:18 +0100
commit50e711d24b517bc93c37d89f258c9dafaa038ad1 (patch)
tree201c27b6860ac14b6ddc673ff099d74f53b4e15c /plugins/kotlin-as-java/src
parent5a432c9c62ff95779a495fb354c83f5f7c481a1d (diff)
downloaddokka-50e711d24b517bc93c37d89f258c9dafaa038ad1.tar.gz
dokka-50e711d24b517bc93c37d89f258c9dafaa038ad1.tar.bz2
dokka-50e711d24b517bc93c37d89f258c9dafaa038ad1.zip
kotlin-as-java plugin
Diffstat (limited to 'plugins/kotlin-as-java/src')
-rw-r--r--plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaDescriptorToDocumentationTranslator.kt75
-rw-r--r--plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageBuilder.kt68
-rw-r--r--plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageContentBuilder.kt65
-rw-r--r--plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPlugin.kt39
-rw-r--r--plugins/kotlin-as-java/src/main/kotlin/KotlinToJVMResolver.kt151
-rw-r--r--plugins/kotlin-as-java/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin1
-rw-r--r--plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt98
7 files changed, 497 insertions, 0 deletions
diff --git a/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaDescriptorToDocumentationTranslator.kt b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaDescriptorToDocumentationTranslator.kt
new file mode 100644
index 00000000..9c4ee9aa
--- /dev/null
+++ b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaDescriptorToDocumentationTranslator.kt
@@ -0,0 +1,75 @@
+package org.jetbrains.dokka.kotlinAsJava
+
+import org.jetbrains.dokka.analysis.DokkaResolutionFacade
+import org.jetbrains.dokka.links.Callable
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.links.withClass
+import org.jetbrains.dokka.model.*
+import org.jetbrains.dokka.model.Function
+import org.jetbrains.dokka.pages.PlatformData
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.transformers.descriptors.DRIWithPlatformInfo
+import org.jetbrains.dokka.transformers.descriptors.DescriptorToDocumentationTranslator
+import org.jetbrains.dokka.transformers.descriptors.DokkaDescriptorVisitor
+import org.jetbrains.dokka.transformers.descriptors.withEmptyInfo
+import org.jetbrains.kotlin.descriptors.*
+
+object KotlinAsJavaDescriptorToDocumentationTranslator : DescriptorToDocumentationTranslator {
+ override fun invoke(
+ moduleName: String,
+ packageFragments: Iterable<PackageFragmentDescriptor>,
+ platformData: PlatformData,
+ context: DokkaContext
+ ): Module =
+ KotlinAsJavaDokkaDescriptorVisitor(platformData, context.platforms[platformData]?.facade!!).run {
+ packageFragments.map { visitPackageFragmentDescriptor(it, DRI.topLevel.withEmptyInfo()) }
+ }.let { Module(moduleName, it) }
+}
+
+class KotlinAsJavaDokkaDescriptorVisitor(
+ platformData: PlatformData,
+ resolutionFacade: DokkaResolutionFacade
+) : DokkaDescriptorVisitor(platformData, resolutionFacade) {
+ override fun visitPackageFragmentDescriptor(
+ descriptor: PackageFragmentDescriptor,
+ parent: DRIWithPlatformInfo
+ ): Package {
+ val dri = DRI(packageName = descriptor.fqName.asString())
+ DescriptorCache.add(dri, descriptor)
+ return super.visitPackageFragmentDescriptor(descriptor, parent)
+ }
+
+ override fun visitClassDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): Classlike {
+ val dri = parent.dri.withClass(descriptor.name.asString())
+ DescriptorCache.add(dri, descriptor)
+ return super.visitClassDescriptor(descriptor, parent)
+ }
+
+ override fun visitPropertyDescriptor(descriptor: PropertyDescriptor, parent: DRIWithPlatformInfo): Property {
+ val dri = parent.dri.copy(callable = Callable.from(descriptor))
+ DescriptorCache.add(dri, descriptor)
+ return super.visitPropertyDescriptor(descriptor, parent)
+ }
+
+ override fun visitFunctionDescriptor(descriptor: FunctionDescriptor, parent: DRIWithPlatformInfo): Function {
+ val dri = parent.dri.copy(callable = Callable.from(descriptor))
+ DescriptorCache.add(dri, descriptor)
+ return super.visitFunctionDescriptor(descriptor, parent)
+ }
+
+ override fun visitConstructorDescriptor(descriptor: ConstructorDescriptor, parent: DRIWithPlatformInfo): Function {
+ val dri = parent.dri.copy(callable = Callable.from(descriptor))
+ DescriptorCache.add(dri, descriptor)
+ return super.visitConstructorDescriptor(descriptor, parent)
+ }
+
+ override fun visitPropertyAccessorDescriptor(
+ descriptor: PropertyAccessorDescriptor,
+ propertyDescriptor: PropertyDescriptor,
+ parent: DRI
+ ): Function {
+ val dri = parent.copy(callable = Callable.from(descriptor))
+ DescriptorCache.add(dri, descriptor)
+ return super.visitPropertyAccessorDescriptor(descriptor, propertyDescriptor, parent)
+ }
+} \ No newline at end of file
diff --git a/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageBuilder.kt b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageBuilder.kt
new file mode 100644
index 00000000..67a3ee86
--- /dev/null
+++ b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageBuilder.kt
@@ -0,0 +1,68 @@
+package org.jetbrains.dokka.kotlinAsJava
+
+import org.jetbrains.dokka.kotlinAsJava.conversions.asJava
+import org.jetbrains.dokka.kotlinAsJava.conversions.asStatic
+import org.jetbrains.dokka.kotlinAsJava.conversions.withClass
+import org.jetbrains.dokka.links.withClass
+import org.jetbrains.dokka.model.*
+import org.jetbrains.dokka.model.Function
+import org.jetbrains.dokka.model.Enum
+import org.jetbrains.dokka.model.doc.TagWrapper
+import org.jetbrains.dokka.pages.*
+import org.jetbrains.dokka.transformers.descriptors.KotlinClassKindTypes
+import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
+import org.jetbrains.kotlin.descriptors.Visibilities
+import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
+
+fun DeclarationDescriptor.sourceLocation(): String? = this.findPsi()?.containingFile?.virtualFile?.path
+fun <T : Documentable> List<T>.groupedByLocation(): Map<String, List<T>> =
+ this.map { DescriptorCache[it.dri]?.sourceLocation() to it }
+ .filter { it.first != null }.groupBy({ (location, _) ->
+ location!!.let { it.split("/").last().split(".").first() + "Kt" }
+ }) { it.second }
+
+class KotlinAsJavaPageBuilder(rootContentGroup: RootContentBuilder) : DefaultPageBuilder(rootContentGroup) {
+
+ data class FunsAndProps(val key: String, val funs: List<Function>, val props: List<Property>)
+
+ override fun pageForPackage(p: Package): PackagePageNode {
+
+ val funs = p.functions.groupedByLocation()
+
+ val props = p.properties.groupedByLocation()
+
+ val zipped = (funs.keys + props.keys)
+ .map { k -> FunsAndProps(k, funs[k].orEmpty(), props[k].orEmpty()) }
+
+ val classes = (p.classlikes + zipped.map { (key, funs, props) ->
+ val dri = p.dri.withClass(key)
+ Class(
+ dri = dri,
+ name = key,
+ kind = KotlinClassKindTypes.CLASS,
+ constructors = emptyList(),
+ functions = funs.map { it.withClass(key, dri).asStatic() },
+ properties = props.map { it.withClass(key, dri) },
+ classlikes = emptyList(),
+ actual = emptyList(),
+ expected = null,
+ visibility = p.platformData.map { it to Visibilities.PUBLIC }.toMap()
+ )
+ }).map { it.asJava() }
+
+ return PackagePageNode(
+ p.name, contentForPackage(p, classes), setOf(p.dri), p,
+ classes.map(::pageForClasslike)
+ )
+ }
+
+ private fun contentForPackage(p: Package, nClasses: List<Classlike>) = group(p) {
+ header(1) { text("Package ${p.name}") }
+ block("Types", 2, ContentKind.Properties, nClasses, p.platformData) {
+ link(it.name, it.dri)
+ text(it.briefDocTagString)
+ }
+ }
+
+ private fun TagWrapper.toHeaderString() = this.javaClass.toGenericString().split('.').last()
+} \ No newline at end of file
diff --git a/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageContentBuilder.kt b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageContentBuilder.kt
new file mode 100644
index 00000000..65925fb2
--- /dev/null
+++ b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPageContentBuilder.kt
@@ -0,0 +1,65 @@
+package org.jetbrains.dokka.kotlinAsJava
+
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.model.Documentable
+import org.jetbrains.dokka.model.Function
+import org.jetbrains.dokka.model.TypeWrapper
+import org.jetbrains.dokka.model.doc.DocTag
+import org.jetbrains.dokka.pages.*
+import org.jetbrains.dokka.transformers.psi.JavaTypeWrapper
+import org.jetbrains.dokka.utilities.DokkaLogger
+
+class KotlinAsJavaPageContentBuilder(
+ private val dri: Set<DRI>,
+ private val platformData: Set<PlatformData>,
+ private val kind: Kind,
+ private val commentsConverter: CommentsToContentConverter,
+ override val logger: DokkaLogger,
+ private val styles: Set<Style> = emptySet(),
+ private val extras: Set<Extra> = emptySet()
+) : DefaultPageContentBuilder(dri, platformData, kind, commentsConverter, logger, styles, extras) {
+ private val contents = mutableListOf<ContentNode>()
+
+ override fun signature(f: Function) = signature(f) {
+
+ val returnType = f.returnType
+ if (!f.isConstructor) {
+ if (returnType != null &&
+ returnType.constructorFqName != Unit::class.qualifiedName
+ ) {
+ if ((returnType as? JavaTypeWrapper)?.isPrimitive == true)
+ text(returnType.constructorFqName ?: "")
+ else
+ type(returnType)
+ text(" ")
+ } else text("void ")
+
+ }
+
+ link(f.name, f.dri)
+ text("(")
+ val params = listOfNotNull(f.receiver) + f.parameters
+ list(params) {
+ if ((it.type as? JavaTypeWrapper)?.isPrimitive == true)
+ text(it.type.constructorFqName ?: "")
+ else
+ type(it.type)
+
+ text(" ")
+ link(it.name ?: "receiver", it.dri)
+ }
+ text(")")
+ }
+
+ companion object {
+ fun group(
+ dri: Set<DRI>,
+ platformData: Set<PlatformData>,
+ kind: Kind,
+ commentsConverter: CommentsToContentConverter,
+ logger: DokkaLogger,
+ block: PageContentBuilderFunction
+ ): ContentGroup =
+ KotlinAsJavaPageContentBuilder(dri, platformData, kind, commentsConverter, logger).apply(block).build()
+ }
+} \ No newline at end of file
diff --git a/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPlugin.kt b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPlugin.kt
new file mode 100644
index 00000000..345dc9be
--- /dev/null
+++ b/plugins/kotlin-as-java/src/main/kotlin/KotlinAsJavaPlugin.kt
@@ -0,0 +1,39 @@
+package org.jetbrains.dokka.kotlinAsJava
+
+
+import org.jetbrains.dokka.CoreExtensions
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.model.Module
+import org.jetbrains.dokka.pages.ModulePageNode
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.DokkaPlugin
+import org.jetbrains.dokka.plugability.single
+import org.jetbrains.dokka.transformers.documentation.DocumentationToPageTranslator
+import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
+
+class KotlinAsJavaPlugin : DokkaPlugin() {
+ val kotlinAsJavaDescriptorToDocumentableTranslator by extending { CoreExtensions.descriptorToDocumentationTranslator with KotlinAsJavaDescriptorToDocumentationTranslator }
+ val kotlinAsJavaDocumentableToPageTranslator by extending { CoreExtensions.documentationToPageTranslator with KotlinAsJavaDocumentationToPageTranslator }
+}
+
+object DescriptorCache {
+ private val cache: HashMap<DRI, DeclarationDescriptor> = HashMap()
+
+ fun add(dri: DRI, descriptor: DeclarationDescriptor): Boolean = cache.putIfAbsent(dri, descriptor) == null
+ operator fun get(dri: DRI): DeclarationDescriptor? = cache[dri]
+}
+
+object KotlinAsJavaDocumentationToPageTranslator : DocumentationToPageTranslator {
+ override fun invoke(module: Module, context: DokkaContext): ModulePageNode =
+ KotlinAsJavaPageBuilder { node, kind, operation ->
+ KotlinAsJavaPageContentBuilder.group(
+ setOf(node.dri),
+ node.platformData,
+ kind,
+ context.single(CoreExtensions.commentsToContentConverter),
+ context.logger,
+ operation
+ )
+ }.pageForModule(module)
+
+} \ No newline at end of file
diff --git a/plugins/kotlin-as-java/src/main/kotlin/KotlinToJVMResolver.kt b/plugins/kotlin-as-java/src/main/kotlin/KotlinToJVMResolver.kt
new file mode 100644
index 00000000..87a173f3
--- /dev/null
+++ b/plugins/kotlin-as-java/src/main/kotlin/KotlinToJVMResolver.kt
@@ -0,0 +1,151 @@
+package org.jetbrains.dokka.kotlinAsJava.conversions
+
+import org.jetbrains.dokka.kotlinAsJava.DescriptorCache
+import org.jetbrains.dokka.links.*
+import org.jetbrains.dokka.model.*
+import org.jetbrains.dokka.model.Function
+import org.jetbrains.dokka.model.Enum
+import org.jetbrains.dokka.transformers.psi.JavaTypeWrapper
+import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap
+import org.jetbrains.kotlin.descriptors.FunctionDescriptor
+import org.jetbrains.kotlin.descriptors.PropertyDescriptor
+import org.jetbrains.kotlin.name.ClassId
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType
+
+fun String.getAsPrimitive(): JvmPrimitiveType? = org.jetbrains.kotlin.builtins.PrimitiveType.values()
+ .find { it.typeFqName.asString() == this }
+ ?.let { JvmPrimitiveType.get(it) }
+
+fun TypeWrapper.getAsType(classId: ClassId, fqName: String, top: Boolean): TypeWrapper {
+ val fqNameSplitted = fqName.takeIf { top }?.getAsPrimitive()?.name?.toLowerCase()
+ ?.let { listOf(it) } ?: classId.asString().split("/")
+ return JavaTypeWrapper(
+ fqNameSplitted,
+ arguments.mapNotNull { it.asJava(false) },
+ classId.toDRI(dri),
+ fqNameSplitted.last()[0].isLowerCase()
+ )
+}
+
+fun TypeWrapper?.asJava(top: Boolean = true): TypeWrapper? = this?.constructorFqName
+ ?.takeUnless { it.endsWith(".Unit") }
+ ?.let { fqName ->
+ fqName.mapToJava()
+ ?.let { getAsType(it, fqName, top) } ?: this
+ }
+
+fun Classlike.asJava(): Classlike = when {
+ this is Class -> this.asJava()
+ this is Enum -> this.asJava()
+ this is EnumEntry -> this
+ else -> throw IllegalArgumentException("$this shouldn't be here")
+}
+
+fun Class.asJava(): Class = Class(
+ dri, name, kind,
+ constructors.map { it.asJava() },
+ (functions + properties.flatMap { it.accessors }).map { it.asJava() },
+ properties, classlikes.mapNotNull { (it as? Class)?.asJava() }, expected, actual, extra, visibility
+)
+
+fun Enum.asJava(): Enum = Enum(
+ dri = dri,
+ name = name,
+ entries = entries.mapNotNull { it.asJava() as? EnumEntry },
+ constructors = constructors.map(Function::asJava),
+ functions = (functions + properties.flatMap { it.accessors }).map(Function::asJava),
+ properties = properties,
+ classlikes = classlikes.map(Classlike::asJava),
+ expected = expected,
+ actual = actual,
+ extra = extra,
+ visibility = visibility
+)
+
+fun tcAsJava(tc: TypeConstructor): TypeReference =
+ tc.fullyQualifiedName.mapToJava()
+ ?.let {
+ tc.copy(
+ fullyQualifiedName = it.asString(),
+ params = tc.params.map { it.asJava() }
+ )
+ } ?: tc
+
+fun tpAsJava(tp: TypeParam): TypeReference =
+ tp.copy(bounds = tp.bounds.map { it.asJava() })
+
+fun TypeReference.asJava(): TypeReference = when (this) {
+ is TypeConstructor -> tcAsJava(this)
+ is TypeParam -> tpAsJava(this)
+ else -> this
+}
+
+fun Callable.asJava(): Callable = copy(params = params.mapNotNull { (it as? TypeConstructor)?.asJava() })
+
+
+fun Parameter.asJava(): Parameter = Parameter(
+ dri.copy(callable = dri.callable?.asJava()),
+ name,
+ type.asJava()!!,
+ expected,
+ actual,
+ extra
+)
+
+fun Function.asJava(): Function {
+ val newName = when {
+ isConstructor -> "init"
+ else -> name
+ }
+ return Function(
+ dri.copy(callable = dri.callable?.asJava()),
+ newName,
+ returnType.asJava(),
+ isConstructor,
+ receiver,
+ parameters.map { it.asJava() },
+ expected,
+ actual,
+ extra,
+ visibility
+ )
+}
+
+private fun String.mapToJava(): ClassId? =
+ JavaToKotlinClassMap.mapKotlinToJava(FqName(this).toUnsafe())
+
+fun ClassId.toDRI(dri: DRI?): DRI = DRI(
+ packageName = packageFqName.asString(),
+ classNames = classNames(),
+ callable = dri?.callable?.asJava(),
+ extra = null,
+ target = null
+)
+
+fun ClassId.classNames(): String =
+ shortClassName.identifier + (outerClassId?.classNames()?.let { ".$it" } ?: "")
+
+fun Function.asStatic(): Function = also { it.extra.add(STATIC) }
+
+fun Property.withClass(className: String, dri: DRI): Property {
+ val nDri = dri.withClass(className).copy(
+ callable = getDescriptor()?.let { Callable.from(it) }
+ )
+ return Property(
+ nDri, name, receiver, expected, actual, extra, accessors, visibility
+ )
+}
+
+fun Function.withClass(className: String, dri: DRI): Function {
+ val nDri = dri.withClass(className).copy(
+ callable = getDescriptor()?.let { Callable.from(it) }
+ )
+ return Function(
+ nDri, name, returnType, isConstructor, receiver, parameters, expected, actual, extra, visibility
+ )
+}
+
+fun Function.getDescriptor(): FunctionDescriptor? = DescriptorCache[dri].let { it as? FunctionDescriptor }
+
+fun Property.getDescriptor(): PropertyDescriptor? = DescriptorCache[dri].let { it as? PropertyDescriptor }
diff --git a/plugins/kotlin-as-java/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/plugins/kotlin-as-java/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin
new file mode 100644
index 00000000..8ff3df82
--- /dev/null
+++ b/plugins/kotlin-as-java/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin
@@ -0,0 +1 @@
+org.jetbrains.dokka.kotlinAsJava.KotlinAsJavaPlugin
diff --git a/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt
new file mode 100644
index 00000000..c0833293
--- /dev/null
+++ b/plugins/kotlin-as-java/src/test/kotlin/KotlinAsJavaPluginTest.kt
@@ -0,0 +1,98 @@
+package kotlinAsJavaPlugin
+
+import junit.framework.Assert.fail
+import org.jetbrains.dokka.pages.ContentGroup
+import org.jetbrains.dokka.pages.ContentPage
+import org.jetbrains.dokka.pages.ContentTable
+import org.jetbrains.dokka.pages.children
+import org.junit.Test
+import testApi.testRunner.AbstractCoreTest
+
+class KotlinAsJavaPluginTest : AbstractCoreTest() {
+
+ @Test
+ fun topLevelTest() {
+ val configuration = dokkaConfiguration {
+ passes {
+ pass {
+ sourceRoots = listOf("src/")
+ }
+ }
+ }
+ testInline(
+ """
+ |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt
+ |package kotlinAsJavaPlugin
+ |
+ |object TestObj {}
+ |
+ |fun testFL(l: List<String>) = l
+ |fun testF() {}
+ |fun testF2(i: Int) = i
+ |fun testF3(to: TestObj) = to
+ |fun <T : Char> testF4(t: T) = listOf(t)
+ |val testV = 1
+ """,
+ configuration,
+ cleanupOutput = true
+ ) {
+ pagesGenerationStage = { root ->
+ val content = (root.children.firstOrNull()?.children?.firstOrNull() as? ContentPage )?.content ?: run {
+ fail("Either children or content is null")
+ }
+
+ val children =
+ if (content is ContentGroup)
+ content.children.filterIsInstance<ContentTable>().filter { it.children.isNotEmpty() }
+ else emptyList()
+
+ children.assertCount(2)
+ }
+ }
+ }
+
+ @Test
+ fun topLevelWithClassTest() {
+ val configuration = dokkaConfiguration {
+ passes {
+ pass {
+ sourceRoots = listOf("src/")
+ }
+ }
+ }
+ testInline(
+ """
+ |/src/main/kotlin/kotlinAsJavaPlugin/Test.kt
+ |package kotlinAsJavaPlugin
+ |
+ |class Test {
+ | fun testFC() {}
+ | val testVC = 1
+ |}
+ |
+ |fun testF(i: Int) = i
+ |val testV = 1
+ """,
+ configuration,
+ cleanupOutput = true
+ ) {
+ pagesGenerationStage = { root ->
+ val contentList = root.children
+ .flatMap { it.children<ContentPage>() }
+ .map { it.content }
+
+ val children = contentList.flatMap { content ->
+ if (content is ContentGroup)
+ content.children.filterIsInstance<ContentTable>().filter { it.children.isNotEmpty() }
+ else emptyList()
+ }.filterNot { it.toString().contains("<init>") }
+
+ children.assertCount(4)
+ }
+ }
+ }
+
+ private fun <T> Collection<T>.assertCount(n: Int) =
+ assert(count() == n) { "Expected $n, got ${count()}" }
+
+} \ No newline at end of file