diff options
author | Szymon Świstun <sswistun@virtuslab.com> | 2020-01-20 14:55:42 +0100 |
---|---|---|
committer | Paweł Marks <Kordyjan@users.noreply.github.com> | 2020-02-12 13:13:18 +0100 |
commit | 50e711d24b517bc93c37d89f258c9dafaa038ad1 (patch) | |
tree | 201c27b6860ac14b6ddc673ff099d74f53b4e15c /plugins/kotlin-as-java | |
parent | 5a432c9c62ff95779a495fb354c83f5f7c481a1d (diff) | |
download | dokka-50e711d24b517bc93c37d89f258c9dafaa038ad1.tar.gz dokka-50e711d24b517bc93c37d89f258c9dafaa038ad1.tar.bz2 dokka-50e711d24b517bc93c37d89f258c9dafaa038ad1.zip |
kotlin-as-java plugin
Diffstat (limited to 'plugins/kotlin-as-java')
8 files changed, 514 insertions, 0 deletions
diff --git a/plugins/kotlin-as-java/build.gradle.kts b/plugins/kotlin-as-java/build.gradle.kts new file mode 100644 index 00000000..5d04060f --- /dev/null +++ b/plugins/kotlin-as-java/build.gradle.kts @@ -0,0 +1,17 @@ +publishing { + publications { + register<MavenPublication>("kotlin-as-java-plugin") { + artifactId = "kotlin-as-java-plugin" + from(components["java"]) + } + } +} + +dependencies { + implementation(kotlin("stdlib-jdk8")) + compileOnly(project(":coreDependencies", configuration = "shadow")) + testImplementation(project(":core")) + testImplementation(project(":coreDependencies", configuration = "shadow")) + testImplementation(project(":testApi")) + testImplementation("junit:junit:4.13") +}
\ No newline at end of file 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 |