aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/Java
diff options
context:
space:
mode:
authorDmitry Jemerov <yole@jetbrains.com>2015-12-03 16:22:11 +0100
committerDmitry Jemerov <yole@jetbrains.com>2015-12-03 16:22:49 +0100
commit39631054c58df5841ea268b7002b820ec55f6e0a (patch)
treecefedd8411c859243bd181568e16fcdd372a38c8 /core/src/main/kotlin/Java
parent797cb4732c53bf1e3b2091add8cf731fc436607f (diff)
downloaddokka-39631054c58df5841ea268b7002b820ec55f6e0a.tar.gz
dokka-39631054c58df5841ea268b7002b820ec55f6e0a.tar.bz2
dokka-39631054c58df5841ea268b7002b820ec55f6e0a.zip
restructure Dokka build to use Gradle for everything except for the Maven plugin
Diffstat (limited to 'core/src/main/kotlin/Java')
-rw-r--r--core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt266
-rw-r--r--core/src/main/kotlin/Java/JavadocParser.kt170
2 files changed, 436 insertions, 0 deletions
diff --git a/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt
new file mode 100644
index 00000000..3c9875cd
--- /dev/null
+++ b/core/src/main/kotlin/Java/JavaPsiDocumentationBuilder.kt
@@ -0,0 +1,266 @@
+package org.jetbrains.dokka
+
+import com.google.inject.Inject
+import com.intellij.psi.*
+import org.jetbrains.dokka.DocumentationNode.Kind
+
+fun getSignature(element: PsiElement?) = when(element) {
+ is PsiClass -> element.qualifiedName
+ is PsiField -> element.containingClass!!.qualifiedName + "#" + element.name
+ is PsiMethod ->
+ element.containingClass!!.qualifiedName + "#" + element.name + "(" +
+ element.parameterList.parameters.map { it.type.typeSignature() }.joinToString(",") + ")"
+ else -> null
+}
+
+private fun PsiType.typeSignature(): String = when(this) {
+ is PsiArrayType -> "Array<${componentType.typeSignature()}>"
+ else -> mapTypeName(this)
+}
+
+private fun mapTypeName(psiType: PsiType): String = when (psiType) {
+ is PsiPrimitiveType -> psiType.canonicalText
+ is PsiClassType -> psiType.resolve()?.qualifiedName ?: psiType.className
+ is PsiEllipsisType -> mapTypeName(psiType.componentType)
+ is PsiArrayType -> "Array"
+ else -> psiType.canonicalText
+}
+
+interface JavaDocumentationBuilder {
+ fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>)
+}
+
+class JavaPsiDocumentationBuilder : JavaDocumentationBuilder {
+ private val options: DocumentationOptions
+ private val refGraph: NodeReferenceGraph
+ private val docParser: JavaDocumentationParser
+
+ @Inject constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph) {
+ this.options = options
+ this.refGraph = refGraph
+ this.docParser = JavadocParser(refGraph)
+ }
+
+ constructor(options: DocumentationOptions, refGraph: NodeReferenceGraph, docParser: JavaDocumentationParser) {
+ this.options = options
+ this.refGraph = refGraph
+ this.docParser = docParser
+ }
+
+ override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) {
+ if (file.classes.all { skipElement(it) }) {
+ return
+ }
+ val packageNode = module.findOrCreatePackageNode(file.packageName, emptyMap())
+ appendClasses(packageNode, file.classes)
+ }
+
+ fun appendClasses(packageNode: DocumentationNode, classes: Array<PsiClass>) {
+ packageNode.appendChildren(classes) { build() }
+ }
+
+ fun register(element: PsiElement, node: DocumentationNode) {
+ val signature = getSignature(element)
+ if (signature != null) {
+ refGraph.register(signature, node)
+ }
+ }
+
+ fun link(node: DocumentationNode, element: PsiElement?) {
+ val qualifiedName = getSignature(element)
+ if (qualifiedName != null) {
+ refGraph.link(node, qualifiedName, DocumentationReference.Kind.Link)
+ }
+ }
+
+ fun link(element: PsiElement?, node: DocumentationNode, kind: DocumentationReference.Kind) {
+ val qualifiedName = getSignature(element)
+ if (qualifiedName != null) {
+ refGraph.link(qualifiedName, node, kind)
+ }
+ }
+
+ fun nodeForElement(element: PsiNamedElement,
+ kind: Kind,
+ name: String = element.name ?: "<anonymous>"): DocumentationNode {
+ val (docComment, deprecatedContent) = docParser.parseDocumentation(element)
+ val node = DocumentationNode(name, docComment, kind)
+ if (element is PsiModifierListOwner) {
+ node.appendModifiers(element)
+ val modifierList = element.modifierList
+ if (modifierList != null) {
+ modifierList.annotations.filter { !ignoreAnnotation(it) }.forEach {
+ val annotation = it.build()
+ node.append(annotation,
+ if (it.qualifiedName == "java.lang.Deprecated") DocumentationReference.Kind.Deprecation else DocumentationReference.Kind.Annotation)
+ }
+ }
+ }
+ if (deprecatedContent != null) {
+ val deprecationNode = DocumentationNode("", deprecatedContent, Kind.Modifier)
+ node.append(deprecationNode, DocumentationReference.Kind.Deprecation)
+ }
+ if (element is PsiDocCommentOwner && element.isDeprecated && node.deprecation == null) {
+ val deprecationNode = DocumentationNode("", Content.of(ContentText("Deprecated")), Kind.Modifier)
+ node.append(deprecationNode, DocumentationReference.Kind.Deprecation)
+ }
+ return node
+ }
+
+ fun ignoreAnnotation(annotation: PsiAnnotation) = when(annotation.qualifiedName) {
+ "java.lang.SuppressWarnings" -> true
+ else -> false
+ }
+
+ fun <T : Any> DocumentationNode.appendChildren(elements: Array<T>,
+ kind: DocumentationReference.Kind = DocumentationReference.Kind.Member,
+ buildFn: T.() -> DocumentationNode) {
+ elements.forEach {
+ if (!skipElement(it)) {
+ append(it.buildFn(), kind)
+ }
+ }
+ }
+
+ private fun skipElement(element: Any) = skipElementByVisibility(element) || hasSuppressTag(element)
+
+ private fun skipElementByVisibility(element: Any): Boolean =
+ !options.includeNonPublic && element is PsiModifierListOwner &&
+ (element.hasModifierProperty(PsiModifier.PRIVATE) || element.hasModifierProperty(PsiModifier.PACKAGE_LOCAL))
+
+ private fun hasSuppressTag(element: Any) =
+ element is PsiDocCommentOwner && element.docComment?.let { it.findTagByName("suppress") != null } ?: false
+
+ fun <T : Any> DocumentationNode.appendMembers(elements: Array<T>, buildFn: T.() -> DocumentationNode) =
+ appendChildren(elements, DocumentationReference.Kind.Member, buildFn)
+
+ fun <T : Any> DocumentationNode.appendDetails(elements: Array<T>, buildFn: T.() -> DocumentationNode) =
+ appendChildren(elements, DocumentationReference.Kind.Detail, buildFn)
+
+ fun PsiClass.build(): DocumentationNode {
+ val kind = when {
+ isInterface -> DocumentationNode.Kind.Interface
+ isEnum -> DocumentationNode.Kind.Enum
+ isAnnotationType -> DocumentationNode.Kind.AnnotationClass
+ else -> DocumentationNode.Kind.Class
+ }
+ val node = nodeForElement(this, kind)
+ superTypes.filter { !ignoreSupertype(it) }.forEach {
+ node.appendType(it, Kind.Supertype)
+ val superClass = it.resolve()
+ if (superClass != null) {
+ link(superClass, node, DocumentationReference.Kind.Inheritor)
+ }
+ }
+ node.appendDetails(typeParameters) { build() }
+ node.appendMembers(methods) { build() }
+ node.appendMembers(fields) { build() }
+ node.appendMembers(innerClasses) { build() }
+ register(this, node)
+ return node
+ }
+
+ fun ignoreSupertype(psiType: PsiClassType): Boolean =
+ psiType.isClass("java.lang.Enum") || psiType.isClass("java.lang.Object")
+
+ fun PsiClassType.isClass(qName: String): Boolean {
+ val shortName = qName.substringAfterLast('.')
+ if (className == shortName) {
+ val psiClass = resolve()
+ return psiClass?.qualifiedName == qName
+ }
+ return false
+ }
+
+ fun PsiField.build(): DocumentationNode {
+ val node = nodeForElement(this, nodeKind())
+ node.appendType(type)
+ node.appendModifiers(this)
+ register(this, node)
+ return node
+ }
+
+ private fun PsiField.nodeKind(): Kind = when {
+ this is PsiEnumConstant -> Kind.EnumItem
+ else -> Kind.Field
+ }
+
+ fun PsiMethod.build(): DocumentationNode {
+ val node = nodeForElement(this, nodeKind(),
+ if (isConstructor) "<init>" else name)
+
+ if (!isConstructor) {
+ node.appendType(returnType)
+ }
+ node.appendDetails(parameterList.parameters) { build() }
+ node.appendDetails(typeParameters) { build() }
+ register(this, node)
+ return node
+ }
+
+ private fun PsiMethod.nodeKind(): Kind = when {
+ isConstructor -> Kind.Constructor
+ else -> Kind.Function
+ }
+
+ fun PsiParameter.build(): DocumentationNode {
+ val node = nodeForElement(this, Kind.Parameter)
+ node.appendType(type)
+ if (type is PsiEllipsisType) {
+ node.appendTextNode("vararg", Kind.Modifier, DocumentationReference.Kind.Detail)
+ }
+ return node
+ }
+
+ fun PsiTypeParameter.build(): DocumentationNode {
+ val node = nodeForElement(this, Kind.TypeParameter)
+ extendsListTypes.forEach { node.appendType(it, Kind.UpperBound) }
+ implementsListTypes.forEach { node.appendType(it, Kind.UpperBound) }
+ return node
+ }
+
+ fun DocumentationNode.appendModifiers(element: PsiModifierListOwner) {
+ val modifierList = element.modifierList ?: return
+
+ PsiModifier.MODIFIERS.forEach {
+ if (modifierList.hasExplicitModifier(it)) {
+ appendTextNode(it, Kind.Modifier)
+ }
+ }
+ }
+
+ fun DocumentationNode.appendType(psiType: PsiType?, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type) {
+ if (psiType == null) {
+ return
+ }
+ append(psiType.build(kind), DocumentationReference.Kind.Detail)
+ }
+
+ fun PsiType.build(kind: DocumentationNode.Kind = DocumentationNode.Kind.Type): DocumentationNode {
+ val name = mapTypeName(this)
+ val node = DocumentationNode(name, Content.Empty, kind)
+ if (this is PsiClassType) {
+ node.appendDetails(parameters) { build(Kind.Type) }
+ link(node, resolve())
+ }
+ if (this is PsiArrayType && this !is PsiEllipsisType) {
+ node.append(componentType.build(Kind.Type), DocumentationReference.Kind.Detail)
+ }
+ return node
+ }
+
+ fun PsiAnnotation.build(): DocumentationNode {
+ val node = DocumentationNode(nameReferenceElement?.text ?: "<?>", Content.Empty, DocumentationNode.Kind.Annotation)
+ parameterList.attributes.forEach {
+ val parameter = DocumentationNode(it.name ?: "value", Content.Empty, DocumentationNode.Kind.Parameter)
+ val value = it.value
+ if (value != null) {
+ val valueText = (value as? PsiLiteralExpression)?.value as? String ?: value.text
+ val valueNode = DocumentationNode(valueText, Content.Empty, DocumentationNode.Kind.Value)
+ parameter.append(valueNode, DocumentationReference.Kind.Detail)
+ }
+ node.append(parameter, DocumentationReference.Kind.Detail)
+ }
+ return node
+ }
+}
diff --git a/core/src/main/kotlin/Java/JavadocParser.kt b/core/src/main/kotlin/Java/JavadocParser.kt
new file mode 100644
index 00000000..1378a5a7
--- /dev/null
+++ b/core/src/main/kotlin/Java/JavadocParser.kt
@@ -0,0 +1,170 @@
+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 org.jsoup.Jsoup
+import org.jsoup.nodes.Element
+import org.jsoup.nodes.Node
+import org.jsoup.nodes.TextNode
+
+data class JavadocParseResult(val content: Content, val deprecatedContent: Content?) {
+ companion object {
+ val Empty = JavadocParseResult(Content.Empty, null)
+ }
+}
+
+interface JavaDocumentationParser {
+ fun parseDocumentation(element: PsiNamedElement): JavadocParseResult
+}
+
+class JavadocParser(private val refGraph: NodeReferenceGraph) : JavaDocumentationParser {
+ 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() })
+ docComment.tags.forEach { tag ->
+ when(tag.name) {
+ "see" -> result.convertSeeTag(tag)
+ "deprecated" -> {
+ deprecatedContent = Content()
+ deprecatedContent!!.convertJavadocElements(tag.contentElements())
+ }
+ else -> {
+ val subjectName = tag.getSubjectName()
+ val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName)
+
+ section.convertJavadocElements(tag.contentElements())
+ }
+ }
+ }
+ return JavadocParseResult(result, deprecatedContent)
+ }
+
+ 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 }
+ return if (getSubjectName() != null) tagValueElements.dropWhile { it is PsiDocTagValue } else tagValueElements
+ }
+
+ private fun ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>) {
+ val htmlBuilder = StringBuilder()
+ elements.forEach {
+ if (it is PsiInlineDocTag) {
+ htmlBuilder.append(convertInlineDocTag(it))
+ } else {
+ htmlBuilder.append(it.text)
+ }
+ }
+ val doc = Jsoup.parse(htmlBuilder.toString().trimStart())
+ doc.body().childNodes().forEach {
+ convertHtmlNode(it)
+ }
+ }
+
+ private fun ContentBlock.convertHtmlNode(node: Node) {
+ if (node is TextNode) {
+ append(ContentText(node.text()))
+ } else if (node is Element) {
+ val childBlock = createBlock(node)
+ node.childNodes().forEach {
+ childBlock.convertHtmlNode(it)
+ }
+ append(childBlock)
+ }
+ }
+
+ private fun createBlock(element: Element): ContentBlock = when(element.tagName()) {
+ "p" -> ContentParagraph()
+ "b", "strong" -> ContentStrong()
+ "i", "em" -> ContentEmphasis()
+ "s", "del" -> ContentStrikethrough()
+ "code" -> ContentCode()
+ "pre" -> ContentBlockCode()
+ "ul" -> ContentUnorderedList()
+ "ol" -> ContentOrderedList()
+ "li" -> ContentListItem()
+ "a" -> createLink(element)
+ else -> ContentBlock()
+ }
+
+ private fun createLink(element: Element): ContentBlock {
+ val docref = element.attr("docref")
+ if (docref != null) {
+ return ContentNodeLazyLink(docref, { -> refGraph.lookup(docref)})
+ }
+ val href = element.attr("href")
+ if (href != null) {
+ return ContentExternalLink(href)
+ } else {
+ return ContentBlock()
+ }
+ }
+
+ private fun MutableContent.convertSeeTag(tag: PsiDocTag) {
+ val linkElement = tag.linkElement()
+ if (linkElement == null) {
+ return
+ }
+ val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null)
+ val linkSignature = resolveLink(linkElement)
+ val text = ContentText(linkElement.text)
+ if (linkSignature != null) {
+ val linkNode = ContentNodeLazyLink(tag.valueElement!!.text, { -> refGraph.lookup(linkSignature)})
+ linkNode.append(text)
+ seeSection.append(linkNode)
+ } else {
+ seeSection.append(text)
+ }
+ }
+
+ private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) {
+ "link", "linkplain" -> {
+ val valueElement = tag.linkElement()
+ val linkSignature = resolveLink(valueElement)
+ if (linkSignature != null) {
+ val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: valueElement!!.text
+ val link = "<a docref=\"$linkSignature\">${labelText.htmlEscape()}</a>"
+ if (tag.name == "link") "<code>$link</code>" 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().htmlEscape()
+ if (tag.name == "code") "<code>$escaped</code>" else escaped
+ }
+ else -> tag.text
+ }
+
+ private fun PsiDocTag.linkElement(): PsiElement? =
+ valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
+
+ private fun resolveLink(valueElement: PsiElement?): String? {
+ val target = valueElement?.reference?.resolve()
+ if (target != null) {
+ return getSignature(target)
+ }
+ return null
+ }
+
+ fun PsiDocTag.getSubjectName(): String? {
+ if (name == "param" || name == "throws" || name == "exception") {
+ return valueElement?.text
+ }
+ return null
+ }
+}