diff options
Diffstat (limited to 'core/src/main/kotlin/Kotlin')
6 files changed, 1500 insertions, 0 deletions
diff --git a/core/src/main/kotlin/Kotlin/ContentBuilder.kt b/core/src/main/kotlin/Kotlin/ContentBuilder.kt new file mode 100644 index 00000000..c4bb18de --- /dev/null +++ b/core/src/main/kotlin/Kotlin/ContentBuilder.kt @@ -0,0 +1,132 @@ +package org.jetbrains.dokka + +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.html.entities.EntityConverter +import java.util.* + +public fun buildContent(tree: MarkdownNode, linkResolver: (String) -> ContentBlock, inline: Boolean = false): MutableContent { + val result = MutableContent() + if (inline) { + buildInlineContentTo(tree, result, linkResolver) + } + else { + buildContentTo(tree, result, linkResolver) + } + return result +} + +public fun buildContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: (String) -> ContentBlock) { +// println(tree.toTestString()) + val nodeStack = ArrayDeque<ContentBlock>() + nodeStack.push(target) + + tree.visit {node, processChildren -> + val parent = nodeStack.peek() + + fun appendNodeWithChildren(content: ContentBlock) { + nodeStack.push(content) + processChildren() + parent.append(nodeStack.pop()) + } + + when (node.type) { + MarkdownElementTypes.ATX_1 -> appendNodeWithChildren(ContentHeading(1)) + MarkdownElementTypes.ATX_2 -> appendNodeWithChildren(ContentHeading(2)) + MarkdownElementTypes.ATX_3 -> appendNodeWithChildren(ContentHeading(3)) + MarkdownElementTypes.ATX_4 -> appendNodeWithChildren(ContentHeading(4)) + MarkdownElementTypes.ATX_5 -> appendNodeWithChildren(ContentHeading(5)) + MarkdownElementTypes.ATX_6 -> appendNodeWithChildren(ContentHeading(6)) + MarkdownElementTypes.UNORDERED_LIST -> appendNodeWithChildren(ContentUnorderedList()) + MarkdownElementTypes.ORDERED_LIST -> appendNodeWithChildren(ContentOrderedList()) + MarkdownElementTypes.LIST_ITEM -> appendNodeWithChildren(ContentListItem()) + MarkdownElementTypes.EMPH -> appendNodeWithChildren(ContentEmphasis()) + MarkdownElementTypes.STRONG -> appendNodeWithChildren(ContentStrong()) + MarkdownElementTypes.CODE_SPAN -> appendNodeWithChildren(ContentCode()) + MarkdownElementTypes.CODE_BLOCK, + MarkdownElementTypes.CODE_FENCE -> appendNodeWithChildren(ContentBlockCode()) + MarkdownElementTypes.PARAGRAPH -> appendNodeWithChildren(ContentParagraph()) + + MarkdownElementTypes.INLINE_LINK -> { + val label = node.child(MarkdownElementTypes.LINK_TEXT)?.child(MarkdownTokenTypes.TEXT) + val destination = node.child(MarkdownElementTypes.LINK_DESTINATION) + if (label != null) { + if (destination != null) { + val link = ContentExternalLink(destination.text) + link.append(ContentText(label.text)) + parent.append(link) + } else { + val link = ContentExternalLink(label.text) + link.append(ContentText(label.text)) + parent.append(link) + } + } + } + MarkdownElementTypes.SHORT_REFERENCE_LINK, + MarkdownElementTypes.FULL_REFERENCE_LINK -> { + val label = node.child(MarkdownElementTypes.LINK_LABEL)?.child(MarkdownTokenTypes.TEXT) + if (label != null) { + val link = linkResolver(label.text) + val linkText = node.child(MarkdownElementTypes.LINK_TEXT)?.child(MarkdownTokenTypes.TEXT) + link.append(ContentText(linkText?.text ?: label.text)) + parent.append(link) + } + } + MarkdownTokenTypes.WHITE_SPACE, + MarkdownTokenTypes.EOL -> { + if (keepWhitespace(nodeStack.peek()) && node.parent?.children?.last() != node) { + parent.append(ContentText(node.text)) + } + } + + MarkdownTokenTypes.CODE -> { + val block = ContentBlockCode() + block.append(ContentText(node.text)) + parent.append(block) + } + + MarkdownTokenTypes.TEXT -> { + fun createEntityOrText(text: String): ContentNode { + if (text == "&" || text == """ || text == "<" || text == ">") { + return ContentEntity(text) + } + if (text == "&") { + return ContentEntity("&") + } + val decodedText = EntityConverter.replaceEntities(text, true, true) + if (decodedText != text) { + return ContentEntity(text) + } + return ContentText(text) + } + + parent.append(createEntityOrText(node.text)) + } + + MarkdownTokenTypes.COLON, + MarkdownTokenTypes.DOUBLE_QUOTE, + MarkdownTokenTypes.LT, + MarkdownTokenTypes.GT, + MarkdownTokenTypes.LPAREN, + MarkdownTokenTypes.RPAREN, + MarkdownTokenTypes.LBRACKET, + MarkdownTokenTypes.RBRACKET, + MarkdownTokenTypes.CODE_FENCE_CONTENT -> { + parent.append(ContentText(node.text)) + } + else -> { + processChildren() + } + } + } +} + +private fun keepWhitespace(node: ContentNode) = node is ContentParagraph || node is ContentSection + +public fun buildInlineContentTo(tree: MarkdownNode, target: ContentBlock, linkResolver: (String) -> ContentBlock) { + val inlineContent = tree.children.singleOrNull { it.type == MarkdownElementTypes.PARAGRAPH }?.children ?: listOf(tree) + inlineContent.forEach { + buildContentTo(it, target, linkResolver) + } +} + diff --git a/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt b/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt new file mode 100644 index 00000000..2569bc71 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DeclarationLinkResolver.kt @@ -0,0 +1,43 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink + +class DeclarationLinkResolver + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph, + val logger: DokkaLogger) { + fun resolveContentLink(fromDescriptor: DeclarationDescriptor, href: String): ContentBlock { + val symbol = try { + val symbols = resolveKDocLink(resolutionFacade, fromDescriptor, null, href.split('.').toList()) + findTargetSymbol(symbols) + } catch(e: Exception) { + null + } + + // don't include unresolved links in generated doc + // assume that if an href doesn't contain '/', it's not an attempt to reference an external file + if (symbol != null) { + return ContentNodeLazyLink(href, { -> refGraph.lookup(symbol.signature()) }) + } + if ("/" in href) { + return ContentExternalLink(href) + } + logger.warn("Unresolved link to $href in doc comment of ${fromDescriptor.signatureWithSourceLocation()}") + return ContentExternalLink("#") + } + + fun findTargetSymbol(symbols: Collection<DeclarationDescriptor>): DeclarationDescriptor? { + if (symbols.isEmpty()) { + return null + } + val symbol = symbols.first() + if (symbol is CallableMemberDescriptor && symbol.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + return symbol.overriddenDescriptors.firstOrNull() + } + return symbol + } + +}
\ No newline at end of file diff --git a/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt new file mode 100644 index 00000000..b7705ec9 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt @@ -0,0 +1,199 @@ +package org.jetbrains.dokka.Kotlin + +import com.google.inject.Inject +import com.intellij.psi.PsiDocCommentOwner +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.idea.kdoc.KDocFinder +import org.jetbrains.kotlin.idea.kdoc.getResolutionScope +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.load.java.descriptors.JavaCallableMemberDescriptor +import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.ResolutionScope +import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered +import org.jetbrains.kotlin.resolve.source.PsiSourceElement + +class DescriptorDocumentationParser + @Inject constructor(val options: DocumentationOptions, + val logger: DokkaLogger, + val linkResolver: DeclarationLinkResolver, + val resolutionFacade: DokkaResolutionFacade, + val refGraph: NodeReferenceGraph) +{ + fun parseDocumentation(descriptor: DeclarationDescriptor, inline: Boolean = false): Content = + parseDocumentationAndDetails(descriptor, inline).first + + fun parseDocumentationAndDetails(descriptor: DeclarationDescriptor, inline: Boolean = false): Pair<Content, (DocumentationNode) -> Unit> { + if (descriptor is JavaClassDescriptor || descriptor is JavaCallableMemberDescriptor) { + return parseJavadoc(descriptor) + } + + val kdoc = KDocFinder.findKDoc(descriptor) ?: findStdlibKDoc(descriptor) + if (kdoc == null) { + if (options.reportUndocumented && !descriptor.isDeprecated() && + descriptor !is ValueParameterDescriptor && descriptor !is TypeParameterDescriptor && + descriptor !is PropertyAccessorDescriptor) { + logger.warn("No documentation for ${descriptor.signatureWithSourceLocation()}") + } + return Content.Empty to { node -> } + } + var kdocText = kdoc.getContent() + // workaround for code fence parsing problem in IJ markdown parser + if (kdocText.endsWith("```") || kdocText.endsWith("~~~")) { + kdocText += "\n" + } + val tree = parseMarkdown(kdocText) + val content = buildContent(tree, { href -> linkResolver.resolveContentLink(descriptor, href) }, inline) + if (kdoc is KDocSection) { + val tags = kdoc.getTags() + tags.forEach { + when (it.name) { + "sample" -> + content.append(functionBody(descriptor, it.getSubjectName())) + "see" -> + content.addTagToSeeAlso(descriptor, it) + else -> { + val section = content.addSection(javadocSectionDisplayName(it.name), it.getSubjectName()) + val sectionContent = it.getContent() + val markdownNode = parseMarkdown(sectionContent) + buildInlineContentTo(markdownNode, section, { href -> linkResolver.resolveContentLink(descriptor, href) }) + } + } + } + } + return content to { node -> } + } + + /** + * Special case for generating stdlib documentation (the Any class to which the override chain will resolve + * is not the same one as the Any class included in the source scope). + */ + fun findStdlibKDoc(descriptor: DeclarationDescriptor): KDocTag? { + if (descriptor !is CallableMemberDescriptor) { + return null + } + val name = descriptor.name.asString() + if (name == "equals" || name == "hashCode" || name == "toString") { + var deepestDescriptor: CallableMemberDescriptor = descriptor + while (!deepestDescriptor.overriddenDescriptors.isEmpty()) { + deepestDescriptor = deepestDescriptor.overriddenDescriptors.first() + } + if (DescriptorUtils.getFqName(deepestDescriptor.containingDeclaration).asString() == "kotlin.Any") { + val anyClassDescriptors = resolutionFacade.resolveSession.getTopLevelClassDescriptors( + FqName.fromSegments(listOf("kotlin", "Any")), NoLookupLocation.FROM_IDE) + anyClassDescriptors.forEach { + val anyMethod = it.getMemberScope(listOf()) + .getDescriptorsFiltered(DescriptorKindFilter.FUNCTIONS, { it == descriptor.name }) + .single() + val kdoc = KDocFinder.findKDoc(anyMethod) + if (kdoc != null) { + return kdoc + } + } + } + } + return null + } + + fun parseJavadoc(descriptor: DeclarationDescriptor): Pair<Content, (DocumentationNode) -> Unit> { + val psi = ((descriptor as? DeclarationDescriptorWithSource)?.source as? PsiSourceElement)?.psi + if (psi is PsiDocCommentOwner) { + val parseResult = JavadocParser(refGraph).parseDocumentation(psi as PsiNamedElement) + return parseResult.content to { node -> + parseResult.deprecatedContent?.let { + val deprecationNode = DocumentationNode("", it, DocumentationNode.Kind.Modifier) + node.append(deprecationNode, DocumentationReference.Kind.Deprecation) + } + } + } + return Content.Empty to { node -> } + } + + fun KDocSection.getTags(): Array<KDocTag> = PsiTreeUtil.getChildrenOfType(this, KDocTag::class.java) ?: arrayOf() + + private fun MutableContent.addTagToSeeAlso(descriptor: DeclarationDescriptor, seeTag: KDocTag) { + val subjectName = seeTag.getSubjectName() + if (subjectName != null) { + val seeSection = findSectionByTag("See Also") ?: addSection("See Also", null) + val link = linkResolver.resolveContentLink(descriptor, subjectName) + link.append(ContentText(subjectName)) + val para = ContentParagraph() + para.append(link) + seeSection.append(para) + } + } + + private fun functionBody(descriptor: DeclarationDescriptor, functionName: String?): ContentNode { + if (functionName == null) { + logger.warn("Missing function name in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Missing function name in @sample")); it } + } + val scope = getResolutionScope(resolutionFacade, descriptor) + val rootPackage = resolutionFacade.moduleDescriptor.getPackage(FqName.ROOT) + val rootScope = rootPackage.memberScope + val symbol = resolveInScope(functionName, scope) ?: resolveInScope(functionName, rootScope) + if (symbol == null) { + logger.warn("Unresolved function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Unresolved: $functionName")); it } + } + val psiElement = DescriptorToSourceUtils.descriptorToDeclaration(symbol) + if (psiElement == null) { + logger.warn("Can't find source for function $functionName in @sample in ${descriptor.signature()}") + return ContentBlockCode().let() { it.append(ContentText("Source not found: $functionName")); it } + } + + val text = when (psiElement) { + is KtDeclarationWithBody -> ContentBlockCode().let() { + val bodyExpression = psiElement.bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + val lines = text.trimEnd().split("\n".toRegex()).toTypedArray().filterNot { it.length == 0 } + val indent = lines.map { it.takeWhile { it.isWhitespace() }.count() }.min() ?: 0 + val finalText = lines.map { it.drop(indent) }.joinToString("\n") + return ContentBlockCode("kotlin").let() { it.append(ContentText(finalText)); it } + } + + private fun resolveInScope(functionName: String, scope: ResolutionScope): DeclarationDescriptor? { + var currentScope = scope + val parts = functionName.split('.') + + var symbol: DeclarationDescriptor? = null + + for (part in parts) { + // short name + val symbolName = Name.guess(part) + val partSymbol = currentScope.getContributedDescriptors(DescriptorKindFilter.ALL, { it == symbolName }) + .filter { it.name == symbolName } + .firstOrNull() + + if (partSymbol == null) { + symbol = null + break + } + currentScope = if (partSymbol is ClassDescriptor) + partSymbol.defaultType.memberScope + else + getResolutionScope(resolutionFacade, partSymbol) + symbol = partSymbol + } + + return symbol + } +} diff --git a/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt new file mode 100644 index 00000000..6551ded6 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/DocumentationBuilder.kt @@ -0,0 +1,653 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiJavaFile +import org.jetbrains.dokka.DocumentationNode.Kind +import org.jetbrains.dokka.Kotlin.DescriptorDocumentationParser +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.idea.caches.resolve.getModuleInfo +import org.jetbrains.kotlin.idea.kdoc.KDocFinder +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.constants.TypedCompileTimeConstant +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.isDocumentedAnnotation +import org.jetbrains.kotlin.resolve.jvm.JavaDescriptorResolver +import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.types.ErrorUtils +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjection + +public data class DocumentationOptions(val outputDir: String, + val outputFormat: String, + val includeNonPublic: Boolean = false, + val reportUndocumented: Boolean = true, + val skipEmptyPackages: Boolean = true, + val skipDeprecated: Boolean = false, + val sourceLinks: List<SourceLinkDefinition>) + +private fun isSamePackage(descriptor1: DeclarationDescriptor, descriptor2: DeclarationDescriptor): Boolean { + val package1 = DescriptorUtils.getParentOfType(descriptor1, PackageFragmentDescriptor::class.java) + val package2 = DescriptorUtils.getParentOfType(descriptor2, PackageFragmentDescriptor::class.java) + return package1 != null && package2 != null && package1.fqName == package2.fqName +} + +interface PackageDocumentationBuilder { + fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) +} + +class DocumentationBuilder + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val descriptorDocumentationParser: DescriptorDocumentationParser, + val options: DocumentationOptions, + val refGraph: NodeReferenceGraph, + val logger: DokkaLogger) +{ + val visibleToDocumentation = setOf(Visibilities.PROTECTED, Visibilities.PUBLIC) + val boringBuiltinClasses = setOf( + "kotlin.Unit", "kotlin.Byte", "kotlin.Short", "kotlin.Int", "kotlin.Long", "kotlin.Char", "kotlin.Boolean", + "kotlin.Float", "kotlin.Double", "kotlin.String", "kotlin.Array", "kotlin.Any") + val knownModifiers = setOf( + KtTokens.PUBLIC_KEYWORD, KtTokens.PROTECTED_KEYWORD, KtTokens.INTERNAL_KEYWORD, KtTokens.PRIVATE_KEYWORD, + KtTokens.OPEN_KEYWORD, KtTokens.FINAL_KEYWORD, KtTokens.ABSTRACT_KEYWORD, KtTokens.SEALED_KEYWORD, + KtTokens.OVERRIDE_KEYWORD) + + fun link(node: DocumentationNode, descriptor: DeclarationDescriptor, kind: DocumentationReference.Kind) { + refGraph.link(node, descriptor.signature(), kind) + } + + fun link(fromDescriptor: DeclarationDescriptor?, toDescriptor: DeclarationDescriptor?, kind: DocumentationReference.Kind) { + if (fromDescriptor != null && toDescriptor != null) { + refGraph.link(fromDescriptor.signature(), toDescriptor.signature(), kind) + } + } + + fun register(descriptor: DeclarationDescriptor, node: DocumentationNode) { + refGraph.register(descriptor.signature(), node) + } + + fun <T> nodeForDescriptor(descriptor: T, kind: Kind): DocumentationNode where T : DeclarationDescriptor, T : Named { + val (doc, callback) = descriptorDocumentationParser.parseDocumentationAndDetails(descriptor, kind == Kind.Parameter) + val node = DocumentationNode(descriptor.name.asString(), doc, kind).withModifiers(descriptor) + callback(node) + return node + } + + private fun DocumentationNode.withModifiers(descriptor: DeclarationDescriptor) : DocumentationNode{ + if (descriptor is MemberDescriptor) { + appendVisibility(descriptor) + if (descriptor !is ConstructorDescriptor) { + appendModality(descriptor) + } + } + return this + } + + fun DocumentationNode.appendModality(descriptor: MemberDescriptor) { + var modality = descriptor.modality + if (modality == Modality.OPEN) { + val containingClass = descriptor.containingDeclaration as? ClassDescriptor + if (containingClass?.modality == Modality.FINAL) { + modality = Modality.FINAL + } + } + val modifier = modality.name.toLowerCase() + appendTextNode(modifier, DocumentationNode.Kind.Modifier) + } + + fun DocumentationNode.appendVisibility(descriptor: DeclarationDescriptorWithVisibility) { + val modifier = descriptor.visibility.normalize().displayName + appendTextNode(modifier, DocumentationNode.Kind.Modifier) + } + + fun DocumentationNode.appendSupertypes(descriptor: ClassDescriptor) { + val superTypes = descriptor.typeConstructor.supertypes + for (superType in superTypes) { + if (!ignoreSupertype(superType)) { + appendType(superType, DocumentationNode.Kind.Supertype) + val superclass = superType?.constructor?.declarationDescriptor + link(superclass, descriptor, DocumentationReference.Kind.Inheritor) + link(descriptor, superclass, DocumentationReference.Kind.Superclass) + } + } + } + + private fun ignoreSupertype(superType: KotlinType): Boolean { + val superClass = superType.constructor.declarationDescriptor as? ClassDescriptor + if (superClass != null) { + val fqName = DescriptorUtils.getFqNameSafe(superClass).asString() + return fqName == "kotlin.Annotation" || fqName == "kotlin.Enum" || fqName == "kotlin.Any" + } + return false + } + + fun DocumentationNode.appendProjection(projection: TypeProjection, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type) { + if (projection.isStarProjection) { + appendTextNode("*", Kind.Type) + } + else { + appendType(projection.type, kind, projection.projectionKind.label) + } + } + + fun DocumentationNode.appendType(kotlinType: KotlinType?, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type, prefix: String = "") { + if (kotlinType == null) + return + val classifierDescriptor = kotlinType.constructor.declarationDescriptor + val name = when (classifierDescriptor) { + is ClassDescriptor -> { + if (classifierDescriptor.isCompanionObject) { + classifierDescriptor.containingDeclaration.name.asString() + + "." + classifierDescriptor.name.asString() + } + else { + classifierDescriptor.name.asString() + } + } + is Named -> classifierDescriptor.name.asString() + else -> "<anonymous>" + } + val node = DocumentationNode(name, Content.Empty, kind) + if (prefix != "") { + node.appendTextNode(prefix, Kind.Modifier) + } + if (kotlinType.isMarkedNullable) { + node.appendTextNode("?", Kind.NullabilityModifier) + } + if (classifierDescriptor != null) { + link(node, classifierDescriptor, + if (classifierDescriptor.isBoringBuiltinClass()) DocumentationReference.Kind.HiddenLink else DocumentationReference.Kind.Link) + } + + append(node, DocumentationReference.Kind.Detail) + node.appendAnnotations(kotlinType) + for (typeArgument in kotlinType.arguments) { + node.appendProjection(typeArgument) + } + } + + fun ClassifierDescriptor.isBoringBuiltinClass(): Boolean = + DescriptorUtils.getFqName(this).asString() in boringBuiltinClasses + + fun DocumentationNode.appendAnnotations(annotated: Annotated) { + annotated.annotations.filter { it.isDocumented() }.forEach { + val annotationNode = it.build() + if (annotationNode != null) { + append(annotationNode, + if (annotationNode.isDeprecation()) DocumentationReference.Kind.Deprecation else DocumentationReference.Kind.Annotation) + } + } + } + + fun DocumentationNode.appendModifiers(descriptor: DeclarationDescriptor) { + val psi = (descriptor as DeclarationDescriptorWithSource).source.getPsi() as? KtModifierListOwner ?: return + KtTokens.MODIFIER_KEYWORDS_ARRAY.filter { it !in knownModifiers }.forEach { + if (psi.hasModifier(it)) { + appendTextNode(it.value, Kind.Modifier) + } + } + } + + fun DocumentationNode.isDeprecation() = name == "Deprecated" || name == "deprecated" + + fun DocumentationNode.appendSourceLink(sourceElement: SourceElement) { + appendSourceLink(sourceElement.getPsi(), options.sourceLinks) + } + + fun DocumentationNode.appendChild(descriptor: DeclarationDescriptor, kind: DocumentationReference.Kind): DocumentationNode? { + // do not include generated code + if (descriptor is CallableMemberDescriptor && descriptor.kind != CallableMemberDescriptor.Kind.DECLARATION) + return null + + if (descriptor.isDocumented()) { + val node = descriptor.build() + append(node, kind) + return node + } + return null + } + + fun DeclarationDescriptor.isDocumented(): Boolean { + return (options.includeNonPublic + || this !is MemberDescriptor + || this.visibility in visibleToDocumentation) && + !isDocumentationSuppressed() && + (!options.skipDeprecated || !isDeprecated()) + } + + fun DocumentationNode.appendMembers(descriptors: Iterable<DeclarationDescriptor>): List<DocumentationNode> { + val nodes = descriptors.map { descriptor -> + if (descriptor is CallableMemberDescriptor && descriptor.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + val baseDescriptor = descriptor.overriddenDescriptors.firstOrNull() + if (baseDescriptor != null) { + link(this, baseDescriptor, DocumentationReference.Kind.InheritedMember) + } + null + } + else { + val descriptorToUse = if (descriptor is ConstructorDescriptor) descriptor else descriptor.original + appendChild(descriptorToUse, DocumentationReference.Kind.Member) + } + } + return nodes.filterNotNull() + } + + fun DocumentationNode.appendInPageChildren(descriptors: Iterable<DeclarationDescriptor>, kind: DocumentationReference.Kind) { + descriptors.forEach { descriptor -> + val node = appendChild(descriptor, kind) + node?.addReferenceTo(this, DocumentationReference.Kind.TopLevelPage) + } + } + + fun DocumentationModule.appendFragments(fragments: Collection<PackageFragmentDescriptor>, + packageContent: Map<String, Content>, + packageDocumentationBuilder: PackageDocumentationBuilder) { + val allFqNames = fragments.map { it.fqName }.distinct() + + for (packageName in allFqNames) { + val declarations = fragments.filter { it.fqName == packageName }.flatMap { it.getMemberScope().getContributedDescriptors() } + + if (options.skipEmptyPackages && declarations.none { it.isDocumented() }) continue + logger.info(" package $packageName: ${declarations.count()} declarations") + val packageNode = findOrCreatePackageNode(packageName.asString(), packageContent) + packageDocumentationBuilder.buildPackageDocumentation(this@DocumentationBuilder, packageName, packageNode, declarations) + } + } + + fun DeclarationDescriptor.build(): DocumentationNode = when (this) { + is ClassDescriptor -> build() + is ConstructorDescriptor -> build() + is PropertyDescriptor -> build() + is FunctionDescriptor -> build() + is TypeParameterDescriptor -> build() + is ValueParameterDescriptor -> build() + is ReceiverParameterDescriptor -> build() + else -> throw IllegalStateException("Descriptor $this is not known") + } + + fun ClassDescriptor.build(): DocumentationNode { + val kind = when (kind) { + ClassKind.OBJECT -> Kind.Object + ClassKind.INTERFACE -> Kind.Interface + ClassKind.ENUM_CLASS -> Kind.Enum + ClassKind.ANNOTATION_CLASS -> Kind.AnnotationClass + ClassKind.ENUM_ENTRY -> Kind.EnumItem + else -> Kind.Class + } + val node = nodeForDescriptor(this, kind) + node.appendSupertypes(this) + if (getKind() != ClassKind.OBJECT && getKind() != ClassKind.ENUM_ENTRY) { + node.appendInPageChildren(typeConstructor.parameters, DocumentationReference.Kind.Detail) + val constructorsToDocument = if (getKind() == ClassKind.ENUM_CLASS) + constructors.filter { it.valueParameters.size > 0 } + else + constructors + node.appendMembers(constructorsToDocument) + } + val members = defaultType.memberScope.getContributedDescriptors().filter { it != companionObjectDescriptor } + node.appendMembers(members) + node.appendMembers(staticScope.getContributedDescriptors()).forEach { + it.appendTextNode("static", Kind.Modifier) + } + val companionObjectDescriptor = companionObjectDescriptor + if (companionObjectDescriptor != null) { + node.appendMembers(companionObjectDescriptor.defaultType.memberScope.getContributedDescriptors()) + } + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + register(this, node) + return node + } + + fun ConstructorDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, Kind.Constructor) + node.appendInPageChildren(valueParameters, DocumentationReference.Kind.Detail) + register(this, node) + return node + } + + private fun CallableMemberDescriptor.inCompanionObject(): Boolean { + val containingDeclaration = containingDeclaration + if ((containingDeclaration as? ClassDescriptor)?.isCompanionObject ?: false) { + return true + } + val receiver = extensionReceiverParameter + return (receiver?.type?.constructor?.declarationDescriptor as? ClassDescriptor)?.isCompanionObject ?: false + } + + fun FunctionDescriptor.build(): DocumentationNode { + if (ErrorUtils.containsErrorType(this)) { + logger.warn("Found an unresolved type in ${signatureWithSourceLocation()}") + } + + val node = nodeForDescriptor(this, if (inCompanionObject()) Kind.CompanionObjectFunction else Kind.Function) + + node.appendInPageChildren(typeParameters, DocumentationReference.Kind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, DocumentationReference.Kind.Detail) } + node.appendInPageChildren(valueParameters, DocumentationReference.Kind.Detail) + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + + overriddenDescriptors.forEach { + addOverrideLink(it, this) + } + + register(this, node) + return node + } + + fun addOverrideLink(baseClassFunction: CallableMemberDescriptor, overridingFunction: CallableMemberDescriptor) { + val source = baseClassFunction.original.source.getPsi() + if (source != null) { + link(overridingFunction, baseClassFunction, DocumentationReference.Kind.Override) + } else { + baseClassFunction.overriddenDescriptors.forEach { + addOverrideLink(it, overridingFunction) + } + } + } + + fun PropertyDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, if (inCompanionObject()) Kind.CompanionObjectProperty else Kind.Property) + node.appendInPageChildren(typeParameters, DocumentationReference.Kind.Detail) + extensionReceiverParameter?.let { node.appendChild(it, DocumentationReference.Kind.Detail) } + node.appendType(returnType) + node.appendAnnotations(this) + node.appendModifiers(this) + node.appendSourceLink(source) + if (isVar) { + node.appendTextNode("var", DocumentationNode.Kind.Modifier) + } + getter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Getter") + } + } + setter?.let { + if (!it.isDefault) { + node.addAccessorDocumentation(descriptorDocumentationParser.parseDocumentation(it), "Setter") + } + } + + overriddenDescriptors.forEach { + addOverrideLink(it, this) + } + + register(this, node) + return node + } + + fun DocumentationNode.addAccessorDocumentation(documentation: Content, prefix: String) { + if (documentation == Content.Empty) return + updateContent { + if (!documentation.children.isEmpty()) { + val section = addSection(prefix, null) + documentation.children.forEach { section.append(it) } + } + documentation.sections.forEach { + val section = addSection("$prefix ${it.tag}", it.subjectName) + it.children.forEach { section.append(it) } + } + } + } + + fun ValueParameterDescriptor.build(): DocumentationNode { + val node = nodeForDescriptor(this, Kind.Parameter) + node.appendType(varargElementType ?: type) + if (declaresDefaultValue()) { + val psi = source.getPsi() as? KtParameter + if (psi != null) { + val defaultValueText = psi.defaultValue?.text + if (defaultValueText != null) { + node.appendTextNode(defaultValueText, Kind.Value) + } + } + } + node.appendAnnotations(this) + node.appendModifiers(this) + if (varargElementType != null && node.details(Kind.Modifier).none { it.name == "vararg" }) { + node.appendTextNode("vararg", Kind.Modifier) + } + register(this, node) + return node + } + + fun TypeParameterDescriptor.build(): DocumentationNode { + val doc = descriptorDocumentationParser.parseDocumentation(this) + val name = name.asString() + val prefix = variance.label + + val node = DocumentationNode(name, doc, DocumentationNode.Kind.TypeParameter) + if (prefix != "") { + node.appendTextNode(prefix, Kind.Modifier) + } + if (isReified) { + node.appendTextNode("reified", Kind.Modifier) + } + + for (constraint in upperBounds) { + if (KotlinBuiltIns.isDefaultBound(constraint)) { + continue + } + node.appendType(constraint, Kind.UpperBound) + } + + for (constraint in lowerBounds) { + if (KotlinBuiltIns.isNothing(constraint)) + continue + node.appendType(constraint, Kind.LowerBound) + } + return node + } + + fun ReceiverParameterDescriptor.build(): DocumentationNode { + var receiverClass: DeclarationDescriptor = type.constructor.declarationDescriptor!! + if ((receiverClass as? ClassDescriptor)?.isCompanionObject ?: false) { + receiverClass = receiverClass.containingDeclaration!! + } + link(receiverClass, + containingDeclaration, + DocumentationReference.Kind.Extension) + + val node = DocumentationNode(name.asString(), Content.Empty, Kind.Receiver) + node.appendType(type) + return node + } + + fun AnnotationDescriptor.build(): DocumentationNode? { + val annotationClass = type.constructor.declarationDescriptor + if (annotationClass == null || ErrorUtils.isError(annotationClass)) { + return null + } + val node = DocumentationNode(annotationClass.name.asString(), Content.Empty, DocumentationNode.Kind.Annotation) + val arguments = allValueArguments.toList().sortedBy { it.first.index } + arguments.forEach { + val valueNode = it.second.toDocumentationNode() + if (valueNode != null) { + val paramNode = DocumentationNode(it.first.name.asString(), Content.Empty, DocumentationNode.Kind.Parameter) + paramNode.append(valueNode, DocumentationReference.Kind.Detail) + node.append(paramNode, DocumentationReference.Kind.Detail) + } + } + return node + } + + fun CompileTimeConstant<Any?>.build(): DocumentationNode? = when (this) { + is TypedCompileTimeConstant -> constantValue.toDocumentationNode() + else -> null + } + + fun ConstantValue<*>.toDocumentationNode(): DocumentationNode? = value?.let { value -> + when (value) { + is String -> + "\"" + StringUtil.escapeStringCharacters(value) + "\"" + is EnumEntrySyntheticClassDescriptor -> + value.containingDeclaration.name.asString() + "." + value.name.asString() + else -> value.toString() + }.let { valueString -> + DocumentationNode(valueString, Content.Empty, DocumentationNode.Kind.Value) + } + } +} + +class KotlinPackageDocumentationBuilder : PackageDocumentationBuilder { + override fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) { + val externalClassNodes = hashMapOf<FqName, DocumentationNode>() + declarations.forEach { descriptor -> + with(documentationBuilder) { + if (descriptor.isDocumented()) { + val parent = packageNode.getParentForPackageMember(descriptor, externalClassNodes) + parent.appendChild(descriptor, DocumentationReference.Kind.Member) + } + } + } + } +} + +class KotlinJavaDocumentationBuilder + @Inject constructor(val documentationBuilder: DocumentationBuilder, + val logger: DokkaLogger) : JavaDocumentationBuilder +{ + override fun appendFile(file: PsiJavaFile, module: DocumentationModule, packageContent: Map<String, Content>) { + val packageNode = module.findOrCreatePackageNode(file.packageName, packageContent) + + file.classes.forEach { + val javaDescriptorResolver = KotlinCacheService.getInstance(file.project).getProjectService(JvmPlatform, + it.getModuleInfo(), JavaDescriptorResolver::class.java) + + val descriptor = javaDescriptorResolver.resolveClass(JavaClassImpl(it)) + if (descriptor == null) { + logger.warn("Cannot find descriptor for Java class ${it.qualifiedName}") + } + else { + with(documentationBuilder) { + packageNode.appendChild(descriptor, DocumentationReference.Kind.Member) + } + } + } + } +} + +private fun AnnotationDescriptor.isDocumented(): Boolean { + if (source.getPsi() != null && mustBeDocumented()) return true + val annotationClassName = type.constructor.declarationDescriptor?.fqNameSafe?.asString() + return annotationClassName == "kotlin.Extension" +} + +fun AnnotationDescriptor.mustBeDocumented(): Boolean { + val annotationClass = type.constructor.declarationDescriptor as? Annotated ?: return false + return annotationClass.isDocumentedAnnotation() +} + +fun DeclarationDescriptor.isDocumentationSuppressed(): Boolean { + val doc = KDocFinder.findKDoc(this) + return doc is KDocSection && doc.findTagByName("suppress") != null +} + +fun DeclarationDescriptor.isDeprecated(): Boolean = annotations.any { + DescriptorUtils.getFqName(it.type.constructor.declarationDescriptor!!).asString() == "kotlin.Deprecated" +} || (this is ConstructorDescriptor && containingDeclaration.isDeprecated()) + +fun DocumentationNode.getParentForPackageMember(descriptor: DeclarationDescriptor, + externalClassNodes: MutableMap<FqName, DocumentationNode>): DocumentationNode { + if (descriptor is CallableMemberDescriptor) { + val extensionClassDescriptor = descriptor.getExtensionClassDescriptor() + if (extensionClassDescriptor != null && !isSamePackage(descriptor, extensionClassDescriptor) && + !ErrorUtils.isError(extensionClassDescriptor)) { + val fqName = DescriptorUtils.getFqNameSafe(extensionClassDescriptor) + return externalClassNodes.getOrPut(fqName, { + val newNode = DocumentationNode(fqName.asString(), Content.Empty, Kind.ExternalClass) + append(newNode, DocumentationReference.Kind.Member) + newNode + }) + } + } + return this +} + +fun CallableMemberDescriptor.getExtensionClassDescriptor(): ClassifierDescriptor? { + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + val type = extensionReceiver.type + return type.constructor.declarationDescriptor as? ClassDescriptor + } + return null +} + +fun DeclarationDescriptor.signature(): String = when(this) { + is ClassDescriptor, is PackageFragmentDescriptor -> DescriptorUtils.getFqName(this).asString() + is PropertyDescriptor -> containingDeclaration.signature() + "#" + name + receiverSignature() + is FunctionDescriptor -> containingDeclaration.signature() + "#" + name + parameterSignature() + is ValueParameterDescriptor -> containingDeclaration.signature() + ":" + name + is TypeParameterDescriptor -> containingDeclaration.signature() + "<" + name + + else -> throw UnsupportedOperationException("Don't know how to calculate signature for $this") +} + +fun PropertyDescriptor.receiverSignature(): String { + val receiver = extensionReceiverParameter + if (receiver != null) { + return "#" + receiver.type.signature() + } + return "" +} + +fun CallableMemberDescriptor.parameterSignature(): String { + val params = valueParameters.map { it.type }.toArrayList() + val extensionReceiver = extensionReceiverParameter + if (extensionReceiver != null) { + params.add(0, extensionReceiver.type) + } + return "(" + params.map { it.signature() }.joinToString() + ")" +} + +fun KotlinType.signature(): String { + val declarationDescriptor = constructor.declarationDescriptor ?: return "<null>" + val typeName = DescriptorUtils.getFqName(declarationDescriptor).asString() + if (typeName == "Array" && arguments.size == 1) { + return "Array<" + arguments.first().type.signature() + ">" + } + return typeName +} + +fun DeclarationDescriptor.signatureWithSourceLocation(): String { + val signature = signature() + val sourceLocation = sourceLocation() + return if (sourceLocation != null) "$signature ($sourceLocation)" else signature +} + +fun DeclarationDescriptor.sourceLocation(): String? { + if (this is DeclarationDescriptorWithSource) { + val psi = (this.source as? PsiSourceElement)?.getPsi() + if (psi != null) { + val fileName = psi.containingFile.name + val lineNumber = psi.lineNumber() + return if (lineNumber != null) "$fileName:$lineNumber" else fileName + } + } + return null +} diff --git a/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt new file mode 100644 index 00000000..7a1d591c --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinAsJavaDocumentationBuilder.kt @@ -0,0 +1,64 @@ +package org.jetbrains.dokka + +import com.google.inject.Inject +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.Kotlin.DescriptorDocumentationParser +import org.jetbrains.kotlin.asJava.KtLightElement +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtPropertyAccessor + +class KotlinAsJavaDocumentationBuilder + @Inject constructor(val kotlinAsJavaDocumentationParser: KotlinAsJavaDocumentationParser) : PackageDocumentationBuilder +{ + override fun buildPackageDocumentation(documentationBuilder: DocumentationBuilder, + packageName: FqName, + packageNode: DocumentationNode, + declarations: List<DeclarationDescriptor>) { + val project = documentationBuilder.resolutionFacade.project + val psiPackage = JavaPsiFacade.getInstance(project).findPackage(packageName.asString()) + if (psiPackage == null) { + documentationBuilder.logger.error("Cannot find Java package by qualified name: ${packageName.asString()}") + return + } + + val javaDocumentationBuilder = JavaPsiDocumentationBuilder(documentationBuilder.options, + documentationBuilder.refGraph, + kotlinAsJavaDocumentationParser) + + psiPackage.classes.filter { it is KtLightElement<*, *> }.filter { it.isVisibleInDocumentation() }.forEach { + javaDocumentationBuilder.appendClasses(packageNode, arrayOf(it)) + } + } + + fun PsiClass.isVisibleInDocumentation() : Boolean { + val origin: KtDeclaration? = (this as KtLightElement<*, *>).getOrigin() + return origin?.hasModifier(KtTokens.INTERNAL_KEYWORD) != true && + origin?.hasModifier(KtTokens.PRIVATE_KEYWORD) != true + } +} + +class KotlinAsJavaDocumentationParser + @Inject constructor(val resolutionFacade: DokkaResolutionFacade, + val descriptorDocumentationParser: DescriptorDocumentationParser) : JavaDocumentationParser +{ + override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult { + val kotlinLightElement = element as? KtLightElement<*, *> ?: return JavadocParseResult.Empty + val origin = kotlinLightElement.getOrigin() ?: return JavadocParseResult.Empty + if (origin is KtParameter) { + // LazyDeclarationResolver does not support setter parameters + val grandFather = origin.parent?.parent + if (grandFather is KtPropertyAccessor) { + return JavadocParseResult.Empty + } + } + val descriptor = resolutionFacade.resolveToDescriptor(origin) + val content = descriptorDocumentationParser.parseDocumentation(descriptor, origin is KtParameter) + return JavadocParseResult(content, null) + } +} diff --git a/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt new file mode 100644 index 00000000..0d39f410 --- /dev/null +++ b/core/src/main/kotlin/Kotlin/KotlinLanguageService.kt @@ -0,0 +1,409 @@ +package org.jetbrains.dokka + +import org.jetbrains.dokka.LanguageService.RenderMode + +/** + * Implements [LanguageService] and provides rendering of symbols in Kotlin language + */ +class KotlinLanguageService : LanguageService { + private val fullOnlyModifiers = setOf("public", "protected", "private", "inline", "noinline", "crossinline", "reified") + + override fun render(node: DocumentationNode, renderMode: RenderMode): ContentNode { + return content { + when (node.kind) { + DocumentationNode.Kind.Package -> if (renderMode == RenderMode.FULL) renderPackage(node) + in DocumentationNode.Kind.classLike -> renderClass(node, renderMode) + + DocumentationNode.Kind.EnumItem, + DocumentationNode.Kind.ExternalClass -> if (renderMode == RenderMode.FULL) identifier(node.name) + + DocumentationNode.Kind.TypeParameter -> renderTypeParameter(node, renderMode) + DocumentationNode.Kind.Type, + DocumentationNode.Kind.UpperBound -> renderType(node, renderMode) + + DocumentationNode.Kind.Modifier -> renderModifier(node) + DocumentationNode.Kind.Constructor, + DocumentationNode.Kind.Function, + DocumentationNode.Kind.CompanionObjectFunction -> renderFunction(node, renderMode) + DocumentationNode.Kind.Property, + DocumentationNode.Kind.CompanionObjectProperty -> renderProperty(node, renderMode) + else -> identifier(node.name) + } + } + } + + override fun renderName(node: DocumentationNode): String { + return when (node.kind) { + DocumentationNode.Kind.Constructor -> node.owner!!.name + else -> node.name + } + } + + override fun summarizeSignatures(nodes: List<DocumentationNode>): ContentNode? { + if (nodes.size < 2) return null + val receiverKind = nodes.getReceiverKind() ?: return null + val functionWithTypeParameter = nodes.firstOrNull { it.details(DocumentationNode.Kind.TypeParameter).any() } ?: return null + return content { + val typeParameter = functionWithTypeParameter.details(DocumentationNode.Kind.TypeParameter).first() + if (functionWithTypeParameter.kind == DocumentationNode.Kind.Function) { + renderFunction(functionWithTypeParameter, RenderMode.SUMMARY, SummarizingMapper(receiverKind, typeParameter.name)) + } + else { + renderProperty(functionWithTypeParameter, RenderMode.SUMMARY, SummarizingMapper(receiverKind, typeParameter.name)) + } + } + } + + private fun List<DocumentationNode>.getReceiverKind(): ReceiverKind? { + val qNames = map { it.getReceiverQName() }.filterNotNull() + if (qNames.size != size) + return null + + return ReceiverKind.values.firstOrNull { kind -> qNames.all { it in kind.classes } } + } + + private fun DocumentationNode.getReceiverQName(): String? { + if (kind != DocumentationNode.Kind.Function && kind != DocumentationNode.Kind.Property) return null + val receiver = details(DocumentationNode.Kind.Receiver).singleOrNull() ?: return null + return receiver.detail(DocumentationNode.Kind.Type).qualifiedNameFromType() + } + + companion object { + private val arrayClasses = setOf( + "kotlin.Array", + "kotlin.BooleanArray", + "kotlin.ByteArray", + "kotlin.CharArray", + "kotlin.ShortArray", + "kotlin.IntArray", + "kotlin.LongArray", + "kotlin.FloatArray", + "kotlin.DoubleArray" + ) + + private val arrayOrListClasses = setOf("kotlin.List") + arrayClasses + + private val iterableClasses = setOf( + "kotlin.Collection", + "kotlin.Sequence", + "kotlin.Iterable", + "kotlin.Map", + "kotlin.String", + "kotlin.CharSequence") + arrayOrListClasses + } + + private enum class ReceiverKind(val receiverName: String, val classes: Collection<String>) { + ARRAY("any_array", arrayClasses), + ARRAY_OR_LIST("any_array_or_list", arrayOrListClasses), + ITERABLE("any_iterable", iterableClasses), + } + + interface SignatureMapper { + fun renderReceiver(receiver: DocumentationNode, to: ContentBlock) + } + + private class SummarizingMapper(val kind: ReceiverKind, val typeParameterName: String): SignatureMapper { + override fun renderReceiver(receiver: DocumentationNode, to: ContentBlock) { + to.append(ContentIdentifier(kind.receiverName, IdentifierKind.SummarizedTypeName)) + to.text("<$typeParameterName>") + } + } + + private fun ContentBlock.renderPackage(node: DocumentationNode) { + keyword("package") + text(" ") + identifier(node.name) + } + + private fun ContentBlock.renderList(nodes: List<DocumentationNode>, separator: String = ", ", + noWrap: Boolean = false, renderItem: (DocumentationNode) -> Unit) { + if (nodes.none()) + return + renderItem(nodes.first()) + nodes.drop(1).forEach { + if (noWrap) { + symbol(separator.removeSuffix(" ")) + nbsp() + } else { + symbol(separator) + } + renderItem(it) + } + } + + private fun ContentBlock.renderLinked(node: DocumentationNode, body: ContentBlock.(DocumentationNode)->Unit) { + val to = node.links.firstOrNull() + if (to == null) + body(node) + else + link(to) { + body(node) + } + } + + private fun ContentBlock.renderType(node: DocumentationNode, renderMode: RenderMode) { + var typeArguments = node.details(DocumentationNode.Kind.Type) + if (node.name == "Function${typeArguments.count() - 1}") { + // lambda + val isExtension = node.annotations.any { it.name == "Extension" } + if (isExtension) { + renderType(typeArguments.first(), renderMode) + symbol(".") + typeArguments = typeArguments.drop(1) + } + symbol("(") + renderList(typeArguments.take(typeArguments.size - 1), noWrap = true) { + renderType(it, renderMode) + } + symbol(")") + nbsp() + symbol("->") + nbsp() + renderType(typeArguments.last(), renderMode) + return + } + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode, true) + renderLinked(node) { identifier(it.name, IdentifierKind.TypeName) } + if (typeArguments.any()) { + symbol("<") + renderList(typeArguments, noWrap = true) { + renderType(it, renderMode) + } + symbol(">") + } + val nullabilityModifier = node.details(DocumentationNode.Kind.NullabilityModifier).singleOrNull() + if (nullabilityModifier != null) { + symbol(nullabilityModifier.name) + } + } + + private fun ContentBlock.renderModifier(node: DocumentationNode, nowrap: Boolean = false) { + when (node.name) { + "final", "public", "var" -> {} + else -> { + keyword(node.name) + if (nowrap) { + nbsp() + } + else { + text(" ") + } + } + } + } + + private fun ContentBlock.renderTypeParameter(node: DocumentationNode, renderMode: RenderMode) { + renderModifiersForNode(node, renderMode, true) + + identifier(node.name) + + val constraints = node.details(DocumentationNode.Kind.UpperBound) + if (constraints.any()) { + nbsp() + symbol(":") + nbsp() + renderList(constraints, noWrap=true) { + renderType(it, renderMode) + } + } + } + private fun ContentBlock.renderParameter(node: DocumentationNode, renderMode: RenderMode) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + identifier(node.name, IdentifierKind.ParameterName) + symbol(":") + nbsp() + val parameterType = node.detail(DocumentationNode.Kind.Type) + renderType(parameterType, renderMode) + val valueNode = node.details(DocumentationNode.Kind.Value).firstOrNull() + if (valueNode != null) { + nbsp() + symbol("=") + nbsp() + text(valueNode.name) + } + } + + private fun ContentBlock.renderTypeParametersForNode(node: DocumentationNode, renderMode: RenderMode) { + val typeParameters = node.details(DocumentationNode.Kind.TypeParameter) + if (typeParameters.any()) { + symbol("<") + renderList(typeParameters) { + renderTypeParameter(it, renderMode) + } + symbol(">") + } + } + + private fun ContentBlock.renderSupertypesForNode(node: DocumentationNode, renderMode: RenderMode) { + val supertypes = node.details(DocumentationNode.Kind.Supertype) + if (supertypes.any()) { + nbsp() + symbol(":") + nbsp() + renderList(supertypes) { + indentedSoftLineBreak() + renderType(it, renderMode) + } + } + } + + private fun ContentBlock.renderModifiersForNode(node: DocumentationNode, + renderMode: RenderMode, + nowrap: Boolean = false) { + val modifiers = node.details(DocumentationNode.Kind.Modifier) + for (it in modifiers) { + if (node.kind == org.jetbrains.dokka.DocumentationNode.Kind.Interface && it.name == "abstract") + continue + if (renderMode == RenderMode.SUMMARY && it.name in fullOnlyModifiers) { + continue + } + renderModifier(it, nowrap) + } + } + + private fun ContentBlock.renderAnnotationsForNode(node: DocumentationNode) { + node.annotations.forEach { + renderAnnotation(it) + } + } + + private fun ContentBlock.renderAnnotation(node: DocumentationNode) { + identifier("@" + node.name, IdentifierKind.AnnotationName) + val parameters = node.details(DocumentationNode.Kind.Parameter) + if (!parameters.isEmpty()) { + symbol("(") + renderList(parameters) { + text(it.detail(DocumentationNode.Kind.Value).name) + } + symbol(")") + } + text(" ") + } + + private fun ContentBlock.renderClass(node: DocumentationNode, renderMode: RenderMode) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Class, + DocumentationNode.Kind.AnnotationClass, + DocumentationNode.Kind.Enum -> keyword("class ") + DocumentationNode.Kind.Interface -> keyword("interface ") + DocumentationNode.Kind.EnumItem -> keyword("enum val ") + DocumentationNode.Kind.Object -> keyword("object ") + else -> throw IllegalArgumentException("Node $node is not a class-like object") + } + + identifierOrDeprecated(node) + renderTypeParametersForNode(node, renderMode) + renderSupertypesForNode(node, renderMode) + } + + private fun ContentBlock.renderFunction(node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? = null) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Constructor -> identifier(node.owner!!.name) + DocumentationNode.Kind.Function, + DocumentationNode.Kind.CompanionObjectFunction -> keyword("fun ") + else -> throw IllegalArgumentException("Node $node is not a function-like object") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(DocumentationNode.Kind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + if (node.kind != org.jetbrains.dokka.DocumentationNode.Kind.Constructor) + identifierOrDeprecated(node) + + symbol("(") + val parameters = node.details(DocumentationNode.Kind.Parameter) + renderList(parameters) { + indentedSoftLineBreak() + renderParameter(it, renderMode) + } + if (needReturnType(node)) { + if (parameters.isNotEmpty()) { + softLineBreak() + } + symbol(")") + symbol(": ") + renderType(node.detail(DocumentationNode.Kind.Type), renderMode) + } + else { + symbol(")") + } + } + + private fun ContentBlock.renderReceiver(node: DocumentationNode, renderMode: RenderMode, signatureMapper: SignatureMapper?) { + val receiver = node.details(DocumentationNode.Kind.Receiver).singleOrNull() + if (receiver != null) { + if (signatureMapper != null) { + signatureMapper.renderReceiver(receiver, this) + } else { + renderType(receiver.detail(DocumentationNode.Kind.Type), renderMode) + } + symbol(".") + } + } + + private fun needReturnType(node: DocumentationNode) = when(node.kind) { + DocumentationNode.Kind.Constructor -> false + else -> !node.isUnitReturnType() + } + + fun DocumentationNode.isUnitReturnType(): Boolean = + detail(DocumentationNode.Kind.Type).hiddenLinks.firstOrNull()?.qualifiedName() == "kotlin.Unit" + + private fun ContentBlock.renderProperty(node: DocumentationNode, + renderMode: RenderMode, + signatureMapper: SignatureMapper? = null) { + if (renderMode == RenderMode.FULL) { + renderAnnotationsForNode(node) + } + renderModifiersForNode(node, renderMode) + when (node.kind) { + DocumentationNode.Kind.Property, + DocumentationNode.Kind.CompanionObjectProperty -> keyword("${node.getPropertyKeyword()} ") + else -> throw IllegalArgumentException("Node $node is not a property") + } + renderTypeParametersForNode(node, renderMode) + if (node.details(DocumentationNode.Kind.TypeParameter).any()) { + text(" ") + } + + renderReceiver(node, renderMode, signatureMapper) + + identifierOrDeprecated(node) + symbol(": ") + renderType(node.detail(DocumentationNode.Kind.Type), renderMode) + } + + fun DocumentationNode.getPropertyKeyword() = + if (details(DocumentationNode.Kind.Modifier).any { it.name == "var" }) "var" else "val" + + fun ContentBlock.identifierOrDeprecated(node: DocumentationNode) { + if (node.deprecation != null) { + val strike = ContentStrikethrough() + strike.identifier(node.name) + append(strike) + } else { + identifier(node.name) + } + } +} + +fun DocumentationNode.qualifiedNameFromType() = (links.firstOrNull() ?: hiddenLinks.firstOrNull())?.qualifiedName() ?: name |