diff options
author | Eugene Petrenko <eugene.petrenko@gmail.com> | 2018-09-11 14:03:29 +0300 |
---|---|---|
committer | Eugene Petrenko <eugene.petrenko@gmail.com> | 2018-09-11 14:03:29 +0300 |
commit | a54070f5db04d05b48c369b211efe044fbc02a8d (patch) | |
tree | aac5889906f49f68c39e1416ea39d4d19bad008e /core/src/main/kotlin/Java | |
parent | e29f1c966056b0f7ee31dd9bd7d55f9e526fa7fe (diff) | |
parent | 0df19264ccae3d294946caf634ee15eea0c4fe4a (diff) | |
download | dokka-a54070f5db04d05b48c369b211efe044fbc02a8d.tar.gz dokka-a54070f5db04d05b48c369b211efe044fbc02a8d.tar.bz2 dokka-a54070f5db04d05b48c369b211efe044fbc02a8d.zip |
Merge remote-tracking branch 'zub/dev-multiplatf' into kotlin-website-jonnyzzz
Diffstat (limited to 'core/src/main/kotlin/Java')
-rw-r--r-- | core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt | 59 | ||||
-rw-r--r-- | core/src/main/kotlin/Java/JavadocParser.kt | 340 |
2 files changed, 326 insertions, 73 deletions
diff --git a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt index cf2b0514..1fe4d180 100644 --- a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt +++ b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt @@ -1,7 +1,9 @@ package org.jetbrains.dokka import com.google.inject.Inject +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.* +import com.intellij.psi.impl.JavaConstantExpressionEvaluator import com.intellij.psi.util.InheritanceUtil import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.asJava.elements.KtLightDeclaration @@ -11,9 +13,9 @@ import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtDeclaration import org.jetbrains.kotlin.psi.KtModifierListOwner -import java.io.File fun getSignature(element: PsiElement?) = when(element) { + is PsiPackage -> element.qualifiedName is PsiClass -> element.qualifiedName is PsiField -> element.containingClass!!.qualifiedName + "$" + element.name is PsiMethod -> @@ -41,18 +43,24 @@ interface JavaDocumentationBuilder { } class JavaPsiDocumentationBuilder : JavaDocumentationBuilder { - private val options: DocumentationOptions + private val passConfiguration: DokkaConfiguration.PassConfiguration private val refGraph: NodeReferenceGraph private val docParser: JavaDocumentationParser - @Inject constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph, logger: DokkaLogger) { - this.options = options + @Inject constructor( + passConfiguration: DokkaConfiguration.PassConfiguration, + refGraph: NodeReferenceGraph, + logger: DokkaLogger, + signatureProvider: ElementSignatureProvider, + externalDocumentationLinkResolver: ExternalDocumentationLinkResolver + ) { + this.passConfiguration = passConfiguration this.refGraph = refGraph - this.docParser = JavadocParser(refGraph, logger) + this.docParser = JavadocParser(refGraph, logger, signatureProvider, externalDocumentationLinkResolver) } - constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph, docParser: JavaDocumentationParser) { - this.options = options + constructor(passConfiguration: DokkaConfiguration.PassConfiguration, refGraph: NodeReferenceGraph, docParser: JavaDocumentationParser) { + this.passConfiguration = passConfiguration this.refGraph = refGraph this.docParser = docParser } @@ -61,7 +69,7 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder { if (skipFile(file) || file.classes.all { skipElement(it) }) { return } - val packageNode = module.findOrCreatePackageNode(file.packageName, emptyMap(), refGraph) + val packageNode = findOrCreatePackageNode(module, file.packageName, emptyMap(), refGraph) appendClasses(packageNode, file.classes) } @@ -132,21 +140,23 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder { } } - private fun skipFile(javaFile: PsiJavaFile): Boolean = options.effectivePackageOptions(javaFile.packageName).suppress + private fun skipFile(javaFile: PsiJavaFile): Boolean = passConfiguration.effectivePackageOptions(javaFile.packageName).suppress private fun skipElement(element: Any) = skipElementByVisibility(element) || hasSuppressDocTag(element) || skipElementBySuppressedFiles(element) - private fun skipElementByVisibility(element: Any): Boolean = element is PsiModifierListOwner && - !(options.effectivePackageOptions((element.containingFile as? PsiJavaFile)?.packageName ?: "").includeNonPublic) && - (element.hasModifierProperty(PsiModifier.PRIVATE) || - element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL) || - element.isInternal()) + private fun skipElementByVisibility(element: Any): Boolean = + element is PsiModifierListOwner && + element !is PsiParameter && + !(passConfiguration.effectivePackageOptions((element.containingFile as? PsiJavaFile)?.packageName ?: "").includeNonPublic) && + (element.hasModifierProperty(PsiModifier.PRIVATE) || + element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL) || + element.isInternal()) private fun skipElementBySuppressedFiles(element: Any): Boolean = - element is PsiElement && File(element.containingFile.virtualFile.path).absoluteFile in options.suppressedFiles + element is PsiElement && element.containingFile.virtualFile.path in passConfiguration.suppressedFiles private fun PsiElement.isInternal(): Boolean { val ktElement = (this as? KtLightElement<*, *>)?.kotlinOrigin ?: return false @@ -200,11 +210,28 @@ class JavaPsiDocumentationBuilder : JavaDocumentationBuilder { fun PsiField.build(): DocumentationNode { val node = nodeForElement(this, nodeKind()) node.appendType(type) - node.appendModifiers(this) + + node.appendConstantValueIfAny(this) register(this, node) return node } + private fun DocumentationNode.appendConstantValueIfAny(field: PsiField) { + val modifierList = field.modifierList ?: return + val initializer = field.initializer ?: return + if (field.type is PsiPrimitiveType && + modifierList.hasExplicitModifier(PsiModifier.FINAL) && + modifierList.hasExplicitModifier(PsiModifier.STATIC)) { + val value = JavaConstantExpressionEvaluator.computeConstantExpression(initializer, false) + val text = when(value) { + is String -> + "\"" + StringUtil.escapeStringCharacters(value) + "\"" + else -> value.toString() + } + append(DocumentationNode(text, Content.Empty, NodeKind.Value), RefKind.Detail) + } + } + private fun PsiField.nodeKind(): NodeKind = when { this is PsiEnumConstant -> NodeKind.EnumItem else -> NodeKind.Field diff --git a/core/src/main/kotlin/Java/JavadocParser.kt b/core/src/main/kotlin/Java/JavadocParser.kt index c25f5813..9f9ea017 100644 --- a/core/src/main/kotlin/Java/JavadocParser.kt +++ b/core/src/main/kotlin/Java/JavadocParser.kt @@ -1,14 +1,18 @@ package org.jetbrains.dokka import com.intellij.psi.* -import com.intellij.psi.javadoc.PsiDocTag -import com.intellij.psi.javadoc.PsiDocTagValue -import com.intellij.psi.javadoc.PsiDocToken -import com.intellij.psi.javadoc.PsiInlineDocTag +import com.intellij.psi.impl.source.javadoc.CorePsiDocTagValueImpl +import com.intellij.psi.impl.source.tree.JavaDocElementType +import com.intellij.psi.javadoc.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.IncorrectOperationException +import com.intellij.util.containers.isNullOrEmpty +import org.jetbrains.kotlin.utils.keysToMap import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.jsoup.nodes.Node import org.jsoup.nodes.TextNode +import java.net.URI data class JavadocParseResult(val content: Content, val deprecatedContent: Content?) { companion object { @@ -20,70 +24,151 @@ interface JavaDocumentationParser { fun parseDocumentation(element: PsiNamedElement): JavadocParseResult } -class JavadocParser(private val refGraph: NodeReferenceGraph, - private val logger: DokkaLogger) : JavaDocumentationParser { +class JavadocParser( + private val refGraph: NodeReferenceGraph, + private val logger: DokkaLogger, + private val signatureProvider: ElementSignatureProvider, + private val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver +) : JavaDocumentationParser { + + private fun ContentSection.appendTypeElement(signature: String, selector: (DocumentationNode) -> DocumentationNode?) { + append(LazyContentBlock { + val node = refGraph.lookupOrWarn(signature, logger)?.let(selector) + if (node != null) { + it.append(NodeRenderContent(node, LanguageService.RenderMode.SUMMARY)) + it.symbol(":") + it.text(" ") + } + }) + } + override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult { val docComment = (element as? PsiDocCommentOwner)?.docComment if (docComment == null) return JavadocParseResult.Empty val result = MutableContent() var deprecatedContent: Content? = null - val para = ContentParagraph() - result.append(para) - para.convertJavadocElements(docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() }) + val firstParagraph = ContentParagraph() + firstParagraph.convertJavadocElements(docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() }, element) + val paragraphs = firstParagraph.children.dropWhile { it !is ContentParagraph } + firstParagraph.children.removeAll(paragraphs) + if (!firstParagraph.isEmpty()) { + result.append(firstParagraph) + } + paragraphs.forEach { + result.append(it) + } + + 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 { it.kind == NodeKind.Parameter }?.detailOrNull(NodeKind.Type) + } + } + "return" -> { + section.appendTypeElement(signature) { it.detailOrNull(NodeKind.Type) } + } + } + section.convertJavadocElements(tag.contentElements(), context) + } + } + } + docComment.tags.forEach { tag -> - when(tag.name) { + when (tag.name) { "see" -> result.convertSeeTag(tag) "deprecated" -> { - deprecatedContent = Content() - deprecatedContent!!.convertJavadocElements(tag.contentElements()) + deprecatedContent = Content().apply { + convertJavadocElements(tag.contentElements(), element) + } } + in tagsToInherit -> {} else -> { val subjectName = tag.getSubjectName() val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName) - section.convertJavadocElements(tag.contentElements()) + section.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<String, Collection<TagWithContext>> { + + val output = tagsToInherit.keysToMap { mutableMapOf<String?, TagWithContext>() } + + fun recursiveSearch(methods: Array<PsiMethod>) { + 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<PsiElement> { val tagValueElements = children - .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME } - .dropWhile { it is PsiWhiteSpace } - .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS } + .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 ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>) { + private fun ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>, element: PsiNamedElement) { + val doc = Jsoup.parse(expandAllForElements(elements, element)) + doc.body().childNodes().forEach { + convertHtmlNode(it)?.let { append(it) } + } + } + + private fun expandAllForElements(elements: Iterable<PsiElement>, element: PsiNamedElement): String { val htmlBuilder = StringBuilder() elements.forEach { if (it is PsiInlineDocTag) { - htmlBuilder.append(convertInlineDocTag(it)) + htmlBuilder.append(convertInlineDocTag(it, element)) } else { htmlBuilder.append(it.text) } } - val doc = Jsoup.parse(htmlBuilder.toString().trim()) - doc.body().childNodes().forEach { - convertHtmlNode(it) - } + return htmlBuilder.toString().trim() } - private fun ContentBlock.convertHtmlNode(node: Node) { + private fun convertHtmlNode(node: Node): ContentNode? { if (node is TextNode) { - append(ContentText(node.text())) + return ContentText(node.text()) } else if (node is Element) { val childBlock = createBlock(node) node.childNodes().forEach { - childBlock.convertHtmlNode(it) + val child = convertHtmlNode(it) + if (child != null) { + childBlock.append(child) + } } - append(childBlock) + return (childBlock) } + return null } - private fun createBlock(element: Element): ContentBlock = when(element.tagName()) { + private fun createBlock(element: Element): ContentBlock = when (element.tagName()) { "p" -> ContentParagraph() "b", "strong" -> ContentStrong() "i", "em" -> ContentEmphasis() @@ -99,45 +184,73 @@ class JavadocParser(private val refGraph: NodeReferenceGraph, } private fun createLink(element: Element): ContentBlock { - val docref = element.attr("docref") - if (docref != null) { - return ContentNodeLazyLink(docref, { -> refGraph.lookupOrWarn(docref, logger)}) - } - val href = element.attr("href") - if (href != null) { - return ContentExternalLink(href) - } else { - return ContentBlock() + 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 MutableContent.convertSeeTag(tag: PsiDocTag) { - val linkElement = tag.linkElement() - if (linkElement == null) { - return - } + val linkElement = tag.linkElement() ?: return val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null) - val linkSignature = resolveLink(linkElement) + + val valueElement = tag.referenceElement() + val externalLink = resolveExternalLink(valueElement) val text = ContentText(linkElement.text) - if (linkSignature != null) { - val linkNode = ContentNodeLazyLink(tag.valueElement!!.text, { -> refGraph.lookupOrWarn(linkSignature, logger)}) - linkNode.append(text) - seeSection.append(linkNode) - } else { - seeSection.append(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) = when (tag.name) { + private fun convertInlineDocTag(tag: PsiInlineDocTag, element: PsiNamedElement) = when (tag.name) { "link", "linkplain" -> { - val valueElement = tag.linkElement() - val linkSignature = resolveLink(valueElement) - if (linkSignature != null) { + 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 link = "<a docref=\"$linkSignature\">${labelText.htmlEscape()}</a>" + val linkTarget = if (externalLink != null) "href=\"$externalLink\"" else "docref=\"$linkSignature\"" + val link = "<a $linkTarget>${labelText.htmlEscape()}</a>" if (tag.name == "link") "<code>$link</code>" else link - } - else if (valueElement != null) { + } else if (valueElement != null) { valueElement.text } else { "" @@ -149,16 +262,45 @@ class JavadocParser(private val refGraph: NodeReferenceGraph, val escaped = text.toString().trimStart().htmlEscape() if (tag.name == "code") "<code>$escaped</code>" 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 } + 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 resolveLink(valueElement: PsiElement?): String? { + private fun resolveInternalLink(valueElement: PsiElement?): String? { val target = valueElement?.reference?.resolve() if (target != null) { - return getSignature(target) + return signatureProvider.signature(target) } return null } @@ -169,4 +311,88 @@ class JavadocParser(private val refGraph: NodeReferenceGraph, } 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}:${this.lineNumber()}") + 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}:${this.lineNumber()}") + 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<PsiMethod, PsiDocTag>? { + 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 + } + } |