aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src/main/kotlin/translators/psi
diff options
context:
space:
mode:
authorPaweł Marks <pmarks@virtuslab.com>2020-07-17 16:36:09 +0200
committerPaweł Marks <pmarks@virtuslab.com>2020-07-17 16:36:09 +0200
commit6996b1135f61c7d2cb60b0652c6a2691dda31990 (patch)
treed568096c25e31c28d14d518a63458b5a7526b896 /plugins/base/src/main/kotlin/translators/psi
parentde56cab76f556e5b4af0b8c8cb08d8b482b86d0a (diff)
parent1c3530dcbb50c347f80bef694829dbefe89eca77 (diff)
downloaddokka-6996b1135f61c7d2cb60b0652c6a2691dda31990.tar.gz
dokka-6996b1135f61c7d2cb60b0652c6a2691dda31990.tar.bz2
dokka-6996b1135f61c7d2cb60b0652c6a2691dda31990.zip
Merge branch 'dev-0.11.0'
Diffstat (limited to 'plugins/base/src/main/kotlin/translators/psi')
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt480
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt216
2 files changed, 696 insertions, 0 deletions
diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt
new file mode 100644
index 00000000..6f980383
--- /dev/null
+++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt
@@ -0,0 +1,480 @@
+package org.jetbrains.dokka.base.translators.psi
+
+import com.intellij.lang.jvm.JvmModifier
+import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute
+import com.intellij.lang.jvm.types.JvmReferenceType
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.psi.*
+import com.intellij.psi.impl.source.PsiClassReferenceType
+import com.intellij.psi.impl.source.PsiImmediateClassType
+import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
+import org.jetbrains.dokka.analysis.KotlinAnalysis
+import org.jetbrains.dokka.analysis.PsiDocumentableSource
+import org.jetbrains.dokka.analysis.from
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.links.DriWithKind
+import org.jetbrains.dokka.links.nextTarget
+import org.jetbrains.dokka.links.withClass
+import org.jetbrains.dokka.model.*
+import org.jetbrains.dokka.model.doc.DocumentationLink
+import org.jetbrains.dokka.model.doc.DocumentationNode
+import org.jetbrains.dokka.model.doc.Param
+import org.jetbrains.dokka.model.doc.Text
+import org.jetbrains.dokka.model.properties.PropertyContainer
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator
+import org.jetbrains.dokka.utilities.DokkaLogger
+import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
+import org.jetbrains.kotlin.descriptors.Visibilities
+import org.jetbrains.kotlin.load.java.JvmAbi
+import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName
+import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName
+import org.jetbrains.kotlin.name.Name
+import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
+import org.jetbrains.kotlin.resolve.DescriptorUtils
+import org.jetbrains.kotlin.utils.addToStdlib.safeAs
+import java.io.File
+
+class DefaultPsiToDocumentableTranslator(
+ private val kotlinAnalysis: KotlinAnalysis
+) : SourceToDocumentableTranslator {
+
+ override fun invoke(sourceSet: DokkaSourceSet, context: DokkaContext): DModule {
+
+ fun isFileInSourceRoots(file: File) : Boolean {
+ return sourceSet.sourceRoots.any { root -> file.path.startsWith(File(root.path).absolutePath) }
+ }
+
+ val (environment, _) = kotlinAnalysis[sourceSet]
+
+ val sourceRoots = environment.configuration.get(CLIConfigurationKeys.CONTENT_ROOTS)
+ ?.filterIsInstance<JavaSourceRoot>()
+ ?.mapNotNull { it.file.takeIf(::isFileInSourceRoots) }
+ ?: listOf()
+ val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file")
+
+ val psiFiles = sourceRoots.map { sourceRoot ->
+ sourceRoot.absoluteFile.walkTopDown().mapNotNull {
+ localFileSystem.findFileByPath(it.path)?.let { vFile ->
+ PsiManager.getInstance(environment.project).findFile(vFile) as? PsiJavaFile
+ }
+ }.toList()
+ }.flatten()
+
+ val docParser =
+ DokkaPsiParser(
+ sourceSet,
+ context.logger
+ )
+ return DModule(
+ sourceSet.moduleDisplayName,
+ psiFiles.mapNotNull { it.safeAs<PsiJavaFile>() }.groupBy { it.packageName }.map { (packageName, psiFiles) ->
+ val dri = DRI(packageName = packageName)
+ DPackage(
+ dri,
+ emptyList(),
+ emptyList(),
+ psiFiles.flatMap { psiFile ->
+ psiFile.classes.map { docParser.parseClasslike(it, dri) }
+ },
+ emptyList(),
+ emptyMap(),
+ null,
+ setOf(sourceSet)
+ )
+ },
+ emptyMap(),
+ null,
+ setOf(sourceSet)
+ )
+ }
+
+ class DokkaPsiParser(
+ private val sourceSetData: DokkaSourceSet,
+ private val logger: DokkaLogger
+ ) {
+
+ private val javadocParser: JavaDocumentationParser = JavadocParser(logger)
+
+ private val cachedBounds = hashMapOf<String, Bound>()
+
+ private fun PsiModifierListOwner.getVisibility() = modifierList?.children?.toList()?.let { ml ->
+ when {
+ ml.any { it.text == PsiKeyword.PUBLIC } -> JavaVisibility.Public
+ ml.any { it.text == PsiKeyword.PROTECTED } -> JavaVisibility.Protected
+ ml.any { it.text == PsiKeyword.PRIVATE } -> JavaVisibility.Private
+ else -> JavaVisibility.Default
+ }
+ } ?: JavaVisibility.Default
+
+ private val PsiMethod.hash: Int
+ get() = "$returnType $name$parameterList".hashCode()
+
+ private val PsiClassType.shouldBeIgnored: Boolean
+ get() = isClass("java.lang.Enum") || isClass("java.lang.Object")
+
+ private fun PsiClassType.isClass(qName: String): Boolean {
+ val shortName = qName.substringAfterLast('.')
+ if (className == shortName) {
+ val psiClass = resolve()
+ return psiClass?.qualifiedName == qName
+ }
+ return false
+ }
+
+ private fun <T> T.toSourceSetDependent() = mapOf(sourceSetData to this)
+
+ fun parseClasslike(psi: PsiClass, parent: DRI): DClasslike = with(psi) {
+ val dri = parent.withClass(name.toString())
+ val inheritanceTree = mutableListOf<AncestorLevel>()
+ val superMethodsKeys = hashSetOf<Int>()
+ val superMethods = mutableListOf<Pair<PsiMethod, DRI>>()
+ methods.forEach { superMethodsKeys.add(it.hash) }
+ fun parseSupertypes(superTypes: Array<PsiClassType>, level: Int = 0) {
+ if(superTypes.isEmpty()) return
+ val parsedClasses = superTypes.filter { !it.shouldBeIgnored }.mapNotNull {
+ it.resolve()?.let {
+ when {
+ it.isInterface -> DRI.from(it) to JavaClassKindTypes.INTERFACE
+ else -> DRI.from(it) to JavaClassKindTypes.CLASS
+ }
+ }
+ }
+ val (classes, interfaces) = parsedClasses.partition { it.second == JavaClassKindTypes.CLASS }
+ inheritanceTree.add(AncestorLevel(level, classes.firstOrNull()?.first, interfaces.map { it.first }))
+
+ superTypes.forEach { type ->
+ (type as? PsiClassType)?.takeUnless { type.shouldBeIgnored }?.resolve()?.let {
+ val definedAt = DRI.from(it)
+ it.methods.forEach { method ->
+ val hash = method.hash
+ if (!method.isConstructor && !superMethodsKeys.contains(hash) &&
+ method.getVisibility() != Visibilities.PRIVATE
+ ) {
+ superMethodsKeys.add(hash)
+ superMethods.add(Pair(method, definedAt))
+ }
+ }
+ parseSupertypes(it.superTypes, level + 1)
+ }
+ }
+ }
+ parseSupertypes(superTypes)
+ val (regularFunctions, accessors) = splitFunctionsAndAccessors()
+ val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent()
+ val allFunctions = regularFunctions.mapNotNull { if (!it.isConstructor) parseFunction(it) else null } +
+ superMethods.map { parseFunction(it.first, inheritedFrom = it.second) }
+ val source = PsiDocumentableSource(this).toSourceSetDependent()
+ val classlikes = innerClasses.map { parseClasslike(it, dri) }
+ val visibility = getVisibility().toSourceSetDependent()
+ val ancestors = inheritanceTree.filter { it.level == 0 }.flatMap {
+ listOfNotNull(it.superclass?.let {
+ DriWithKind(
+ dri = it,
+ kind = JavaClassKindTypes.CLASS
+ )
+ }) + it.interfaces.map { DriWithKind(dri = it, kind = JavaClassKindTypes.INTERFACE) }
+ }.toSourceSetDependent()
+ val modifiers = getModifier().toSourceSetDependent()
+ val implementedInterfacesExtra = ImplementedInterfaces(inheritanceTree.flatMap { it.interfaces }.distinct().toSourceSetDependent())
+ return when {
+ isAnnotationType ->
+ DAnnotation(
+ name.orEmpty(),
+ dri,
+ documentation,
+ null,
+ source,
+ allFunctions,
+ fields.mapNotNull { parseField(it, accessors[it].orEmpty()) },
+ classlikes,
+ visibility,
+ null,
+ constructors.map { parseFunction(it, true) },
+ mapTypeParameters(dri),
+ setOf(sourceSetData),
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
+ )
+ isEnum -> DEnum(
+ dri,
+ name.orEmpty(),
+ fields.filterIsInstance<PsiEnumConstant>().map { entry ->
+ DEnumEntry(
+ dri.withClass("${entry.name}"),
+ entry.name,
+ javadocParser.parseDocumentation(entry).toSourceSetDependent(),
+ null,
+ emptyList(),
+ emptyList(),
+ emptyList(),
+ setOf(sourceSetData),
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
+ )
+ },
+ documentation,
+ null,
+ source,
+ allFunctions,
+ fields.filter { it !is PsiEnumConstant }.map { parseField(it, accessors[it].orEmpty()) },
+ classlikes,
+ visibility,
+ null,
+ constructors.map { parseFunction(it, true) },
+ ancestors,
+ setOf(sourceSetData),
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
+ )
+ isInterface -> DInterface(
+ dri,
+ name.orEmpty(),
+ documentation,
+ null,
+ source,
+ allFunctions,
+ fields.mapNotNull { parseField(it, accessors[it].orEmpty()) },
+ classlikes,
+ visibility,
+ null,
+ mapTypeParameters(dri),
+ ancestors,
+ setOf(sourceSetData),
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
+ )
+ else -> DClass(
+ dri,
+ name.orEmpty(),
+ constructors.map { parseFunction(it, true) },
+ allFunctions,
+ fields.mapNotNull { parseField(it, accessors[it].orEmpty()) },
+ classlikes,
+ source,
+ visibility,
+ null,
+ mapTypeParameters(dri),
+ ancestors,
+ documentation,
+ null,
+ modifiers,
+ setOf(sourceSetData),
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
+ )
+ }
+ }
+
+ private fun parseFunction(
+ psi: PsiMethod,
+ isConstructor: Boolean = false,
+ inheritedFrom: DRI? = null
+ ): DFunction {
+ val dri = DRI.from(psi)
+ val docs = javadocParser.parseDocumentation(psi)
+ return DFunction(
+ dri,
+ if (isConstructor) "<init>" else psi.name,
+ isConstructor,
+ psi.parameterList.parameters.map { psiParameter ->
+ DParameter(
+ dri.copy(target = dri.target.nextTarget()),
+ psiParameter.name,
+ DocumentationNode(
+ listOfNotNull(docs.firstChildOfTypeOrNull<Param> {
+ it.firstChildOfTypeOrNull<DocumentationLink>()
+ ?.firstChildOfTypeOrNull<Text>()?.body == psiParameter.name
+ })).toSourceSetDependent(),
+ null,
+ getBound(psiParameter.type),
+ setOf(sourceSetData)
+ )
+ },
+ docs.toSourceSetDependent(),
+ null,
+ PsiDocumentableSource(psi).toSourceSetDependent(),
+ psi.getVisibility().toSourceSetDependent(),
+ psi.returnType?.let { getBound(type = it) } ?: Void,
+ psi.mapTypeParameters(dri),
+ null,
+ psi.getModifier().toSourceSetDependent(),
+ setOf(sourceSetData),
+ psi.additionalExtras().let {
+ PropertyContainer.withAll(
+ InheritedFunction(inheritedFrom.toSourceSetDependent()),
+ it.toSourceSetDependent().toAdditionalModifiers(),
+ (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent()
+ .toAnnotations()
+ )
+ }
+ )
+ }
+
+ private fun PsiModifierListOwner.additionalExtras() = listOfNotNull(
+ ExtraModifiers.JavaOnlyModifiers.Static.takeIf { hasModifier(JvmModifier.STATIC) },
+ ExtraModifiers.JavaOnlyModifiers.Native.takeIf { hasModifier(JvmModifier.NATIVE) },
+ ExtraModifiers.JavaOnlyModifiers.Synchronized.takeIf { hasModifier(JvmModifier.SYNCHRONIZED) },
+ ExtraModifiers.JavaOnlyModifiers.StrictFP.takeIf { hasModifier(JvmModifier.STRICTFP) },
+ ExtraModifiers.JavaOnlyModifiers.Transient.takeIf { hasModifier(JvmModifier.TRANSIENT) },
+ ExtraModifiers.JavaOnlyModifiers.Volatile.takeIf { hasModifier(JvmModifier.VOLATILE) },
+ ExtraModifiers.JavaOnlyModifiers.Transitive.takeIf { hasModifier(JvmModifier.TRANSITIVE) }
+ ).toSet()
+
+ private fun Set<ExtraModifiers>.toListOfAnnotations() = map {
+ if (it !is ExtraModifiers.JavaOnlyModifiers.Static)
+ Annotations.Annotation(DRI("kotlin.jvm", it.name.toLowerCase().capitalize()), emptyMap())
+ else
+ Annotations.Annotation(DRI("kotlin.jvm", "JvmStatic"), emptyMap())
+ }
+
+ private fun getBound(type: PsiType): Bound =
+ cachedBounds.getOrPut(type.canonicalText) {
+ when (type) {
+ is PsiClassReferenceType -> {
+ val resolved: PsiClass = type.resolve()
+ ?: return UnresolvedBound(type.presentableText)
+ when {
+ resolved.qualifiedName == "java.lang.Object" -> JavaObject
+ resolved is PsiTypeParameter && resolved.owner != null ->
+ OtherParameter(
+ declarationDRI = DRI.from(resolved.owner!!),
+ name = resolved.name.orEmpty()
+ )
+ else ->
+ TypeConstructor(DRI.from(resolved), type.parameters.map { getProjection(it) })
+ }
+ }
+ is PsiArrayType -> TypeConstructor(
+ DRI("kotlin", "Array"),
+ listOf(getProjection(type.componentType))
+ )
+ is PsiPrimitiveType -> if (type.name == "void") Void else PrimitiveJavaType(type.name)
+ is PsiImmediateClassType -> JavaObject
+ else -> throw IllegalStateException("${type.presentableText} is not supported by PSI parser")
+ }
+ }
+
+ private fun getVariance(type: PsiWildcardType): Projection = when {
+ type.extendsBound != PsiType.NULL -> Variance(Variance.Kind.Out, getBound(type.extendsBound))
+ type.superBound != PsiType.NULL -> Variance(Variance.Kind.In, getBound(type.superBound))
+ else -> throw IllegalStateException("${type.presentableText} has incorrect bounds")
+ }
+
+ private fun getProjection(type: PsiType): Projection = when (type) {
+ is PsiEllipsisType -> Star
+ is PsiWildcardType -> getVariance(type)
+ else -> getBound(type)
+ }
+
+ private fun PsiModifierListOwner.getModifier() = when {
+ hasModifier(JvmModifier.ABSTRACT) -> JavaModifier.Abstract
+ hasModifier(JvmModifier.FINAL) -> JavaModifier.Final
+ else -> JavaModifier.Empty
+ }
+
+ private fun PsiTypeParameterListOwner.mapTypeParameters(dri: DRI): List<DTypeParameter> {
+ fun mapBounds(bounds: Array<JvmReferenceType>): List<Bound> =
+ if (bounds.isEmpty()) emptyList() else bounds.mapNotNull {
+ (it as? PsiClassType)?.let { classType -> Nullable(getBound(classType)) }
+ }
+ return typeParameters.map { type ->
+ DTypeParameter(
+ dri.copy(target = dri.target.nextTarget()),
+ type.name.orEmpty(),
+ javadocParser.parseDocumentation(type).toSourceSetDependent(),
+ null,
+ mapBounds(type.bounds),
+ setOf(sourceSetData)
+ )
+ }
+ }
+
+ private fun PsiMethod.getPropertyNameForFunction() =
+ getAnnotation(DescriptorUtils.JVM_NAME.asString())?.findAttributeValue("name")?.text
+ ?: when {
+ JvmAbi.isGetterName(name) -> propertyNameByGetMethodName(Name.identifier(name))?.asString()
+ JvmAbi.isSetterName(name) -> propertyNamesBySetMethodName(Name.identifier(name)).firstOrNull()
+ ?.asString()
+ else -> null
+ }
+
+ private fun PsiClass.splitFunctionsAndAccessors(): Pair<MutableList<PsiMethod>, MutableMap<PsiField, MutableList<PsiMethod>>> {
+ val fieldNames = fields.map { it.name to it }.toMap()
+ val accessors = mutableMapOf<PsiField, MutableList<PsiMethod>>()
+ val regularMethods = mutableListOf<PsiMethod>()
+ methods.forEach { method ->
+ val field = method.getPropertyNameForFunction()?.let { name -> fieldNames[name] }
+ if (field != null) {
+ accessors.getOrPut(field, ::mutableListOf).add(method)
+ } else {
+ regularMethods.add(method)
+ }
+ }
+ return regularMethods to accessors
+ }
+
+ private fun parseField(psi: PsiField, accessors: List<PsiMethod>): DProperty {
+ val dri = DRI.from(psi)
+ return DProperty(
+ dri,
+ psi.name,
+ javadocParser.parseDocumentation(psi).toSourceSetDependent(),
+ null,
+ PsiDocumentableSource(psi).toSourceSetDependent(),
+ psi.getVisibility().toSourceSetDependent(),
+ getBound(psi.type),
+ null,
+ accessors.firstOrNull { it.hasParameters() }?.let { parseFunction(it) },
+ accessors.firstOrNull { it.returnType == psi.type }?.let { parseFunction(it) },
+ psi.getModifier().toSourceSetDependent(),
+ setOf(sourceSetData),
+ emptyList(),
+ psi.additionalExtras().let {
+ PropertyContainer.withAll<DProperty>(
+ it.toSourceSetDependent().toAdditionalModifiers(),
+ (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent()
+ .toAnnotations()
+ )
+ }
+ )
+ }
+
+ private fun Collection<PsiAnnotation>.toListOfAnnotations() =
+ filter { it !is KtLightAbstractAnnotation }.mapNotNull { it.toAnnotation() }
+
+ private fun JvmAnnotationAttribute.toValue(): AnnotationParameterValue = when (this) {
+ is PsiNameValuePair -> value?.toValue() ?: StringValue("")
+ else -> StringValue(this.attributeName)
+ }
+
+ private fun PsiAnnotationMemberValue.toValue(): AnnotationParameterValue? = when (this) {
+ is PsiAnnotation -> toAnnotation()?.let { AnnotationValue(it) }
+ is PsiArrayInitializerMemberValue -> ArrayValue(initializers.mapNotNull { it.toValue() })
+ is PsiReferenceExpression -> psiReference?.let { EnumValue(text ?: "", DRI.from(it)) }
+ is PsiClassObjectAccessExpression -> {
+ val psiClass = ((type as PsiImmediateClassType).parameters.single() as PsiClassReferenceType).resolve()
+ psiClass?.let { ClassValue(text ?: "", DRI.from(psiClass)) }
+ }
+ else -> StringValue(text ?: "")
+ }
+
+ private fun PsiAnnotation.toAnnotation() = psiReference?.let { psiElement ->
+ Annotations.Annotation(
+ DRI.from(psiElement),
+ attributes.filter { it !is KtLightAbstractAnnotation }.mapNotNull { it.attributeName to it.toValue() }
+ .toMap(),
+ (psiElement as PsiClass).annotations.any {
+ it.hasQualifiedName("java.lang.annotation.Documented")
+ }
+ )
+ }
+
+ private val PsiElement.psiReference
+ get() = getChildOfType<PsiJavaCodeReferenceElement>()?.resolve()
+ }
+
+ private data class AncestorLevel(val level: Int, val superclass: DRI?, val interfaces: List<DRI>)
+}
diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt
new file mode 100644
index 00000000..81955fde
--- /dev/null
+++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt
@@ -0,0 +1,216 @@
+package org.jetbrains.dokka.base.translators.psi
+
+import com.intellij.psi.*
+import com.intellij.psi.impl.source.javadoc.PsiDocParamRef
+import com.intellij.psi.impl.source.tree.JavaDocElementType
+import com.intellij.psi.impl.source.tree.LeafPsiElement
+import com.intellij.psi.javadoc.*
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.dokka.analysis.from
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.model.doc.*
+import org.jetbrains.dokka.model.doc.Deprecated
+import org.jetbrains.dokka.utilities.DokkaLogger
+import org.jetbrains.kotlin.idea.refactoring.fqName.getKotlinFqName
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
+import org.jetbrains.kotlin.utils.addToStdlib.safeAs
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Element
+import org.jsoup.nodes.Node
+import org.jsoup.nodes.TextNode
+
+interface JavaDocumentationParser {
+ fun parseDocumentation(element: PsiNamedElement): DocumentationNode
+}
+
+class JavadocParser(
+ private val logger: DokkaLogger // TODO: Add logging
+) : JavaDocumentationParser {
+
+ override fun parseDocumentation(element: PsiNamedElement): DocumentationNode {
+ val docComment = findClosestDocComment(element) ?: return DocumentationNode(emptyList())
+ val nodes = mutableListOf<TagWrapper>()
+ docComment.getDescription()?.let { nodes.add(it) }
+ nodes.addAll(docComment.tags.mapNotNull { tag ->
+ when (tag.name) {
+ "param" -> Param(P(convertJavadocElements(tag.dataElements.toList())), tag.text)
+ "throws" -> Throws(P(convertJavadocElements(tag.dataElements.toList())), tag.text)
+ "return" -> Return(P(convertJavadocElements(tag.dataElements.toList())))
+ "author" -> Author(P(convertJavadocElements(tag.dataElements.toList())))
+ "see" -> See(P(getSeeTagElementContent(tag)), tag.referenceElement()?.text.orEmpty(), null)
+ "deprecated" -> Deprecated(P(convertJavadocElements(tag.dataElements.toList())))
+ else -> null
+ }
+ })
+ return DocumentationNode(nodes)
+ }
+
+ private fun findClosestDocComment(element: PsiNamedElement): PsiDocComment? {
+ (element as? PsiDocCommentOwner)?.docComment?.run { return this }
+ if (element is PsiMethod) {
+ val superMethods = element.findSuperMethodsOrEmptyArray()
+ if (superMethods.isEmpty()) return null
+
+ if (superMethods.size == 1) {
+ return findClosestDocComment(superMethods.single())
+ }
+
+ val superMethodDocumentation = superMethods.map(::findClosestDocComment)
+ if (superMethodDocumentation.size == 1) {
+ return superMethodDocumentation.single()
+ }
+
+ logger.warn(
+ "Conflicting documentation for ${DRI.from(element)}" +
+ "${superMethods.map { DRI.from(it) }}"
+ )
+
+ /* Prioritize super class over interface */
+ val indexOfSuperClass = superMethods.indexOfFirst { method ->
+ val parent = method.parent
+ if (parent is PsiClass) !parent.isInterface
+ else false
+ }
+
+ return if (indexOfSuperClass >= 0) superMethodDocumentation[indexOfSuperClass]
+ else superMethodDocumentation.first()
+ }
+
+ return null
+ }
+
+ /**
+ * Workaround for failing [PsiMethod.findSuperMethods].
+ * This might be resolved once ultra light classes are enabled for dokka
+ * See [KT-39518](https://youtrack.jetbrains.com/issue/KT-39518)
+ */
+ private fun PsiMethod.findSuperMethodsOrEmptyArray(): Array<PsiMethod> {
+ return try {
+ /*
+ We are not even attempting to call "findSuperMethods" on all methods called "getGetter" or "getSetter"
+ on any object implementing "kotlin.reflect.KProperty", since we know that those methods will fail
+ (KT-39518). Just catching the exception is not good enough, since "findSuperMethods" will
+ print the whole exception to stderr internally and then spoil the console.
+ */
+ val kPropertyFqName = FqName("kotlin.reflect.KProperty")
+ if (
+ this.parent?.safeAs<PsiClass>()?.implementsInterface(kPropertyFqName) == true &&
+ (this.name == "getSetter" || this.name == "getGetter")
+ ) {
+ logger.warn("Skipped lookup of super methods for ${getKotlinFqName()} (KT-39518)")
+ return emptyArray()
+ }
+ findSuperMethods()
+ } catch (exception: Throwable) {
+ logger.warn("Failed to lookup of super methods for ${getKotlinFqName()} (KT-39518)")
+ emptyArray()
+ }
+ }
+
+ private fun PsiClass.implementsInterface(fqName: FqName): Boolean {
+ return allInterfaces().any { it.getKotlinFqName() == fqName }
+ }
+
+ private fun PsiClass.allInterfaces(): Sequence<PsiClass> {
+ return sequence {
+ this.yieldAll(interfaces.toList())
+ interfaces.forEach { yieldAll(it.allInterfaces()) }
+ }
+ }
+
+ private fun getSeeTagElementContent(tag: PsiDocTag): List<DocTag> =
+ listOfNotNull(tag.referenceElement()?.toDocumentationLink())
+
+ private fun PsiDocComment.getDescription(): Description? {
+ val nonEmptyDescriptionElements = descriptionElements.filter { it.text.trim().isNotEmpty() }
+ val convertedDescriptionElements = convertJavadocElements(nonEmptyDescriptionElements)
+ if (convertedDescriptionElements.isNotEmpty()) {
+ return Description(P(convertedDescriptionElements))
+ }
+
+ return null
+ }
+
+ private fun convertJavadocElements(elements: Iterable<PsiElement>): List<DocTag> =
+ elements.mapNotNull {
+ when (it) {
+ is PsiReference -> convertJavadocElements(it.children.toList())
+ is PsiInlineDocTag -> listOfNotNull(convertInlineDocTag(it))
+ is PsiDocParamRef -> listOfNotNull(it.toDocumentationLink())
+ is PsiDocTagValue,
+ is LeafPsiElement -> Jsoup.parse(it.text.trim()).body().childNodes().mapNotNull(::convertHtmlNode)
+ else -> null
+ }
+ }.flatten()
+
+ private fun convertHtmlNode(node: Node, insidePre: Boolean = false): DocTag? = when (node) {
+ is TextNode -> Text(body = if (insidePre) node.wholeText else node.text())
+ is Element -> createBlock(node)
+ else -> null
+ }
+
+ private fun createBlock(element: Element): DocTag {
+ val children = element.childNodes().mapNotNull { convertHtmlNode(it) }
+ return when (element.tagName()) {
+ "p" -> P(listOf(Br, Br) + children)
+ "b" -> B(children)
+ "strong" -> Strong(children)
+ "i" -> I(children)
+ "em" -> Em(children)
+ "code" -> CodeBlock(children)
+ "pre" -> Pre(children)
+ "ul" -> Ul(children)
+ "ol" -> Ol(children)
+ "li" -> Li(children)
+ "a" -> createLink(element, children)
+ else -> Text(body = element.ownText())
+ }
+ }
+
+ private fun createLink(element: Element, children: List<DocTag>): DocTag {
+ return when {
+ element.hasAttr("docref") -> {
+ A(children, params = mapOf("docref" to element.attr("docref")))
+ }
+ element.hasAttr("href") -> {
+ A(children, params = mapOf("href" to element.attr("href")))
+ }
+ else -> Text(children = children)
+ }
+ }
+
+ private fun PsiDocToken.isSharpToken() = tokenType.toString() == "DOC_TAG_VALUE_SHARP_TOKEN"
+
+ private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null) =
+ reference?.resolve()?.let {
+ val dri = DRI.from(it)
+ val label = labelElement ?: children.firstOrNull {
+ it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken()
+ } ?: this
+ DocumentationLink(dri, convertJavadocElements(listOfNotNull(label)))
+ }
+
+ private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) {
+ "link", "linkplain" -> {
+ tag.referenceElement()?.toDocumentationLink(tag.dataElements.firstIsInstanceOrNull<PsiDocToken>())
+ }
+ "code", "literal" -> {
+ CodeInline(listOf(Text(tag.text)))
+ }
+ "index" -> Index(tag.children.filterIsInstance<PsiDocTagValue>().map { Text(it.text) })
+ else -> Text(tag.text)
+ }
+
+ private fun PsiDocTag.referenceElement(): PsiElement? =
+ linkElement()?.let {
+ if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) {
+ PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java)
+ } else {
+ it
+ }
+ }
+
+ private fun PsiDocTag.linkElement(): PsiElement? =
+ valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
+}