From d41b4c65a0ace7e60f19fc9211947d894a0442f1 Mon Sep 17 00:00:00 2001 From: Błażej Kardyś Date: Wed, 4 Mar 2020 03:30:27 +0100 Subject: Working javadoc parsing --- core/build.gradle.kts | 1 - core/src/main/kotlin/Java/JavadocParser.kt | 369 --------------------- core/src/main/kotlin/links/DRI.kt | 26 +- plugins/base/build.gradle.kts | 4 + plugins/base/src/main/kotlin/DokkaBase.kt | 2 +- .../psi/DefaultPsiToDocumentableTranslator.kt | 319 ------------------ .../psi/DefaultPsiToDocumentableTranslator.kt | 318 ++++++++++++++++++ .../main/kotlin/translators/psi/JavadocParser.kt | 149 +++++++++ 8 files changed, 496 insertions(+), 692 deletions(-) delete mode 100644 core/src/main/kotlin/Java/JavadocParser.kt delete mode 100644 plugins/base/src/main/kotlin/transformers/psi/DefaultPsiToDocumentableTranslator.kt create mode 100644 plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt create mode 100644 plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 365cf7e8..080e1fe6 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { api("org.jetbrains.kotlin:kotlin-compiler:$kotlin_version") implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version") - implementation("org.jsoup:jsoup:1.12.1") implementation("com.google.code.gson:gson:2.8.5") testImplementation(project(":testApi")) diff --git a/core/src/main/kotlin/Java/JavadocParser.kt b/core/src/main/kotlin/Java/JavadocParser.kt deleted file mode 100644 index 856cfa04..00000000 --- a/core/src/main/kotlin/Java/JavadocParser.kt +++ /dev/null @@ -1,369 +0,0 @@ -package org.jetbrains.dokka - -import com.intellij.psi.* -import com.intellij.psi.impl.source.tree.JavaDocElementType -import com.intellij.psi.javadoc.* -import com.intellij.psi.util.PsiTreeUtil -import org.jetbrains.dokka.model.doc.* -import org.jetbrains.dokka.utilities.DokkaLogger -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 -) : JavaDocumentationParser { - - override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { - val docComment = (element as? PsiDocCommentOwner)?.docComment ?: return DocumentationNode(emptyList()) - val nodes = - convertJavadocElements(docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() }, element) - return DocumentationNode(nodes.map { Description(it) }) - /*val firstParagraphContents = nodes.takeWhile { it !is ContentParagraph } - val firstParagraph = ContentParagraph() - if (firstParagraphContents.isNotEmpty()) { - firstParagraphContents.forEach { firstParagraph.append(it) } - result.add(firstParagraph) - } - - result.appendAll(nodes.drop(firstParagraphContents.size)) - - if (element is PsiMethod) { - val tagsByName = element.searchInheritedTags() - for ((tagName, tags) in tagsByName) { - for ((tag, context) in tags) { - val section = result.addSection(javadocSectionDisplayName(tagName), tag.getSubjectName()) - val signature = signatureProvider.signature(element) - when (tagName) { - "param" -> { - section.appendTypeElement(signature) { - it.details - .find { node -> node.kind == NodeKind.Parameter && node.name == tag.getSubjectName() } - ?.detailOrNull(NodeKind.Type) - } - } - "return" -> { - section.appendTypeElement(signature) { it.detailOrNull(NodeKind.Type) } - } - } - section.appendAll(convertJavadocElements(tag.contentElements(), context)) - } - } - } - - docComment.tags.forEach { tag -> - when (tag.name) { - "see" -> result.convertSeeTag(tag) - "deprecated" -> { - deprecatedContent = Content().apply { - appendAll(convertJavadocElements(tag.contentElements(), element)) - } - } - in tagsToInherit -> {} - else -> { - val subjectName = tag.getSubjectName() - val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName) - - section.appendAll(convertJavadocElements(tag.contentElements(), element)) - } - } - } - return JavadocParseResult(result, deprecatedContent)*/ - } - - private val tagsToInherit = setOf("param", "return", "throws") - - private data class TagWithContext(val tag: PsiDocTag, val context: PsiNamedElement) -/* - private fun PsiMethod.searchInheritedTags(): Map> { - - val output = tagsToInherit.keysToMap { mutableMapOf() } - - fun recursiveSearch(methods: Array) { - for (method in methods) { - recursiveSearch(method.findSuperMethods()) - } - for (method in methods) { - for (tag in method.docComment?.tags.orEmpty()) { - if (tag.name in tagsToInherit) { - output[tag.name]!![tag.getSubjectName()] = TagWithContext(tag, method) - } - } - } - } - - recursiveSearch(arrayOf(this)) - return output.mapValues { it.value.values } - } -*/ - - private fun PsiDocTag.contentElements(): Iterable { - val tagValueElements = children - .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME } - .dropWhile { it is PsiWhiteSpace } - .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS } - return if (getSubjectName() != null) tagValueElements.dropWhile { it is PsiDocTagValue } else tagValueElements - } - - private fun convertJavadocElements(elements: Iterable, element: PsiNamedElement): List { - val doc = Jsoup.parse(expandAllForElements(elements, element)) - return doc.body().childNodes().mapNotNull { convertHtmlNode(it) } - } - - private fun expandAllForElements(elements: Iterable, element: PsiNamedElement): String { - val htmlBuilder = StringBuilder() - elements.forEach { - if (it is PsiInlineDocTag) { - htmlBuilder.append(convertInlineDocTag(it, element)) - } else { - htmlBuilder.append(it.text) - } - } - return htmlBuilder.toString().trim() - } - - 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(children) - "b" -> B(children) - "strong" -> Strong(children) - "i" -> I(children) - "em" -> Em(children) -// "s", "del" -> ContentStrikethrough() - "code" -> Code(children) - "pre" -> Pre(children) - "ul" -> Ul(children) - "ol" -> Ol(children) - "li" -> Li(children) -// "a" -> createLink(element) -// "br" -> ContentBlock().apply { hardLineBreak() } - else -> Text(body = element.ownText()) - } - } -/* - - private fun createLink(element: Element): DocTag { - return when { - element.hasAttr("docref") -> { - val docref = element.attr("docref") - ContentNodeLazyLink(docref) { refGraph.lookupOrWarn(docref, logger) } - } - element.hasAttr("href") -> { - val href = element.attr("href") - - val uri = try { - URI(href) - } catch (_: Exception) { - null - } - - if (uri?.isAbsolute == false) { - ContentLocalLink(href) - } else { - ContentExternalLink(href) - } - } - element.hasAttr("name") -> { - ContentBookmark(element.attr("name")) - } - else -> ContentBlock() - } - } - - - private fun convertSeeTag(tag: PsiDocTag) { - val linkElement = tag.linkElement() ?: return - val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null) - - val valueElement = tag.referenceElement() - val externalLink = resolveExternalLink(valueElement) - val text = ContentText(linkElement.text) - - val linkSignature by lazy { resolveInternalLink(valueElement) } - val node = when { - externalLink != null -> { - val linkNode = ContentExternalLink(externalLink) - linkNode.append(text) - linkNode - } - linkSignature != null -> { - val linkNode = - ContentNodeLazyLink( - (tag.valueElement ?: linkElement).text - ) { refGraph.lookupOrWarn(linkSignature!!, logger) } - linkNode.append(text) - linkNode - } - else -> text - } - seeSection.append(node) - } -*/ - private fun convertInlineDocTag(tag: PsiInlineDocTag, element: PsiNamedElement) = when (tag.name) { - "link", "linkplain" -> { - val valueElement = tag.referenceElement() - val externalLink = resolveExternalLink(valueElement) - val linkSignature by lazy { resolveInternalLink(valueElement) } - if (externalLink != null || linkSignature != null) { - val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: valueElement!!.text - val linkTarget = if (externalLink != null) "href=\"$externalLink\"" else "docref=\"$linkSignature\"" - val link = "$labelText" - if (tag.name == "link") "$link" else link - } else if (valueElement != null) { - valueElement.text - } else { - "" - } - } - "code", "literal" -> { - val text = StringBuilder() - tag.dataElements.forEach { text.append(it.text) } - val escaped = text.toString().trimStart() - if (tag.name == "code") "$escaped" else escaped - } - "inheritDoc" -> { - val result = (element as? PsiMethod)?.let { - // @{inheritDoc} is only allowed on functions - val parent = tag.parent - when (parent) { - is PsiDocComment -> element.findSuperDocCommentOrWarn() - is PsiDocTag -> element.findSuperDocTagOrWarn(parent) - else -> null - } - } - result ?: tag.text - } - else -> 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 } - - private fun resolveExternalLink(valueElement: PsiElement?): String? { - /*val target = valueElement?.reference?.resolve() - if (target != null) { - return externalDocumentationLinkResolver.buildExternalDocumentationLink(target) - }*/ - return null - } - - private fun resolveInternalLink(valueElement: PsiElement?): String? { - /*val target = valueElement?.reference?.resolve() - if (target != null) { - return signatureProvider.signature(target) - }*/ - return null - } - - fun PsiDocTag.getSubjectName(): String? { - if (name == "param" || name == "throws" || name == "exception") { - return valueElement?.text - } - return null - } - - private fun PsiMethod.findSuperDocCommentOrWarn(): String { - val method = findFirstSuperMethodWithDocumentation(this) - if (method != null) { - val descriptionElements = method.docComment?.descriptionElements?.dropWhile { - it.text.trim().isEmpty() - } ?: return "" - - return expandAllForElements(descriptionElements, method) - } - logger.warn("No docs found on supertype with {@inheritDoc} method ${this.name} in ${this.containingFile.name}}") - return "" - } - - - private fun PsiMethod.findSuperDocTagOrWarn(elementToExpand: PsiDocTag): String { - val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, this) - - if (result != null) { - val (method, tag) = result - - val contentElements = tag.contentElements().dropWhile { it.text.trim().isEmpty() } - - val expandedString = expandAllForElements(contentElements, method) - - return expandedString - } - logger.warn("No docs found on supertype for @${elementToExpand.name} ${elementToExpand.getSubjectName()} with {@inheritDoc} method ${this.name} in ${this.containingFile.name}}") - return "" - } - - private fun findFirstSuperMethodWithDocumentation(current: PsiMethod): PsiMethod? { - val superMethods = current.findSuperMethods() - for (method in superMethods) { - val docs = method.docComment?.descriptionElements?.dropWhile { it.text.trim().isEmpty() } - if (!docs.isNullOrEmpty()) { - return method - } - } - for (method in superMethods) { - val result = findFirstSuperMethodWithDocumentation(method) - if (result != null) { - return result - } - } - - return null - } - - private fun findFirstSuperMethodWithDocumentationforTag(elementToExpand: PsiDocTag, current: PsiMethod): Pair? { - val superMethods = current.findSuperMethods() - val mappedFilteredTags = superMethods.map { - it to it.docComment?.tags?.filter { it.name == elementToExpand.name } - } - - for ((method, tags) in mappedFilteredTags) { - tags ?: continue - for (tag in tags) { - val (tagSubject, elementSubject) = when (tag.name) { - "throws" -> { - // match class names only for throws, ignore possibly fully qualified path - // TODO: Always match exactly here - tag.getSubjectName()?.split(".")?.last() to elementToExpand.getSubjectName()?.split(".")?.last() - } - else -> { - tag.getSubjectName() to elementToExpand.getSubjectName() - } - } - - if (tagSubject == elementSubject) { - return method to tag - } - } - } - - for (method in superMethods) { - val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, method) - if (result != null) { - return result - } - } - return null - } - -} diff --git a/core/src/main/kotlin/links/DRI.kt b/core/src/main/kotlin/links/DRI.kt index aefaf8f6..791d2b5e 100644 --- a/core/src/main/kotlin/links/DRI.kt +++ b/core/src/main/kotlin/links/DRI.kt @@ -1,6 +1,11 @@ package org.jetbrains.dokka.links +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf import org.jetbrains.kotlin.resolve.scopes.receivers.ExtensionReceiver @@ -36,6 +41,17 @@ data class DRI( ) } + fun from(psi: PsiElement) = psi.parentsWithSelf.run { + val callable = firstIsInstanceOrNull() + val params = (callable?.parameterList?.parameters).orEmpty() + val classes = filterIsInstance().toList() + DRI( + classes.lastOrNull()?.qualifiedName?.substringBeforeLast('.', ""), + classes.toList().takeIf { it.isNotEmpty() }?.asReversed()?.mapNotNull { it.name }?.joinToString("."), + callable?.let { Callable.from(it) }, + firstIsInstanceOrNull()?.let { params.indexOf(it) } + ) + } val topLevel = DRI() } } @@ -67,6 +83,12 @@ data class Callable( valueParameters.mapNotNull { TypeReference.from(it) } ) } + fun from(psi: PsiMethod) = with(psi) { + Callable( + name, + null, + parameterList.parameters.map { param -> JavaClassReference(param.type.canonicalText) }) + } } } @@ -110,7 +132,7 @@ sealed class TypeReference { } } -data class JavaClassReference(val name: String): TypeReference() { +data class JavaClassReference(val name: String) : TypeReference() { override fun toString(): String = name } @@ -132,7 +154,7 @@ data class Nullable(val wrapped: TypeReference) : TypeReference() { override fun toString() = "$wrapped?" } -object StarProjection: TypeReference() { +object StarProjection : TypeReference() { override fun toString() = "*" } diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts index fd7ae978..08f8601e 100644 --- a/plugins/base/build.gradle.kts +++ b/plugins/base/build.gradle.kts @@ -4,6 +4,10 @@ plugins { id("com.jfrog.bintray") } +dependencies { + implementation("org.jsoup:jsoup:1.12.1") +} + publishing { publications { register("basePlugin") { diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index 489da3ef..03875320 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -13,7 +13,7 @@ import org.jetbrains.dokka.base.transformers.pages.merger.FallbackPageMergerStra import org.jetbrains.dokka.base.transformers.pages.merger.PageMerger import org.jetbrains.dokka.base.transformers.pages.merger.PageMergerStrategy import org.jetbrains.dokka.base.transformers.pages.merger.SameMethodNamePageMergerStrategy -import org.jetbrains.dokka.base.transformers.psi.DefaultPsiToDocumentableTranslator +import org.jetbrains.dokka.base.translators.psi.DefaultPsiToDocumentableTranslator import org.jetbrains.dokka.base.translators.descriptors.DefaultDescriptorToDocumentableTranslator import org.jetbrains.dokka.base.translators.documentables.DefaultDocumentableToPageTranslator import org.jetbrains.dokka.plugability.DokkaPlugin diff --git a/plugins/base/src/main/kotlin/transformers/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/transformers/psi/DefaultPsiToDocumentableTranslator.kt deleted file mode 100644 index 4aea45e3..00000000 --- a/plugins/base/src/main/kotlin/transformers/psi/DefaultPsiToDocumentableTranslator.kt +++ /dev/null @@ -1,319 +0,0 @@ -package org.jetbrains.dokka.base.transformers.psi - -import com.intellij.lang.jvm.JvmModifier -import com.intellij.lang.jvm.types.JvmReferenceType -import com.intellij.psi.* -import org.jetbrains.dokka.JavadocParser -import org.jetbrains.dokka.links.Callable -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.links.JavaClassReference -import org.jetbrains.dokka.links.withClass -import org.jetbrains.dokka.model.* -import org.jetbrains.dokka.model.Annotation -import org.jetbrains.dokka.model.Enum -import org.jetbrains.dokka.model.Function -import org.jetbrains.dokka.model.properties.PropertyContainer -import org.jetbrains.dokka.pages.PlatformData -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.transformers.psi.PsiToDocumentableTranslator -import org.jetbrains.dokka.utilities.DokkaLogger -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.resolve.DescriptorUtils - -object DefaultPsiToDocumentableTranslator : PsiToDocumentableTranslator { - - override fun invoke( - moduleName: String, - psiFiles: List, - platformData: PlatformData, - context: DokkaContext - ): Module { - val docParser = - DokkaPsiParser( - platformData, - context.logger - ) - return Module( - moduleName, - psiFiles.map { psiFile -> - val dri = DRI(packageName = psiFile.packageName) - Package( - dri, - emptyList(), - emptyList(), - psiFile.classes.map { docParser.parseClasslike(it, dri) }, - emptyList(), - PlatformDependent.empty(), - listOf(platformData) - ) - }, - PlatformDependent.empty(), - listOf(platformData) - ) - } - - class DokkaPsiParser( - private val platformData: PlatformData, - logger: DokkaLogger - ) { - - private val javadocParser: JavadocParser = JavadocParser(logger) - - 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.toPlatformDependant() = - PlatformDependent(mapOf(platformData to this)) - - fun parseClasslike(psi: PsiClass, parent: DRI): Classlike = with(psi) { - val dri = parent.withClass(name.toString()) - val ancestorsSet = hashSetOf() - val superMethodsKeys = hashSetOf() - val superMethods = mutableListOf() - methods.forEach { superMethodsKeys.add(it.hash) } - fun addAncestors(element: PsiClass) { - ancestorsSet.add(element.toDRI()) - element.interfaces.forEach(::addAncestors) - element.superClass?.let(::addAncestors) - } - - fun parseSupertypes(superTypes: Array) { - superTypes.forEach { type -> - (type as? PsiClassType)?.takeUnless { type.shouldBeIgnored }?.resolve()?.let { - it.methods.forEach { method -> - val hash = method.hash - if (!method.isConstructor && !superMethodsKeys.contains(hash) && - method.getVisibility() != Visibilities.PRIVATE - ) { - superMethodsKeys.add(hash) - superMethods.add(method) - } - } - addAncestors(it) - parseSupertypes(it.superTypes) - } - } - } - parseSupertypes(superTypes) - val (regularFunctions, accessors) = splitFunctionsAndAccessors() - val documentation = javadocParser.parseDocumentation(this).toPlatformDependant() - val allFunctions = regularFunctions.mapNotNull { if (!it.isConstructor) parseFunction(it, dri) else null } + - superMethods.map { parseFunction(it, dri, isInherited = true) } - val source = PsiDocumentableSource(this).toPlatformDependant() - val classlikes = innerClasses.map { parseClasslike(it, dri) } - val visibility = getVisibility().toPlatformDependant() - val ancestors = ancestorsSet.toList().toPlatformDependant() - return when { - isAnnotationType -> - Annotation( - name.orEmpty(), - dri, - documentation, - source, - allFunctions, - fields.mapNotNull { parseField(it, dri, accessors[it].orEmpty()) }, - classlikes, - visibility, - null, - constructors.map { parseFunction(it, dri, true) }, - listOf(platformData) - ) - isEnum -> Enum( - dri, - name.orEmpty(), - fields.filterIsInstance().map { entry -> - EnumEntry( - dri.withClass("$name.${entry.name}"), - entry.name.orEmpty(), - javadocParser.parseDocumentation(entry).toPlatformDependant(), - emptyList(), - emptyList(), - emptyList(), - listOf(platformData) - ) - }, - documentation, - source, - allFunctions, - fields.filter { it !is PsiEnumConstant }.map { parseField(it, dri, accessors[it].orEmpty()) }, - classlikes, - visibility, - null, - constructors.map { parseFunction(it, dri, true) }, - ancestors, - listOf(platformData) - ) - isInterface -> Interface( - dri, - name.orEmpty(), - documentation, - source, - allFunctions, - fields.mapNotNull { parseField(it, dri, accessors[it].orEmpty()) }, - classlikes, - visibility, - null, - mapTypeParameters(dri), - ancestors, - listOf(platformData) - ) - else -> Class( - dri, - name.orEmpty(), - constructors.map { parseFunction(it, dri, true) }, - allFunctions, - fields.mapNotNull { parseField(it, dri, accessors[it].orEmpty()) }, - classlikes, - source, - visibility, - null, - mapTypeParameters(dri), - ancestors, - documentation, - getModifier(), - listOf(platformData) - ) - } - } - - private fun parseFunction( - psi: PsiMethod, - parent: DRI, - isConstructor: Boolean = false, - isInherited: Boolean = false - ): Function { - val dri = parent.copy( - callable = Callable( - psi.name, - JavaClassReference(psi.containingClass?.name.orEmpty()), - psi.parameterList.parameters.map { parameter -> - JavaClassReference(parameter.type.canonicalText) - }) - ) - return Function( - dri, - if (isConstructor) "" else psi.name, - isConstructor, - psi.parameterList.parameters.mapIndexed { index, psiParameter -> - Parameter( - dri.copy(target = index + 1), - psiParameter.name, - javadocParser.parseDocumentation(psiParameter).toPlatformDependant(), - JavaTypeWrapper(psiParameter.type), - listOf(platformData) - ) - }, - javadocParser.parseDocumentation(psi).toPlatformDependant(), - PsiDocumentableSource(psi).toPlatformDependant(), - psi.getVisibility().toPlatformDependant(), - psi.returnType?.let { JavaTypeWrapper(type = it) } ?: JavaTypeWrapper.VOID, - psi.mapTypeParameters(dri), - null, - psi.getModifier(), - listOf(platformData), - PropertyContainer.empty() + InheritedFunction( - isInherited - ) - - ) - } - - private fun PsiModifierListOwner.getModifier() = when { - hasModifier(JvmModifier.ABSTRACT) -> WithAbstraction.Modifier.Abstract - hasModifier(JvmModifier.FINAL) -> WithAbstraction.Modifier.Final - else -> null - } - - private fun PsiTypeParameterListOwner.mapTypeParameters(dri: DRI): List { - fun mapProjections(bounds: Array) = - if (bounds.isEmpty()) listOf(Projection.Star) else bounds.mapNotNull { - (it as PsiClassType).let { classType -> - Projection.Nullable(Projection.TypeConstructor(classType.resolve()!!.toDRI(), emptyList())) - } - } - return typeParameters.mapIndexed { index, type -> - TypeParameter( - dri.copy(genericTarget = index), - type.name.orEmpty(), - javadocParser.parseDocumentation(type).toPlatformDependant(), - mapProjections(type.bounds), - listOf(platformData) - ) - } - } - - private fun PsiQualifiedNamedElement.toDRI() = - DRI(qualifiedName.orEmpty().substringBeforeLast('.', ""), name) - - 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, MutableMap>> { - val fieldNames = fields.map { it.name to it }.toMap() - val accessors = mutableMapOf>() - val regularMethods = mutableListOf() - 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, parent: DRI, accessors: List): Property { - val dri = parent.copy( - callable = Callable( - psi.name!!, // TODO: Investigate if this is indeed nullable - JavaClassReference(psi.containingClass?.name.orEmpty()), - emptyList() - ) - ) - return Property( - dri, - psi.name!!, // TODO: Investigate if this is indeed nullable - javadocParser.parseDocumentation(psi).toPlatformDependant(), - PsiDocumentableSource(psi).toPlatformDependant(), - psi.getVisibility().toPlatformDependant(), - JavaTypeWrapper(psi.type), - null, - accessors.firstOrNull { it.hasParameters() }?.let { parseFunction(it, parent) }, - accessors.firstOrNull { it.returnType == psi.type }?.let { parseFunction(it, parent) }, - psi.getModifier(), - listOf(platformData) - ) - } - } -} 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..3e95865e --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -0,0 +1,318 @@ +package org.jetbrains.dokka.base.translators.psi + +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.types.JvmReferenceType +import com.intellij.psi.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.JavaClassReference +import org.jetbrains.dokka.links.withClass +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.Annotation +import org.jetbrains.dokka.model.Enum +import org.jetbrains.dokka.model.Function +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.PlatformData +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.psi.PsiToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +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.resolve.DescriptorUtils + +object DefaultPsiToDocumentableTranslator : PsiToDocumentableTranslator { + + override fun invoke( + moduleName: String, + psiFiles: List, + platformData: PlatformData, + context: DokkaContext + ): Module { + val docParser = + DokkaPsiParser( + platformData, + context.logger + ) + return Module( + moduleName, + psiFiles.map { psiFile -> + val dri = DRI(packageName = psiFile.packageName) + Package( + dri, + emptyList(), + emptyList(), + psiFile.classes.map { docParser.parseClasslike(it, dri) }, + emptyList(), + PlatformDependent.empty(), + listOf(platformData) + ) + }, + PlatformDependent.empty(), + listOf(platformData) + ) + } + + class DokkaPsiParser( + private val platformData: PlatformData, + logger: DokkaLogger + ) { + + private val javadocParser: JavaDocumentationParser = JavadocParser(logger) + + 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.toPlatformDependant() = + PlatformDependent(mapOf(platformData to this)) + + fun parseClasslike(psi: PsiClass, parent: DRI): Classlike = with(psi) { + val dri = parent.withClass(name.toString()) + val ancestorsSet = hashSetOf() + val superMethodsKeys = hashSetOf() + val superMethods = mutableListOf() + methods.forEach { superMethodsKeys.add(it.hash) } + fun addAncestors(element: PsiClass) { + ancestorsSet.add(element.toDRI()) + element.interfaces.forEach(::addAncestors) + element.superClass?.let(::addAncestors) + } + + fun parseSupertypes(superTypes: Array) { + superTypes.forEach { type -> + (type as? PsiClassType)?.takeUnless { type.shouldBeIgnored }?.resolve()?.let { + it.methods.forEach { method -> + val hash = method.hash + if (!method.isConstructor && !superMethodsKeys.contains(hash) && + method.getVisibility() != Visibilities.PRIVATE + ) { + superMethodsKeys.add(hash) + superMethods.add(method) + } + } + addAncestors(it) + parseSupertypes(it.superTypes) + } + } + } + parseSupertypes(superTypes) + val (regularFunctions, accessors) = splitFunctionsAndAccessors() + val documentation = javadocParser.parseDocumentation(this).toPlatformDependant() + val allFunctions = regularFunctions.mapNotNull { if (!it.isConstructor) parseFunction(it, dri) else null } + + superMethods.map { parseFunction(it, dri, isInherited = true) } + val source = PsiDocumentableSource(this).toPlatformDependant() + val classlikes = innerClasses.map { parseClasslike(it, dri) } + val visibility = getVisibility().toPlatformDependant() + val ancestors = ancestorsSet.toList().toPlatformDependant() + return when { + isAnnotationType -> + Annotation( + name.orEmpty(), + dri, + documentation, + source, + allFunctions, + fields.mapNotNull { parseField(it, dri, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + constructors.map { parseFunction(it, dri, true) }, + listOf(platformData) + ) + isEnum -> Enum( + dri, + name.orEmpty(), + fields.filterIsInstance().map { entry -> + EnumEntry( + dri.withClass("$name.${entry.name}"), + entry.name.orEmpty(), + javadocParser.parseDocumentation(entry).toPlatformDependant(), + emptyList(), + emptyList(), + emptyList(), + listOf(platformData) + ) + }, + documentation, + source, + allFunctions, + fields.filter { it !is PsiEnumConstant }.map { parseField(it, dri, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + constructors.map { parseFunction(it, dri, true) }, + ancestors, + listOf(platformData) + ) + isInterface -> Interface( + dri, + name.orEmpty(), + documentation, + source, + allFunctions, + fields.mapNotNull { parseField(it, dri, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + mapTypeParameters(dri), + ancestors, + listOf(platformData) + ) + else -> Class( + dri, + name.orEmpty(), + constructors.map { parseFunction(it, dri, true) }, + allFunctions, + fields.mapNotNull { parseField(it, dri, accessors[it].orEmpty()) }, + classlikes, + source, + visibility, + null, + mapTypeParameters(dri), + ancestors, + documentation, + getModifier(), + listOf(platformData) + ) + } + } + + private fun parseFunction( + psi: PsiMethod, + parent: DRI, + isConstructor: Boolean = false, + isInherited: Boolean = false + ): Function { + val dri = parent.copy( + callable = Callable( + psi.name, + JavaClassReference(psi.containingClass?.name.orEmpty()), + psi.parameterList.parameters.map { parameter -> + JavaClassReference(parameter.type.canonicalText) + }) + ) + return Function( + dri, + if (isConstructor) "" else psi.name, + isConstructor, + psi.parameterList.parameters.mapIndexed { index, psiParameter -> + Parameter( + dri.copy(target = index + 1), + psiParameter.name, + javadocParser.parseDocumentation(psiParameter).toPlatformDependant(), + JavaTypeWrapper(psiParameter.type), + listOf(platformData) + ) + }, + javadocParser.parseDocumentation(psi).toPlatformDependant(), + PsiDocumentableSource(psi).toPlatformDependant(), + psi.getVisibility().toPlatformDependant(), + psi.returnType?.let { JavaTypeWrapper(type = it) } ?: JavaTypeWrapper.VOID, + psi.mapTypeParameters(dri), + null, + psi.getModifier(), + listOf(platformData), + PropertyContainer.empty() + InheritedFunction( + isInherited + ) + + ) + } + + private fun PsiModifierListOwner.getModifier() = when { + hasModifier(JvmModifier.ABSTRACT) -> WithAbstraction.Modifier.Abstract + hasModifier(JvmModifier.FINAL) -> WithAbstraction.Modifier.Final + else -> null + } + + private fun PsiTypeParameterListOwner.mapTypeParameters(dri: DRI): List { + fun mapProjections(bounds: Array) = + if (bounds.isEmpty()) listOf(Projection.Star) else bounds.mapNotNull { + (it as PsiClassType).let { classType -> + Projection.Nullable(Projection.TypeConstructor(classType.resolve()!!.toDRI(), emptyList())) + } + } + return typeParameters.mapIndexed { index, type -> + TypeParameter( + dri.copy(genericTarget = index), + type.name.orEmpty(), + javadocParser.parseDocumentation(type).toPlatformDependant(), + mapProjections(type.bounds), + listOf(platformData) + ) + } + } + + private fun PsiQualifiedNamedElement.toDRI() = + DRI(qualifiedName.orEmpty().substringBeforeLast('.', ""), name) + + 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, MutableMap>> { + val fieldNames = fields.map { it.name to it }.toMap() + val accessors = mutableMapOf>() + val regularMethods = mutableListOf() + 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, parent: DRI, accessors: List): Property { + val dri = parent.copy( + callable = Callable( + psi.name!!, // TODO: Investigate if this is indeed nullable + JavaClassReference(psi.containingClass?.name.orEmpty()), + emptyList() + ) + ) + return Property( + dri, + psi.name!!, // TODO: Investigate if this is indeed nullable + javadocParser.parseDocumentation(psi).toPlatformDependant(), + PsiDocumentableSource(psi).toPlatformDependant(), + psi.getVisibility().toPlatformDependant(), + JavaTypeWrapper(psi.type), + null, + accessors.firstOrNull { it.hasParameters() }?.let { parseFunction(it, parent) }, + accessors.firstOrNull { it.returnType == psi.type }?.let { parseFunction(it, parent) }, + psi.getModifier(), + listOf(platformData) + ) + } + } +} 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..5b9af028 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt @@ -0,0 +1,149 @@ +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.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.utils.addToStdlib.firstIsInstanceOrNull +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 = (element as? PsiDocCommentOwner)?.docComment ?: return DocumentationNode(emptyList()) + val nodes = mutableListOf() + 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()) + "deprecated" -> Deprecated(P(convertJavadocElements(tag.dataElements.toList()))) + else -> null + } + }) + return DocumentationNode(nodes) + } + + private fun getSeeTagElementContent(tag: PsiDocTag): List = + listOfNotNull(tag.referenceElement()?.toDocumentationLink()) + + private fun PsiDocComment.getDescription(): Description? = + convertJavadocElements(descriptionElements.dropWhile { + it.text.trim().isEmpty() + }).takeIf { it.isNotEmpty() }?.let { list -> + Description(P(list)) + } + + private fun convertJavadocElements(elements: Iterable): List = + elements.mapNotNull { + when (it) { + is PsiReference -> convertJavadocElements(it.children.toList()) + is PsiInlineDocTag -> listOfNotNull(convertInlineDocTag(it)) + is PsiDocParamRef -> listOfNotNull(it.toDocumentationLink()) + is PsiDocTagValue, + is PsiWhiteSpace -> listOfNotNull(Text(it.text)) + is LeafPsiElement -> Jsoup.parse(it.text).body().childNodes().mapNotNull { convertHtmlNode(it) } + 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(children) + "b" -> B(children) + "strong" -> Strong(children) + "i" -> I(children) + "em" -> Em(children) + "code" -> Code(children) + "pre" -> Pre(children) + "ul" -> Ul(children) + "ol" -> Ol(children) + "li" -> Li(children) + //"a" -> createLink(element, children) // TODO: add proper inline link handling + "br" -> Br + else -> Text(body = element.ownText()) + } + } +/* + + private fun createLink(element: Element, children: List): DocTag { + return when { + element.hasAttr("docref") -> { + A(children, params = mapOf("docref" to element.attr("docref"))) + } + element.hasAttr("href") -> { + val href = element.attr("href") + + val uri = try { + A(children, params = mapOf("href" to href)) + } catch (_: Exception) { + null + } + + if (uri?.isAbsolute == false) { + ContentLocalLink(href) + } else { + ContentExternalLink(href) + } + } + element.hasAttr("name") -> { + ContentBookmark(element.attr("name")) + } + else -> Text() + } + }*/ + + 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() } ?: this + DocumentationLink(dri, convertJavadocElements(listOfNotNull(label))) + } + + private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) { + "link", "linkplain" -> { + tag.referenceElement()?.toDocumentationLink(tag.dataElements.firstIsInstanceOrNull()) + } + "code", "literal" -> { + Code(listOf(Text(tag.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 } +} -- cgit