diff options
Diffstat (limited to 'subprojects/analysis-kotlin-symbols/src/main/kotlin/org')
35 files changed, 3182 insertions, 0 deletions
diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt new file mode 100644 index 00000000..c1b8651a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt @@ -0,0 +1,170 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.analysis.java.parsers.JavadocParser +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType +import org.jetbrains.kotlin.psi.psiUtil.isPropertyParameter +import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly + +internal fun KtAnalysisSession.getJavaDocDocumentationFrom( + symbol: KtSymbol, + javadocParser: JavadocParser +): DocumentationNode? { + if (symbol.origin == KtSymbolOrigin.JAVA) { + return (symbol.psi as? PsiNamedElement)?.let { + javadocParser.parseDocumentation(it) + } + } else if (symbol.origin == KtSymbolOrigin.SOURCE && symbol is KtCallableSymbol) { + // Note: javadocParser searches in overridden JAVA declarations for JAVA method, not Kotlin + symbol.getAllOverriddenSymbols().forEach { overrider -> + if (overrider.origin == KtSymbolOrigin.JAVA) + return@getJavaDocDocumentationFrom (overrider.psi as? PsiNamedElement)?.let { + javadocParser.parseDocumentation(it) + } + } + } + return null +} + +internal fun KtAnalysisSession.getKDocDocumentationFrom(symbol: KtSymbol, logger: DokkaLogger) = findKDoc(symbol)?.let { kDocContent -> + + val ktElement = symbol.psi + val kdocLocation = ktElement?.containingFile?.name?.let { + val name = when(symbol) { + is KtCallableSymbol -> symbol.callableIdIfNonLocal?.toString() + is KtClassOrObjectSymbol -> symbol.classIdIfNonLocal?.toString() + is KtNamedSymbol -> symbol.name.asString() + else -> null + }?.replace('/', '.') // replace to be compatible with K1 + + if (name != null) "$it/$name" + else it + } + + + parseFromKDocTag( + kDocTag = kDocContent.contentTag, + externalDri = { link -> resolveKDocLink(link).logIfNotResolved(link.getLinkText(), logger) }, + kdocLocation = kdocLocation + ) +} + + + + +// ----------- copy-paste from IDE ---------------------------------------------------------------------------- + +internal data class KDocContent( + val contentTag: KDocTag, + val sections: List<KDocSection> +) + +internal fun KtAnalysisSession.findKDoc(symbol: KtSymbol): KDocContent? { + // for generated function (e.g. `copy`) psi returns class, see test `data class kdocs over generated methods` + if (symbol.origin != KtSymbolOrigin.SOURCE) return null + val ktElement = symbol.psi as? KtElement + ktElement?.findKDoc()?.let { + return it + } + + if (symbol is KtCallableSymbol) { + symbol.getAllOverriddenSymbols().forEach { overrider -> + findKDoc(overrider)?.let { + return it + } + } + } + return null +} + + +internal fun KtElement.findKDoc(): KDocContent? = this.lookupOwnedKDoc() + ?: this.lookupKDocInContainer() + + + +private fun KtElement.lookupOwnedKDoc(): KDocContent? { + // KDoc for primary constructor is located inside of its class KDoc + val psiDeclaration = when (this) { + is KtPrimaryConstructor -> getContainingClassOrObject() + else -> this + } + + if (psiDeclaration is KtDeclaration) { + val kdoc = psiDeclaration.docComment + if (kdoc != null) { + if (this is KtConstructor<*>) { + // ConstructorDescriptor resolves to the same JetDeclaration + val constructorSection = kdoc.findSectionByTag(KDocKnownTag.CONSTRUCTOR) + if (constructorSection != null) { + // if annotated with @constructor tag and the caret is on constructor definition, + // then show @constructor description as the main content, and additional sections + // that contain @param tags (if any), as the most relatable ones + // practical example: val foo = Fo<caret>o("argument") -- show @constructor and @param content + val paramSections = kdoc.findSectionsContainingTag(KDocKnownTag.PARAM) + return KDocContent(constructorSection, paramSections) + } + } + return KDocContent(kdoc.getDefaultSection(), kdoc.getAllSections()) + } + } + + return null +} + +/** + * Looks for sections that have a deeply nested [tag], + * as opposed to [KDoc.findSectionByTag], which only looks among the top level + */ +private fun KDoc.findSectionsContainingTag(tag: KDocKnownTag): List<KDocSection> { + return getChildrenOfType<KDocSection>() + .filter { it.findTagByName(tag.name.toLowerCaseAsciiOnly()) != null } +} + +private fun KtElement.lookupKDocInContainer(): KDocContent? { + val subjectName = name + val containingDeclaration = + PsiTreeUtil.findFirstParent(this, true) { + it is KtDeclarationWithBody && it !is KtPrimaryConstructor + || it is KtClassOrObject + } + + val containerKDoc = containingDeclaration?.getChildOfType<KDoc>() + if (containerKDoc == null || subjectName == null) return null + val propertySection = containerKDoc.findSectionByTag(KDocKnownTag.PROPERTY, subjectName) + val paramTag = + containerKDoc.findDescendantOfType<KDocTag> { it.knownTag == KDocKnownTag.PARAM && it.getSubjectName() == subjectName } + + val primaryContent = when { + // class Foo(val <caret>s: String) + this is KtParameter && this.isPropertyParameter() -> propertySection ?: paramTag + // fun some(<caret>f: String) || class Some<<caret>T: Base> || Foo(<caret>s = "argument") + this is KtParameter || this is KtTypeParameter -> paramTag + // if this property is declared separately (outside primary constructor), but it's for some reason + // annotated as @property in class's description, instead of having its own KDoc + this is KtProperty && containingDeclaration is KtClassOrObject -> propertySection + else -> null + } + return primaryContent?.let { + // makes little sense to include any other sections, since we found + // documentation for a very specific element, like a property/param + KDocContent(it, sections = emptyList()) + } +} + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt new file mode 100644 index 00000000..ec23b0c8 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt @@ -0,0 +1,97 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser.Companion.fqDeclarationName +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.Suppress +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.psiUtil.allChildren + +internal fun parseFromKDocTag( + kDocTag: KDocTag?, + externalDri: (KDocLink) -> DRI?, + kdocLocation: String?, + parseWithChildren: Boolean = true +): DocumentationNode { + return if (kDocTag == null) { + DocumentationNode(emptyList()) + } else { + fun parseStringToDocNode(text: String, externalDRIProvider: (String) -> DRI?) = + MarkdownParser(externalDRIProvider, kdocLocation).parseStringToDocNode(text) + + fun pointedLink(tag: KDocTag): DRI? = tag.getSubjectLink()?.let(externalDri) + + val allTags = + listOf(kDocTag) + if (kDocTag.canHaveParent() && parseWithChildren) getAllKDocTags(findParent(kDocTag)) else emptyList() + DocumentationNode( + allTags.map { tag -> + val links = tag.allChildren.filterIsInstance<KDocLink>().associate { it.getLinkText() to externalDri(it) } + val externalDRIProvider = { linkText: String -> links[linkText] } + + when (tag.knownTag) { + null -> if (tag.name == null) Description(parseStringToDocNode(tag.getContent(), externalDRIProvider)) else CustomTagWrapper( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.name!! + ) + KDocKnownTag.AUTHOR -> Author(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.THROWS -> { + val dri = pointedLink(tag) + Throws( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + dri?.fqDeclarationName() ?: tag.getSubjectName().orEmpty(), + dri, + ) + } + KDocKnownTag.EXCEPTION -> { + val dri = pointedLink(tag) + Throws( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + dri?.fqDeclarationName() ?: tag.getSubjectName().orEmpty(), + dri + ) + } + KDocKnownTag.PARAM -> Param( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.getSubjectName().orEmpty() + ) + KDocKnownTag.RECEIVER -> Receiver(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.RETURN -> Return(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.SEE -> { + val dri = pointedLink(tag) + See( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + dri?.fqDeclarationName() ?: tag.getSubjectName().orEmpty(), + dri, + ) + } + KDocKnownTag.SINCE -> Since(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.CONSTRUCTOR -> Constructor(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.PROPERTY -> Property( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.getSubjectName().orEmpty() + ) + KDocKnownTag.SAMPLE -> Sample( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.getSubjectName().orEmpty() + ) + KDocKnownTag.SUPPRESS -> Suppress(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + } + } + ) + } +} + +private fun findParent(kDoc: PsiElement): PsiElement = + if (kDoc.canHaveParent()) findParent(kDoc.parent) else kDoc + +private fun PsiElement.canHaveParent(): Boolean = this is KDocSection && knownTag != KDocKnownTag.PROPERTY + +private fun getAllKDocTags(kDocImpl: PsiElement): List<KDocTag> = + kDocImpl.children.filterIsInstance<KDocTag>().filterNot { it is KDocSection } + kDocImpl.children.flatMap { + getAllKDocTags(it) + } diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt new file mode 100644 index 00000000..9d26d917 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromSymbol +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink +import org.jetbrains.kotlin.kdoc.psi.impl.KDocName +import org.jetbrains.kotlin.psi.KtPsiFactory + +internal fun DRI?.logIfNotResolved(link: String, logger: DokkaLogger): DRI? { + if(this == null) + logger.warn("Couldn't resolve link for $link") + return this +} + +/** + * It resolves KDoc link via creating PSI. + * + */ +internal fun KtAnalysisSession.resolveKDocTextLink(link: String, context: PsiElement? = null): DRI? { + val psiFactory = context?.let { KtPsiFactory.contextual(it) } ?: KtPsiFactory(this.useSiteModule.project) + val kDoc = psiFactory.createComment( + """ + /** + * [$link] + */ + """.trimIndent() + ) as? KDoc + val kDocLink = kDoc?.getDefaultSection()?.children?.filterIsInstance<KDocLink>()?.singleOrNull() + return kDocLink?.let { resolveKDocLink(it) } +} + +internal fun KtAnalysisSession.resolveKDocLink(link: KDocLink): DRI? { + val lastNameSegment = link.children.filterIsInstance<KDocName>().lastOrNull() + val linkedSymbol = lastNameSegment?.mainReference?.resolveToSymbols()?.firstOrNull() + return if (linkedSymbol == null) null // logger.warn("Couldn't resolve link for $link") + else getDRIFromSymbol(linkedSymbol) +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt new file mode 100644 index 00000000..9b108f80 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtPossibleMemberSymbol +import org.jetbrains.kotlin.builtins.StandardNames + +private const val ENUM_ENTRIES_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumEntries.kt.template" +private const val ENUM_VALUEOF_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumValueOf.kt.template" +private const val ENUM_VALUES_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumValues.kt.template" + +internal fun KtAnalysisSession.hasGeneratedKDocDocumentation(symbol: KtSymbol): Boolean = + getDocumentationTemplatePath(symbol) != null + +private fun KtAnalysisSession.getDocumentationTemplatePath(symbol: KtSymbol): String? = + when (symbol) { + is KtPropertySymbol -> if (isEnumEntriesProperty(symbol)) ENUM_ENTRIES_TEMPLATE_PATH else null + is KtFunctionSymbol -> { + when { + isEnumValuesMethod(symbol) -> ENUM_VALUES_TEMPLATE_PATH + isEnumValueOfMethod(symbol) -> ENUM_VALUEOF_TEMPLATE_PATH + else -> null + } + } + + else -> null + } + +private fun KtAnalysisSession.isEnumSpecialMember(symbol: KtPossibleMemberSymbol): Boolean = + symbol.origin == KtSymbolOrigin.SOURCE_MEMBER_GENERATED + && (symbol.getContainingSymbol() as? KtClassOrObjectSymbol)?.classKind == KtClassKind.ENUM_CLASS + +private fun KtAnalysisSession.isEnumEntriesProperty(symbol: KtPropertySymbol): Boolean = + symbol.name == StandardNames.ENUM_ENTRIES && isEnumSpecialMember(symbol) + +private fun KtAnalysisSession.isEnumValuesMethod(symbol: KtFunctionSymbol): Boolean = + symbol.name == StandardNames.ENUM_VALUES && isEnumSpecialMember(symbol) + +private fun KtAnalysisSession.isEnumValueOfMethod(symbol: KtFunctionSymbol): Boolean = + symbol.name == StandardNames.ENUM_VALUE_OF && isEnumSpecialMember(symbol) + +internal fun KtAnalysisSession.getGeneratedKDocDocumentationFrom(symbol: KtSymbol): DocumentationNode? { + val templatePath = getDocumentationTemplatePath(symbol) ?: return null + return loadTemplate(templatePath) +} + +private fun KtAnalysisSession.loadTemplate(filePath: String): DocumentationNode? { + val kdoc = loadContent(filePath) ?: return null + val externalDriProvider = { link: String -> + resolveKDocTextLink(link) + } + + val parser = MarkdownParser(externalDriProvider, filePath) + return parser.parse(kdoc) +} + +private fun loadContent(filePath: String): String? = + SymbolsAnalysisPlugin::class.java.getResource(filePath)?.readText() diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorDocumentationContent.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorDocumentationContent.kt new file mode 100644 index 00000000..a3651603 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorDocumentationContent.kt @@ -0,0 +1,15 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.dokka.analysis.java.JavadocTag +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag + +internal data class DescriptorDocumentationContent( + val resolveDocContext: ResolveDocContext, + val element: KDocTag, + override val tag: JavadocTag, +) : DocumentationContent { + override fun resolveSiblings(): List<DocumentationContent> { + return listOf(this) + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorKotlinDocCommentCreator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorKotlinDocCommentCreator.kt new file mode 100644 index 00000000..399d005a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorKotlinDocCommentCreator.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.analysis.java.doccomment.DocCommentCreator +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.findKDoc +import org.jetbrains.kotlin.psi.KtElement + +internal class DescriptorKotlinDocCommentCreator : DocCommentCreator { + override fun create(element: PsiNamedElement): DocComment? { + val ktElement = element.navigationElement as? KtElement ?: return null + val kdoc = ktElement.findKDoc() ?: return null + + return KotlinDocComment(kdoc.contentTag, ResolveDocContext(ktElement)) + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocComment.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocComment.kt new file mode 100644 index 00000000..385b5328 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocComment.kt @@ -0,0 +1,81 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import org.jetbrains.dokka.analysis.java.* +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.KtElement + +internal class ResolveDocContext(val ktElement: KtElement) + +internal class KotlinDocComment( + val comment: KDocTag, + val resolveDocContext: ResolveDocContext +) : DocComment { + + private val tagsWithContent: List<KDocTag> = comment.children.mapNotNull { (it as? KDocTag) } + + override fun hasTag(tag: JavadocTag): Boolean { + return when (tag) { + is DescriptionJavadocTag -> comment.getContent().isNotEmpty() + is ThrowingExceptionJavadocTag -> tagsWithContent.any { it.hasException(tag) } + else -> tagsWithContent.any { it.text.startsWith("@${tag.name}") } + } + } + + private fun KDocTag.hasException(tag: ThrowingExceptionJavadocTag) = + text.startsWith("@${tag.name}") && getSubjectName() == tag.exceptionQualifiedName + + override fun resolveTag(tag: JavadocTag): List<DocumentationContent> { + return when (tag) { + is DescriptionJavadocTag -> listOf(DescriptorDocumentationContent(resolveDocContext, comment, tag)) + is ParamJavadocTag -> { + val resolvedContent = resolveGeneric(tag) + listOf(resolvedContent[tag.paramIndex]) + } + + is ThrowsJavadocTag -> resolveThrowingException(tag) + is ExceptionJavadocTag -> resolveThrowingException(tag) + else -> resolveGeneric(tag) + } + } + + private fun resolveThrowingException(tag: ThrowingExceptionJavadocTag): List<DescriptorDocumentationContent> { + val exceptionName = tag.exceptionQualifiedName ?: return resolveGeneric(tag) + + return comment.children + .filterIsInstance<KDocTag>() + .filter { it.name == tag.name && it.getSubjectName() == exceptionName } + .map { DescriptorDocumentationContent(resolveDocContext, it, tag) } + } + + private fun resolveGeneric(tag: JavadocTag): List<DescriptorDocumentationContent> { + return comment.children.mapNotNull { element -> + if (element is KDocTag && element.name == tag.name) { + DescriptorDocumentationContent(resolveDocContext, element, tag) + } else { + null + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KotlinDocComment + + if (comment != other.comment) return false + //if (resolveDocContext.name != other.resolveDocContext.name) return false + if (tagsWithContent != other.tagsWithContent) return false + + return true + } + + override fun hashCode(): Int { + var result = comment.hashCode() + // result = 31 * result + resolveDocContext.name.hashCode() + result = 31 * result + tagsWithContent.hashCode() + return result + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt new file mode 100644 index 00000000..c8ecc786 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt @@ -0,0 +1,46 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.analysis.java.parsers.DocCommentParser +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logIfNotResolved +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.parseFromKDocTag +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocLink +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.kotlin.analysis.api.analyze + +internal class KotlinDocCommentParser( + private val context: DokkaContext +) : DocCommentParser { + + override fun canParse(docComment: DocComment): Boolean { + return docComment is KotlinDocComment + } + + override fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode { + val kotlinDocComment = docComment as KotlinDocComment + return parseDocumentation(kotlinDocComment) + } + + fun parseDocumentation(element: KotlinDocComment, parseWithChildren: Boolean = true): DocumentationNode { + val sourceSet = context.configuration.sourceSets.let { sourceSets -> + sourceSets.firstOrNull { it.sourceSetID.sourceSetName == "jvmMain" } + ?: sourceSets.first { it.analysisPlatform == Platform.jvm } + } + val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + return analyze(kotlinAnalysis[sourceSet].mainModule) { + parseFromKDocTag( + kDocTag = element.comment, + externalDri = { link -> resolveKDocLink(link).logIfNotResolved(link.getLinkText(), context.logger) }, + kdocLocation = null, + parseWithChildren = parseWithChildren + ) + } + } +} + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinInheritDocTagContentProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinInheritDocTagContentProvider.kt new file mode 100644 index 00000000..6d2bfff4 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinInheritDocTagContentProvider.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.java.parsers.doctag.DocTagParserContext +import org.jetbrains.dokka.analysis.java.parsers.doctag.InheritDocTagContentProvider +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query + +internal class KotlinInheritDocTagContentProvider( + context: DokkaContext +) : InheritDocTagContentProvider { + + val parser: KotlinDocCommentParser by lazy { + context.plugin<JavaAnalysisPlugin>().query { docCommentParsers } + .single { it is KotlinDocCommentParser } as KotlinDocCommentParser + } + + override fun canConvert(content: DocumentationContent): Boolean = content is DescriptorDocumentationContent + + override fun convertToHtml(content: DocumentationContent, docTagParserContext: DocTagParserContext): String { + val descriptorContent = content as DescriptorDocumentationContent + val inheritedDocNode = parser.parseDocumentation( + KotlinDocComment(descriptorContent.element, descriptorContent.resolveDocContext), + parseWithChildren = false + ) + val id = docTagParserContext.store(inheritedDocNode) + return """<inheritdoc id="$id"/>""" + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/IllegalModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/IllegalModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..6e3f215a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/IllegalModuleAndPackageDocumentation.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.DokkaException + +internal class IllegalModuleAndPackageDocumentation( + source: ModuleAndPackageDocumentationSource, message: String +) : DokkaException("[$source] $message") diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..46f714cb --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentation.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.model.doc.DocumentationNode + +internal data class ModuleAndPackageDocumentation( + val name: String, + val classifier: Classifier, + val documentation: DocumentationNode +) { + enum class Classifier { Module, Package } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationFragment.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationFragment.kt new file mode 100644 index 00000000..1eafc688 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationFragment.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + + +internal data class ModuleAndPackageDocumentationFragment( + val name: String, + val classifier: ModuleAndPackageDocumentation.Classifier, + val documentation: String, + val source: ModuleAndPackageDocumentationSource +) diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt new file mode 100644 index 00000000..cf627e77 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt @@ -0,0 +1,47 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logIfNotResolved +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Module +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Package +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLink +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.name.FqName + +internal fun interface ModuleAndPackageDocumentationParsingContext { + fun markdownParserFor(fragment: ModuleAndPackageDocumentationFragment, location: String): MarkdownParser +} + +internal fun ModuleAndPackageDocumentationParsingContext.parse( + fragment: ModuleAndPackageDocumentationFragment +): DocumentationNode { + return markdownParserFor(fragment, fragment.source.sourceDescription).parse(fragment.documentation) +} + +internal fun ModuleAndPackageDocumentationParsingContext( + logger: DokkaLogger, + kotlinAnalysis: KotlinAnalysis? = null, + sourceSet: DokkaConfiguration.DokkaSourceSet? = null +) = ModuleAndPackageDocumentationParsingContext { fragment, sourceLocation -> + + if(kotlinAnalysis == null || sourceSet == null) { + MarkdownParser(externalDri = { null }, sourceLocation) + } else { + val analysisContext = kotlinAnalysis[sourceSet] + analyze(analysisContext.mainModule) { + val contextSymbol = when (fragment.classifier) { + Module -> ROOT_PACKAGE_SYMBOL + Package -> getPackageSymbolIfPackageExists(FqName(fragment.name)) + } + + MarkdownParser( + externalDri = { resolveKDocTextLink(it, contextSymbol?.psi).logIfNotResolved(it, logger) }, + sourceLocation + ) + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationReader.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationReader.kt new file mode 100644 index 00000000..82259b89 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationReader.kt @@ -0,0 +1,110 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.Deprecated +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.associateWithNotNull +import org.jetbrains.dokka.analysis.kotlin.internal.ModuleAndPackageDocumentationReader + +internal fun ModuleAndPackageDocumentationReader(context: DokkaContext): ModuleAndPackageDocumentationReader = + ContextModuleAndPackageDocumentationReader(context) + +private class ContextModuleAndPackageDocumentationReader( + private val context: DokkaContext +) : ModuleAndPackageDocumentationReader { + + private val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + + private val documentationFragments: SourceSetDependent<List<ModuleAndPackageDocumentationFragment>> = + context.configuration.sourceSets.associateWith { sourceSet -> + sourceSet.includes.flatMap { include -> parseModuleAndPackageDocumentationFragments(include) } + } + + private fun findDocumentationNodes( + sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, + predicate: (ModuleAndPackageDocumentationFragment) -> Boolean + ): SourceSetDependent<DocumentationNode> { + return sourceSets.associateWithNotNull { sourceSet -> + val fragments = documentationFragments[sourceSet].orEmpty().filter(predicate) + kotlinAnalysis[sourceSet] // test: to throw exception for unknown sourceSet + val documentations = fragments.map { fragment -> + parseModuleAndPackageDocumentation( + context = ModuleAndPackageDocumentationParsingContext(context.logger, kotlinAnalysis, sourceSet), + fragment = fragment + ) + } + when (documentations.size) { + 0 -> null + 1 -> documentations.single().documentation + else -> DocumentationNode(documentations.flatMap { it.documentation.children } + .mergeDocumentationNodes()) + } + } + } + + private val ModuleAndPackageDocumentationFragment.canonicalPackageName: String + get() { + check(classifier == Classifier.Package) + if (name == "[root]") return "" + return name + } + + override fun read(module: DModule): SourceSetDependent<DocumentationNode> { + return findDocumentationNodes(module.sourceSets) { fragment -> + fragment.classifier == Classifier.Module && (fragment.name == module.name) + } + } + + override fun read(pkg: DPackage): SourceSetDependent<DocumentationNode> { + return findDocumentationNodes(pkg.sourceSets) { fragment -> + fragment.classifier == Classifier.Package && fragment.canonicalPackageName == pkg.dri.packageName + } + } + + override fun read(module: DokkaConfiguration.DokkaModuleDescription): DocumentationNode? { + val parsingContext = ModuleAndPackageDocumentationParsingContext(context.logger) + + val documentationFragment = module.includes + .flatMap { include -> parseModuleAndPackageDocumentationFragments(include) } + .firstOrNull { fragment -> fragment.classifier == Classifier.Module && fragment.name == module.name } + ?: return null + + val moduleDocumentation = parseModuleAndPackageDocumentation(parsingContext, documentationFragment) + return moduleDocumentation.documentation + } + + private fun List<TagWrapper>.mergeDocumentationNodes(): List<TagWrapper> = + groupBy { it::class }.values.map { + it.reduce { acc, tagWrapper -> + val newRoot = CustomDocTag( + acc.children + tagWrapper.children, + name = (tagWrapper as? NamedTagWrapper)?.name.orEmpty() + ) + when (acc) { + is See -> acc.copy(newRoot) + is Param -> acc.copy(newRoot) + is Throws -> acc.copy(newRoot) + is Sample -> acc.copy(newRoot) + is Property -> acc.copy(newRoot) + is CustomTagWrapper -> acc.copy(newRoot) + is Description -> acc.copy(newRoot) + is Author -> acc.copy(newRoot) + is Version -> acc.copy(newRoot) + is Since -> acc.copy(newRoot) + is Return -> acc.copy(newRoot) + is Receiver -> acc.copy(newRoot) + is Constructor -> acc.copy(newRoot) + is Deprecated -> acc.copy(newRoot) + is org.jetbrains.dokka.model.doc.Suppress -> acc.copy(newRoot) + } + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationSource.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationSource.kt new file mode 100644 index 00000000..d7877559 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationSource.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import java.io.File + +internal abstract class ModuleAndPackageDocumentationSource { + abstract val sourceDescription: String + abstract val documentation: String + override fun toString(): String = sourceDescription +} + +internal data class ModuleAndPackageDocumentationFile(private val file: File) : ModuleAndPackageDocumentationSource() { + override val sourceDescription: String = file.path + override val documentation: String by lazy(LazyThreadSafetyMode.PUBLICATION) { file.readText() } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..b92adf4a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentation.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +internal fun parseModuleAndPackageDocumentation( + context: ModuleAndPackageDocumentationParsingContext, + fragment: ModuleAndPackageDocumentationFragment +): ModuleAndPackageDocumentation { + return ModuleAndPackageDocumentation( + name = fragment.name, + classifier = fragment.classifier, + documentation = context.parse(fragment) + ) +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentationFragments.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentationFragments.kt new file mode 100644 index 00000000..ae728a28 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentationFragments.kt @@ -0,0 +1,55 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Module +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Package +import java.io.File + +internal fun parseModuleAndPackageDocumentationFragments(source: File): List<ModuleAndPackageDocumentationFragment> { + return parseModuleAndPackageDocumentationFragments(ModuleAndPackageDocumentationFile(source)) +} + +internal fun parseModuleAndPackageDocumentationFragments( + source: ModuleAndPackageDocumentationSource +): List<ModuleAndPackageDocumentationFragment> { + val fragmentStrings = source.documentation.split(Regex("(|^)#\\s*(?=(Module|Package))")) + return fragmentStrings + .filter(String::isNotBlank) + .map { fragmentString -> parseModuleAndPackageDocFragment(source, fragmentString) } +} + +private fun parseModuleAndPackageDocFragment( + source: ModuleAndPackageDocumentationSource, + fragment: String +): ModuleAndPackageDocumentationFragment { + val firstLineAndDocumentation = fragment.split("\r\n", "\n", "\r", limit = 2) + val firstLine = firstLineAndDocumentation[0] + + val classifierAndName = firstLine.split(Regex("\\s+"), limit = 2) + + val classifier = when (classifierAndName[0].trim()) { + "Module" -> Module + "Package" -> Package + else -> throw IllegalStateException( + """Unexpected classifier: "${classifierAndName[0]}", expected either "Module" or "Package". + |For more information consult the specification: https://kotlinlang.org/docs/dokka-module-and-package-docs.html""".trimMargin() + ) + } + + if (classifierAndName.size != 2 && classifier == Module) { + throw IllegalModuleAndPackageDocumentation(source, "Missing Module name") + } + + val name = classifierAndName.getOrNull(1)?.trim().orEmpty() + if (classifier == Package && name.contains(Regex("\\s"))) { + throw IllegalModuleAndPackageDocumentation( + source, "Package name cannot contain whitespace in '$firstLine'" + ) + } + + return ModuleAndPackageDocumentationFragment( + name = name, + classifier = classifier, + documentation = firstLineAndDocumentation.getOrNull(1)?.trim().orEmpty(), + source = source + ) +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/AnalysisContext.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/AnalysisContext.kt new file mode 100644 index 00000000..eb7c5d70 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/AnalysisContext.kt @@ -0,0 +1,139 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.plugin + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule +import java.io.Closeable +import java.io.File + +@Suppress("FunctionName", "UNUSED_PARAMETER") +internal fun SamplesKotlinAnalysis( + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + context: DokkaContext, + projectKotlinAnalysis: KotlinAnalysis +): KotlinAnalysis { + val environments = sourceSets + .filter { it.samples.isNotEmpty() } + .associateWith { sourceSet -> + createAnalysisContext( + classpath = sourceSet.classpath, + sourceRoots = sourceSet.samples, + sourceSet = sourceSet + ) + } + + return EnvironmentKotlinAnalysis(environments, projectKotlinAnalysis) +} + +internal fun ProjectKotlinAnalysis( + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + context: DokkaContext, +): KotlinAnalysis { + val environments = sourceSets.associateWith { sourceSet -> + createAnalysisContext( + context = context, + sourceSets = sourceSets, + sourceSet = sourceSet + ) + } + return EnvironmentKotlinAnalysis(environments) +} + + +@Suppress("UNUSED_PARAMETER") +internal fun createAnalysisContext( + context: DokkaContext, + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + sourceSet: DokkaConfiguration.DokkaSourceSet +): AnalysisContext { + val parentSourceSets = sourceSets.filter { it.sourceSetID in sourceSet.dependentSourceSets } + val classpath = sourceSet.classpath + parentSourceSets.flatMap { it.classpath } + val sources = sourceSet.sourceRoots + parentSourceSets.flatMap { it.sourceRoots } + + return createAnalysisContext(classpath, sources, sourceSet) +} + +internal fun createAnalysisContext( + classpath: List<File>, + sourceRoots: Set<File>, + sourceSet: DokkaConfiguration.DokkaSourceSet +): AnalysisContext { + val applicationDisposable: Disposable = Disposer.newDisposable("StandaloneAnalysisAPISession.application") + val projectDisposable: Disposable = Disposer.newDisposable("StandaloneAnalysisAPISession.project") + + val analysis= createAnalysisSession( + classpath = classpath, + sourceRoots = sourceRoots, + analysisPlatform = sourceSet.analysisPlatform, + languageVersion = sourceSet.languageVersion, + apiVersion = sourceSet.apiVersion, + applicationDisposable = applicationDisposable, + projectDisposable = projectDisposable + ) + return AnalysisContextImpl( + mainModule = analysis.second, + analysisSession = analysis.first, + applicationDisposable = applicationDisposable, + projectDisposable = projectDisposable + ) +} + + +/** + * First child delegation. It does not close [parent]. + */ +internal abstract class KotlinAnalysis( + private val parent: KotlinAnalysis? = null +) : Closeable { + + operator fun get(key: DokkaConfiguration.DokkaSourceSet): AnalysisContext { + return get(key.sourceSetID) + } + + internal operator fun get(key: DokkaSourceSetID): AnalysisContext { + return find(key) + ?: parent?.get(key) + ?: throw IllegalStateException("Missing EnvironmentAndFacade for sourceSet $key") + } + + internal abstract fun find(sourceSetID: DokkaSourceSetID): AnalysisContext? +} + +internal open class EnvironmentKotlinAnalysis( + private val environments: SourceSetDependent<AnalysisContext>, + parent: KotlinAnalysis? = null, +) : KotlinAnalysis(parent = parent) { + + override fun find(sourceSetID: DokkaSourceSetID): AnalysisContext? = + environments.entries.firstOrNull { (sourceSet, _) -> sourceSet.sourceSetID == sourceSetID }?.value + + override fun close() { + environments.values.forEach(AnalysisContext::close) + } +} + +internal interface AnalysisContext: Closeable { + val project: Project + val mainModule: KtSourceModule +} + +private class AnalysisContextImpl( + override val mainModule: KtSourceModule, + private val analysisSession: StandaloneAnalysisAPISession, + private val applicationDisposable: Disposable, + private val projectDisposable: Disposable +) : AnalysisContext { + override val project: Project + get() = analysisSession.project + + override fun close() { + Disposer.dispose(applicationDisposable) + Disposer.dispose(projectDisposable) + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/KotlinAnalysis.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/KotlinAnalysis.kt new file mode 100644 index 00000000..8b9e552d --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/KotlinAnalysis.kt @@ -0,0 +1,245 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.plugin + +import com.intellij.core.CoreApplicationEnvironment +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.PsiManager +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.ProjectScope +import com.intellij.util.io.URLUtil +import org.jetbrains.dokka.Platform +import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils +import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider +import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule +import org.jetbrains.kotlin.analysis.project.structure.builder.KtModuleBuilder +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSdkModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.platform.CommonPlatforms +import org.jetbrains.kotlin.platform.js.JsPlatforms +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.platform.konan.NativePlatforms +import org.jetbrains.kotlin.psi.KtFile +import java.io.File +import java.io.IOException +import java.nio.file.* +import java.nio.file.attribute.BasicFileAttributes + +internal fun Platform.toTargetPlatform() = when (this) { + Platform.js, Platform.wasm -> JsPlatforms.defaultJsPlatform + Platform.common -> CommonPlatforms.defaultCommonPlatform + Platform.native -> NativePlatforms.unspecifiedNativePlatform + Platform.jvm -> JvmPlatforms.defaultJvmPlatform +} + +private fun getJdkHomeFromSystemProperty(): File? { + val javaHome = File(System.getProperty("java.home")) + if (!javaHome.exists()) { + // messageCollector.report(CompilerMessageSeverity.WARNING, "Set existed java.home to use JDK") + return null + } + return javaHome +} + +internal fun getLanguageVersionSettings( + languageVersionString: String?, + apiVersionString: String? +): LanguageVersionSettingsImpl { + val languageVersion = LanguageVersion.fromVersionString(languageVersionString) ?: LanguageVersion.LATEST_STABLE + val apiVersion = + apiVersionString?.let { ApiVersion.parse(it) } ?: ApiVersion.createByLanguageVersion(languageVersion) + return LanguageVersionSettingsImpl( + languageVersion = languageVersion, + apiVersion = apiVersion, analysisFlags = hashMapOf( + // special flag for Dokka + // force to resolve light classes (lazily by default) + AnalysisFlags.eagerResolveOfLightClasses to true + ) + ) +} + +// it should be changed after https://github.com/Kotlin/dokka/issues/3114 +internal fun createAnalysisSession( + classpath: List<File>, + sourceRoots: Set<File>, + analysisPlatform: Platform, + languageVersion: String?, + apiVersion: String?, + applicationDisposable: Disposable, + projectDisposable: Disposable +): Pair<StandaloneAnalysisAPISession, KtSourceModule> { + + var sourceModule: KtSourceModule? = null + val analysisSession = buildStandaloneAnalysisAPISession( + applicationDisposable = applicationDisposable, + projectDisposable = projectDisposable, + withPsiDeclarationFromBinaryModuleProvider = false + ) { + val project = project + val targetPlatform = analysisPlatform.toTargetPlatform() + fun KtModuleBuilder.addModuleDependencies(moduleName: String) { + val libraryRoots = classpath + addRegularDependency( + buildKtLibraryModule { + contentScope = ProjectScope.getLibrariesScope(project) + this.platform = targetPlatform + this.project = project + binaryRoots = libraryRoots.map { it.toPath() } + libraryName = "Library for $moduleName" + } + ) + getJdkHomeFromSystemProperty()?.let { jdkHome -> + val vfm = VirtualFileManager.getInstance() + val jdkHomePath = jdkHome.toPath() + val jdkHomeVirtualFile = vfm.findFileByNioPath(jdkHome.toPath())//vfm.findFileByPath(jdkHomePath) + val binaryRoots = LibraryUtils.findClassesFromJdkHome(jdkHomePath).map { + Paths.get(URLUtil.extractPath(it)) + } + addRegularDependency( + buildKtSdkModule { + contentScope = GlobalSearchScope.fileScope(project, jdkHomeVirtualFile) + this.platform = targetPlatform + this.project = project + this.binaryRoots = binaryRoots + sdkName = "JDK for $moduleName" + } + ) + } + } + sourceModule = buildKtSourceModule { + this.languageVersionSettings = getLanguageVersionSettings(languageVersion, apiVersion) + + //val fs = StandardFileSystems.local() + //val psiManager = PsiManager.getInstance(project) + // TODO: We should handle (virtual) file changes announced via LSP with the VFS + /*val ktFiles = sources + .flatMap { Files.walk(it).toList() } + .mapNotNull { fs.findFileByPath(it.toString()) } + .mapNotNull { psiManager.findFile(it) } + .map { it as KtFile }*/ + val sourcePaths = sourceRoots.map { it.absolutePath } + val (ktFilePath, javaFilePath) = getSourceFilePaths(sourcePaths).partition { it.endsWith(KotlinFileType.EXTENSION) } + val javaFiles: List<PsiFileSystemItem> = getPsiFilesFromPaths(project, javaFilePath) + val ktFiles: List<KtFile> = getPsiFilesFromPaths(project, getSourceFilePaths(ktFilePath)) + addSourceRoots(ktFiles + javaFiles) + contentScope = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, ktFiles) + platform = targetPlatform + moduleName = "<module>" + this.project = project + addModuleDependencies(moduleName) + } + + buildKtModuleProvider { + platform = targetPlatform + this.project = project + addModule(sourceModule!!) + } + } + // TODO remove further + CoreApplicationEnvironment.registerExtensionPoint( + analysisSession.project.extensionArea, + KtResolveExtensionProvider.EP_NAME.name, + KtResolveExtensionProvider::class.java + ) + return Pair(analysisSession, sourceModule ?: throw IllegalStateException()) +} + +// ----------- copy-paste from Analysis API ---------------------------------------------------------------------------- +/** + * Collect source file path from the given [root] store them in [result]. + * + * E.g., for `project/app/src` as a [root], this will walk the file tree and + * collect all `.kt` and `.java` files under that folder. + * + * Note that this util gracefully skips [IOException] during file tree traversal. + */ +internal fun collectSourceFilePaths( + root: Path, + result: MutableSet<String> +) { + // NB: [Files#walk] throws an exception if there is an issue during IO. + // With [Files#walkFileTree] with a custom visitor, we can take control of exception handling. + Files.walkFileTree( + root, + object : SimpleFileVisitor<Path>() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + return if (Files.isReadable(dir)) + FileVisitResult.CONTINUE + else + FileVisitResult.SKIP_SUBTREE + } + + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + if (!Files.isRegularFile(file) || !Files.isReadable(file)) + return FileVisitResult.CONTINUE + val ext = file.toFile().extension + if (ext == KotlinFileType.EXTENSION || ext == "java"/*JavaFileType.DEFAULT_EXTENSION*/) { + result.add(file.toString()) + } + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult { + // TODO: report or log [IOException]? + // NB: this intentionally swallows the exception, hence fail-safe. + // Skipping subtree doesn't make any sense, since this is not a directory. + // Skipping sibling may drop valid file paths afterward, so we just continue. + return FileVisitResult.CONTINUE + } + } + ) +} + +/** + * Collect source file path as [String] from the given source roots in [sourceRoot]. + * + * this util collects all `.kt` and `.java` files under source roots. + */ +internal fun getSourceFilePaths( + sourceRoot: Collection<String>, + includeDirectoryRoot: Boolean = false, +): Set<String> { + val result = mutableSetOf<String>() + sourceRoot.forEach { srcRoot -> + val path = Paths.get(srcRoot) + if (Files.isDirectory(path)) { + // E.g., project/app/src + collectSourceFilePaths(path, result) + if (includeDirectoryRoot) { + result.add(srcRoot) + } + } else { + // E.g., project/app/src/some/pkg/main.kt + result.add(srcRoot) + } + } + + return result +} + +internal inline fun <reified T : PsiFileSystemItem> getPsiFilesFromPaths( + project: Project, + paths: Collection<String>, +): List<T> { + val fs = StandardFileSystems.local() + val psiManager = PsiManager.getInstance(project) + val result = mutableListOf<T>() + for (path in paths) { + val vFile = fs.findFileByPath(path) ?: continue + val psiFileSystemItem = + if (vFile.isDirectory) + psiManager.findDirectory(vFile) as? T + else + psiManager.findFile(vFile) as? T + psiFileSystemItem?.let { result.add(it) } + } + return result +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt new file mode 100644 index 00000000..71fdfadb --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt @@ -0,0 +1,121 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.plugin + +import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute +import com.intellij.psi.PsiAnnotation +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.analysis.java.BreakingAbstractionKotlinLightMethodChecker +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.KotlinInheritDocTagContentProvider +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.DescriptorKotlinDocCommentCreator +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.KotlinDocCommentParser +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentationReader +import org.jetbrains.dokka.analysis.kotlin.symbols.services.KotlinAnalysisSourceRootsExtractor +import org.jetbrains.dokka.analysis.kotlin.symbols.services.* +import org.jetbrains.dokka.analysis.kotlin.symbols.services.KotlinDocumentableSourceLanguageParser +import org.jetbrains.dokka.analysis.kotlin.symbols.services.SymbolExternalDocumentablesProvider +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.DefaultSymbolToDocumentableTranslator +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.renderers.PostAction +import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation + +@Suppress("unused") +public class SymbolsAnalysisPlugin : DokkaPlugin() { + + internal val kotlinAnalysis by extensionPoint<KotlinAnalysis>() + + internal val defaultKotlinAnalysis by extending { + kotlinAnalysis providing { ctx -> + ProjectKotlinAnalysis( + sourceSets = ctx.configuration.sourceSets, + context = ctx + ) + } + } + + internal val disposeKotlinAnalysisPostAction by extending { + CoreExtensions.postActions with PostAction { querySingle { kotlinAnalysis }.close() } + } + + internal val symbolToDocumentableTranslator by extending { + CoreExtensions.sourceToDocumentableTranslator providing ::DefaultSymbolToDocumentableTranslator + } + + private val javaAnalysisPlugin by lazy { plugin<JavaAnalysisPlugin>() } + + internal val projectProvider by extending { + javaAnalysisPlugin.projectProvider providing { KotlinAnalysisProjectProvider() } + } + + internal val sourceRootsExtractor by extending { + javaAnalysisPlugin.sourceRootsExtractor providing { KotlinAnalysisSourceRootsExtractor() } + } + + internal val kotlinDocCommentCreator by extending { + javaAnalysisPlugin.docCommentCreators providing { + DescriptorKotlinDocCommentCreator() + } + } + + internal val kotlinDocCommentParser by extending { + javaAnalysisPlugin.docCommentParsers providing { context -> + KotlinDocCommentParser( + context + ) + } + } + internal val inheritDocTagProvider by extending { + javaAnalysisPlugin.inheritDocTagContentProviders providing ::KotlinInheritDocTagContentProvider + } + internal val kotlinLightMethodChecker by extending { + javaAnalysisPlugin.kotlinLightMethodChecker providing { + object : BreakingAbstractionKotlinLightMethodChecker { + override fun isLightAnnotation(annotation: PsiAnnotation): Boolean { + return annotation is KtLightAbstractAnnotation + } + + override fun isLightAnnotationAttribute(attribute: JvmAnnotationAttribute): Boolean { + return attribute is KtLightAbstractAnnotation + } + } + } + } + + + internal val symbolAnalyzerImpl by extending { + plugin<InternalKotlinAnalysisPlugin>().documentableSourceLanguageParser providing { KotlinDocumentableSourceLanguageParser() } + } + internal val symbolFullClassHierarchyBuilder by extending { + plugin<InternalKotlinAnalysisPlugin>().fullClassHierarchyBuilder providing { SymbolFullClassHierarchyBuilder() } + } + + internal val symbolSyntheticDocumentableDetector by extending { + plugin<InternalKotlinAnalysisPlugin>().syntheticDocumentableDetector providing { SymbolSyntheticDocumentableDetector() } + } + + internal val moduleAndPackageDocumentationReader by extending { + plugin<InternalKotlinAnalysisPlugin>().moduleAndPackageDocumentationReader providing ::ModuleAndPackageDocumentationReader + } + + /* internal val kotlinToJavaMapper by extending { + plugin<InternalKotlinAnalysisPlugin>().kotlinToJavaService providing { DescriptorKotlinToJavaMapper() } + } + + intern val descriptorInheritanceBuilder by extending { + plugin<InternalKotlinAnalysisPlugin>().inheritanceBuilder providing { DescriptorInheritanceBuilder() } + } + */ + internal val symbolExternalDocumentablesProvider by extending { + plugin<InternalKotlinAnalysisPlugin>().externalDocumentablesProvider providing ::SymbolExternalDocumentablesProvider + } + + internal val kotlinSampleProviderFactory by extending { + plugin<InternalKotlinAnalysisPlugin>().sampleProviderFactory providing ::KotlinSampleProviderFactory + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisProjectProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisProjectProvider.kt new file mode 100644 index 00000000..47bcb156 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisProjectProvider.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.openapi.project.Project +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.ProjectProvider +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle + +internal class KotlinAnalysisProjectProvider : ProjectProvider { + override fun getProject(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): Project { + val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + return kotlinAnalysis[sourceSet].project + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisSourceRootsExtractor.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisSourceRootsExtractor.kt new file mode 100644 index 00000000..43aceccd --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisSourceRootsExtractor.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.SourceRootsExtractor +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File + +internal class KotlinAnalysisSourceRootsExtractor : SourceRootsExtractor { + override fun extract(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): List<File> { + return sourceSet.sourceRoots.filter { directory -> directory.isDirectory || directory.extension == "java" } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinDocumentableSourceLanguageParser.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinDocumentableSourceLanguageParser.kt new file mode 100644 index 00000000..799cd7b6 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinDocumentableSourceLanguageParser.kt @@ -0,0 +1,26 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.WithSources +import org.jetbrains.dokka.analysis.kotlin.internal.DocumentableLanguage +import org.jetbrains.dokka.analysis.kotlin.internal.DocumentableSourceLanguageParser + +internal class KotlinDocumentableSourceLanguageParser : DocumentableSourceLanguageParser { + + /** + * For members inherited from Java in Kotlin - it returns [DocumentableLanguage.KOTLIN] + */ + override fun getLanguage( + documentable: Documentable, + sourceSet: DokkaConfiguration.DokkaSourceSet, + ): DocumentableLanguage? { + val documentableSource = (documentable as? WithSources)?.sources?.get(sourceSet) ?: return null + return when (documentableSource) { + is PsiDocumentableSource -> DocumentableLanguage.JAVA + is KtPsiDocumentableSource -> DocumentableLanguage.KOTLIN + else -> error("Unknown language sources: ${documentableSource::class}") + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt new file mode 100644 index 00000000..dc61b35b --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProvider +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SamplesKotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.kotlin.analysis.api.analyze +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.psi.KtFile + +class KotlinSampleProviderFactory(val context: DokkaContext): SampleProviderFactory { + override fun build(): SampleProvider { + return KotlinSampleProvider(context) + } + +} +/** + * It's declared as open since StdLib has its own sample transformer + * with [processBody] and [processImports] + */ +@InternalDokkaApi +open class KotlinSampleProvider(val context: DokkaContext): SampleProvider { + private val kotlinAnalysis = SamplesKotlinAnalysis( + sourceSets = context.configuration.sourceSets, + context = context, + projectKotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + ) + + protected open fun processBody(psiElement: PsiElement): String { + val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() + val lines = text.split("\n") + val indent = lines.filter(String::isNotBlank).minOfOrNull { it.takeWhile(Char::isWhitespace).count() } ?: 0 + return lines.joinToString("\n") { it.drop(indent) } + } + + private fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { + is KtDeclarationWithBody -> { + when (val bodyExpression = psiElement.bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + protected open fun processImports(psiElement: PsiElement): String { + val psiFile = psiElement.containingFile + return when(val text = (psiFile as? KtFile)?.importList?.text) { + is String -> text + else -> "" + } + } + + /** + * @return [SampleProvider.SampleSnippet] or null if it has not found by [fqLink] + */ + override fun getSample(sourceSet: DokkaConfiguration.DokkaSourceSet, fqLink: String): SampleProvider.SampleSnippet? { + val analysisContext = kotlinAnalysis[sourceSet] + val psiElement = analyze(analysisContext.mainModule) { + val lastDotIndex = fqLink.lastIndexOf('.') + + val functionName = if (lastDotIndex == -1) fqLink else fqLink.substring(lastDotIndex + 1, fqLink.length) + val packageName = if (lastDotIndex == -1) "" else fqLink.substring(0, lastDotIndex) + getTopLevelCallableSymbols(FqName(packageName), Name.identifier(functionName)).firstOrNull()?.psi + } + ?: return null.also { context.logger.warn("Cannot find PsiElement corresponding to $fqLink") } + val imports = + processImports(psiElement) + val body = processBody(psiElement) + + return SampleProvider.SampleSnippet(imports, body) + } + override fun close() { + kotlinAnalysis.close() + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KtPsiDocumentableSource.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KtPsiDocumentableSource.kt new file mode 100644 index 00000000..3a819931 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KtPsiDocumentableSource.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.model.DocumentableSource +import org.jetbrains.kotlin.lexer.KtTokens + + +internal class KtPsiDocumentableSource(val psi: PsiElement?) : DocumentableSource { + override val path = psi?.containingFile?.virtualFile?.path ?: "" + + override fun computeLineNumber(): Int? { + return psi?.let { + val range = it.node?.findChildByType(KtTokens.IDENTIFIER)?.textRange ?: it.textRange + val doc = PsiDocumentManager.getInstance(it.project).getDocument(it.containingFile) + // IJ uses 0-based line-numbers; external source browsers use 1-based + doc?.getLineNumber(range.startOffset)?.plus(1) + } + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt new file mode 100644 index 00000000..c328fe57 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt @@ -0,0 +1,34 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.DokkaSymbolVisitor +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getClassIdFromDRI +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromSymbol +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DClasslike +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.symbols.KtNamedClassOrObjectSymbol +import org.jetbrains.dokka.analysis.kotlin.internal.ExternalDocumentablesProvider + +internal class SymbolExternalDocumentablesProvider(val context: DokkaContext) : ExternalDocumentablesProvider { + private val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + + override fun findClasslike(dri: DRI, sourceSet: DokkaSourceSet): DClasslike? { + val classId = getClassIdFromDRI(dri) + + val analysisContext = kotlinAnalysis[sourceSet] + return analyze(analysisContext.mainModule) { + val symbol = getClassOrObjectSymbolByClassId(classId) as? KtNamedClassOrObjectSymbol?: return@analyze null + val translator = DokkaSymbolVisitor(sourceSet, sourceSet.displayName, analysisContext, logger = context.logger) + + val parentDRI = symbol.getContainingSymbol()?.let { getDRIFromSymbol(it) } ?: /* top level */ DRI(dri.packageName) + with(translator) { + return@analyze visitNamedClassOrObjectSymbol(symbol, parentDRI) + } + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolFullClassHierarchyBuilder.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolFullClassHierarchyBuilder.kt new file mode 100644 index 00000000..d4269b11 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolFullClassHierarchyBuilder.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiClass +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.analysis.java.util.from +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromClassLike +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.types.KtType +import org.jetbrains.dokka.analysis.kotlin.internal.ClassHierarchy +import org.jetbrains.dokka.analysis.kotlin.internal.FullClassHierarchyBuilder +import org.jetbrains.dokka.analysis.kotlin.internal.Supertypes +import org.jetbrains.kotlin.psi.KtClassOrObject +import java.util.concurrent.ConcurrentHashMap + + +internal class SymbolFullClassHierarchyBuilder : FullClassHierarchyBuilder { + override suspend fun build(module: DModule): ClassHierarchy { + val map = module.sourceSets.associateWith { ConcurrentHashMap<DRI, List<DRI>>() } + module.packages.forEach { visitDocumentable(it, map) } + return map + } + + private fun KtAnalysisSession.collectSupertypesFromKtType( + driWithKType: Pair<DRI, KtType>, + supersMap: MutableMap<DRI, Supertypes> + ) { + val (dri, kotlinType) = driWithKType + val supertypes = kotlinType.getDirectSuperTypes().filterNot { it.isAny } + val supertypesDriWithKType = supertypes.mapNotNull { supertype -> + supertype.expandedClassSymbol?.let { + getDRIFromClassLike(it) to supertype + } + } + + if (supersMap[dri] == null) { + supersMap[dri] = supertypesDriWithKType.map { it.first } + supertypesDriWithKType.forEach{ collectSupertypesFromKtType(it, supersMap) } + } + } + + private fun collectSupertypesFromPsiClass( + driWithPsiClass: Pair<DRI, PsiClass>, + supersMap: MutableMap<DRI, Supertypes> + ) { + val (dri, psiClass) = driWithPsiClass + val supertypes = psiClass.superTypes.mapNotNull { it.resolve() } + .filterNot { it.qualifiedName == "java.lang.Object" } + val supertypesDriWithPsiClass = supertypes.map { DRI.from(it) to it } + + if (supersMap[dri] == null) { + supersMap[dri] = supertypesDriWithPsiClass.map { it.first } + supertypesDriWithPsiClass.forEach { collectSupertypesFromPsiClass(it, supersMap) } + } + } + + private fun visitDocumentable( + documentable: Documentable, + hierarchy: SourceSetDependent<MutableMap<DRI, List<DRI>>> + ) { + if (documentable is WithScope) { + documentable.classlikes.forEach { visitDocumentable(it, hierarchy) } + } + if (documentable is DClasslike) { + // to build a full class graph, + // using supertypes from Documentable is not enough since it keeps only one level of hierarchy + documentable.sources.forEach { (sourceSet, source) -> + if (source is KtPsiDocumentableSource) { + (source.psi as? KtClassOrObject)?.let { psi -> + analyze(psi) { + val type = psi.getNamedClassOrObjectSymbol()?.buildSelfClassType() ?: return@analyze + hierarchy[sourceSet]?.let { collectSupertypesFromKtType(documentable.dri to type, it) } + } + } + } else if (source is PsiDocumentableSource) { + val psi = source.psi as PsiClass + hierarchy[sourceSet]?.let { collectSupertypesFromPsiClass(documentable.dri to psi, it) } + } + } + } + } + +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSyntheticDocumentableDetector.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSyntheticDocumentableDetector.kt new file mode 100644 index 00000000..20d2508b --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSyntheticDocumentableDetector.kt @@ -0,0 +1,41 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.InheritedMember +import org.jetbrains.dokka.model.WithSources +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.analysis.kotlin.internal.SyntheticDocumentableDetector + +internal class SymbolSyntheticDocumentableDetector : SyntheticDocumentableDetector { + + /** + * Currently, it's used only for [org.jetbrains.dokka.base.transformers.documentables.ReportUndocumentedTransformer] + * + * For so-called fake-ovveride declarations - we have [InheritedMember] extra. + * For synthesized declaration - we do not have PSI source. + * + * @see org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin.SOURCE_MEMBER_GENERATED + */ + override fun isSynthetic(documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet): Boolean { + @Suppress("UNCHECKED_CAST") + val extra = (documentable as? WithExtraProperties<Documentable>)?.extra + val isInherited = extra?.get(InheritedMember)?.inheritedFrom?.get(sourceSet) != null + // TODO the same for JAVA? + val isSynthesized = documentable.getPsi(sourceSet) == null + return isInherited || isSynthesized + } + + private fun Documentable.getPsi(sourceSet: DokkaConfiguration.DokkaSourceSet): PsiElement? { + val documentableSource = (this as? WithSources)?.sources?.get(sourceSet) ?: return null + return when (documentableSource) { + is PsiDocumentableSource -> documentableSource.psi + is KtPsiDocumentableSource -> documentableSource.psi + else -> error("Unknown language sources: ${documentableSource::class}") + } + } + + +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/AnnotationTranslator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/AnnotationTranslator.kt new file mode 100644 index 00000000..02198518 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/AnnotationTranslator.kt @@ -0,0 +1,134 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.withEnumEntryExtra +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.ClassValue +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.annotations.* +import org.jetbrains.kotlin.analysis.api.base.KtConstantValue +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin +import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtFile + +/** + * Map [KtAnnotationApplication] to Dokka [Annotations.Annotation] + */ +internal class AnnotationTranslator { + private fun KtAnalysisSession.getFileLevelAnnotationsFrom(symbol: KtSymbol) = + if (symbol.origin != KtSymbolOrigin.SOURCE) + null + else + (symbol.psi?.containingFile as? KtFile)?.getFileSymbol()?.annotations + ?.map { toDokkaAnnotation(it) } + + private fun KtAnalysisSession.getDirectAnnotationsFrom(annotated: KtAnnotated) = + annotated.annotations.map { toDokkaAnnotation(it) } + + /** + * @return direct annotations and file-level annotations + */ + fun KtAnalysisSession.getAllAnnotationsFrom(annotated: KtAnnotated): List<Annotations.Annotation> { + val directAnnotations = getDirectAnnotationsFrom(annotated) + val fileLevelAnnotations = (annotated as? KtSymbol)?.let { getFileLevelAnnotationsFrom(it) } ?: emptyList() + return directAnnotations + fileLevelAnnotations + } + + private fun KtAnnotationApplication.isNoExistedInSource() = psi == null + private fun AnnotationUseSiteTarget.toDokkaAnnotationScope(): Annotations.AnnotationScope = when (this) { + AnnotationUseSiteTarget.PROPERTY_GETTER -> Annotations.AnnotationScope.DIRECT // due to compatibility with Dokka K1 + AnnotationUseSiteTarget.PROPERTY_SETTER -> Annotations.AnnotationScope.DIRECT // due to compatibility with Dokka K1 + AnnotationUseSiteTarget.FILE -> Annotations.AnnotationScope.FILE + else -> Annotations.AnnotationScope.DIRECT + } + + private fun KtAnalysisSession.mustBeDocumented(annotationApplication: KtAnnotationApplication): Boolean { + if (annotationApplication.isNoExistedInSource()) return false + val annotationClass = getClassOrObjectSymbolByClassId(annotationApplication.classId ?: return false) + return annotationClass?.hasAnnotation(mustBeDocumentedAnnotation) + ?: false + } + + private fun KtAnalysisSession.toDokkaAnnotation(annotationApplication: KtAnnotationApplication) = + Annotations.Annotation( + dri = annotationApplication.classId?.createDRI() + ?: DRI(packageName = "", classNames = ERROR_CLASS_NAME), // classId might be null on a non-existing annotation call, + params = if (annotationApplication is KtAnnotationApplicationWithArgumentsInfo) annotationApplication.arguments.associate { + it.name.asString() to toDokkaAnnotationValue( + it.expression + ) + } else emptyMap(), + mustBeDocumented = mustBeDocumented(annotationApplication), + scope = annotationApplication.useSiteTarget?.toDokkaAnnotationScope() ?: Annotations.AnnotationScope.DIRECT + ) + + @OptIn(ExperimentalUnsignedTypes::class) + private fun KtAnalysisSession.toDokkaAnnotationValue(annotationValue: KtAnnotationValue): AnnotationParameterValue = + when (annotationValue) { + is KtConstantAnnotationValue -> { + when (val value = annotationValue.constantValue) { + is KtConstantValue.KtNullConstantValue -> NullValue + is KtConstantValue.KtFloatConstantValue -> FloatValue(value.value) + is KtConstantValue.KtDoubleConstantValue -> DoubleValue(value.value) + is KtConstantValue.KtLongConstantValue -> LongValue(value.value) + is KtConstantValue.KtIntConstantValue -> IntValue(value.value) + is KtConstantValue.KtBooleanConstantValue -> BooleanValue(value.value) + is KtConstantValue.KtByteConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtCharConstantValue -> StringValue(value.value.toString()) + is KtConstantValue.KtErrorConstantValue -> StringValue(value.renderAsKotlinConstant()) + is KtConstantValue.KtShortConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtStringConstantValue -> StringValue(value.value) + is KtConstantValue.KtUnsignedByteConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtUnsignedIntConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtUnsignedLongConstantValue -> LongValue(value.value.toLong()) + is KtConstantValue.KtUnsignedShortConstantValue -> IntValue(value.value.toInt()) + } + } + + is KtEnumEntryAnnotationValue -> EnumValue( + with(annotationValue.callableId) { this?.className?.asString() + "." + this?.callableName?.asString() }, + getDRIFrom(annotationValue) + ) + + is KtArrayAnnotationValue -> ArrayValue(annotationValue.values.map { toDokkaAnnotationValue(it) }) + is KtAnnotationApplicationValue -> AnnotationValue(toDokkaAnnotation(annotationValue.annotationValue)) + is KtKClassAnnotationValue.KtNonLocalKClassAnnotationValue -> ClassValue( + annotationValue.classId.relativeClassName.asString(), + annotationValue.classId.createDRI() + ) + + is KtKClassAnnotationValue.KtLocalKClassAnnotationValue -> throw IllegalStateException("Unexpected a local class in annotation") + is KtKClassAnnotationValue.KtErrorClassAnnotationValue -> ClassValue( + annotationValue.unresolvedQualifierName ?: "", + DRI(packageName = "", classNames = ERROR_CLASS_NAME) + ) + + KtUnsupportedAnnotationValue -> TODO() + } + + private fun getDRIFrom(enumEntry: KtEnumEntryAnnotationValue): DRI { + val callableId = + enumEntry.callableId ?: throw IllegalStateException("Can't get `callableId` for enum entry from annotation") + return DRI( + packageName = callableId.packageName.asString(), + classNames = callableId.className?.asString() + "." + callableId.callableName.asString(), + ).withEnumEntryExtra() + } + + companion object { + val mustBeDocumentedAnnotation = ClassId(FqName("kotlin.annotation"), FqName("MustBeDocumented"), false) + private val parameterNameAnnotation = ClassId(FqName("kotlin"), FqName("ParameterName"), false) + + /** + * Functional types can have **generated** [ParameterName] annotation + */ + internal fun KtAnnotated.getPresentableName(): String? = + this.annotationsByClassId(parameterNameAnnotation) + .firstOrNull()?.arguments?.firstOrNull { it.name == Name.identifier("name") }?.expression?.let { it as? KtConstantAnnotationValue } + ?.let { it.constantValue.value.toString() } + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DRIFactory.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DRIFactory.kt new file mode 100644 index 00000000..ed5ed532 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DRIFactory.kt @@ -0,0 +1,140 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.links.* +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolKind +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithKind +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithTypeParameters +import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +internal fun ClassId.createDRI(): DRI = DRI( + packageName = this.packageFqName.asString(), classNames = this.relativeClassName.asString() +) + +private fun CallableId.createDRI(receiver: TypeReference?, params: List<TypeReference>): DRI = DRI( + packageName = this.packageName.asString(), + classNames = this.className?.asString(), + callable = Callable( + this.callableName.asString(), + params = params, + receiver = receiver + ) +) + +internal fun getDRIFromNonErrorClassType(nonErrorClassType: KtNonErrorClassType): DRI = + nonErrorClassType.classId.createDRI() + +private val KtCallableSymbol.callableId + get() = this.callableIdIfNonLocal ?: throw IllegalStateException("Can not get callable Id due to it is local") + +// because of compatibility with Dokka K1, DRI of entry is kept as non-callable +internal fun getDRIFromEnumEntry(symbol: KtEnumEntrySymbol): DRI = + symbol.callableId.let { + DRI( + packageName = it.packageName.asString(), + classNames = it.className?.asString() + "." + it.callableName.asString(), + ) + }.withEnumEntryExtra() + + +internal fun KtAnalysisSession.getDRIFromTypeParameter(symbol: KtTypeParameterSymbol): DRI { + val containingSymbol = + (symbol.getContainingSymbol() as? KtSymbolWithTypeParameters) + ?: throw IllegalStateException("Containing symbol is null for type parameter") + val typeParameters = containingSymbol.typeParameters + val index = typeParameters.indexOfFirst { symbol.name == it.name } + return getDRIFromSymbol(containingSymbol).copy(target = PointingToGenericParameters(index)) +} + +internal fun KtAnalysisSession.getDRIFromConstructor(symbol: KtConstructorSymbol): DRI = + (symbol.containingClassIdIfNonLocal + ?: throw IllegalStateException("Can not get class Id due to it is local")).createDRI().copy( + callable = Callable( + name = symbol.containingClassIdIfNonLocal?.relativeClassName?.asString() ?: "", + params = symbol.valueParameters.map { getTypeReferenceFrom(it.returnType) }) + ) + +internal fun KtAnalysisSession.getDRIFromVariableLike(symbol: KtVariableLikeSymbol): DRI { + val receiver = symbol.receiverType?.let { + getTypeReferenceFrom(it) + } + return symbol.callableId.createDRI(receiver, emptyList()) +} + +internal fun KtAnalysisSession.getDRIFromFunctionLike(symbol: KtFunctionLikeSymbol): DRI { + val params = symbol.valueParameters.map { getTypeReferenceFrom(it.returnType) } + val receiver = symbol.receiverType?.let { + getTypeReferenceFrom(it) + } + return symbol.callableIdIfNonLocal?.createDRI(receiver, params) + ?: getDRIFromLocalFunction(symbol) +} + +internal fun getDRIFromClassLike(symbol: KtClassLikeSymbol): DRI = + symbol.classIdIfNonLocal?.createDRI() ?: throw IllegalStateException("Can not get class Id due to it is local") + +internal fun getDRIFromPackage(symbol: KtPackageSymbol): DRI = + DRI(packageName = symbol.fqName.asString()) + +internal fun KtAnalysisSession.getDRIFromValueParameter(symbol: KtValueParameterSymbol): DRI { + val function = (symbol.getContainingSymbol() as? KtFunctionLikeSymbol) + ?: throw IllegalStateException("Containing symbol is null for type parameter") + val index = function.valueParameters.indexOfFirst { it.name == symbol.name } + val funDRI = getDRIFromFunctionLike(function) + return funDRI.copy(target = PointingToCallableParameters(index)) +} + +internal fun KtAnalysisSession.getDRIFromSymbol(symbol: KtSymbol): DRI = + when (symbol) { + is KtEnumEntrySymbol -> getDRIFromEnumEntry(symbol) + is KtTypeParameterSymbol -> getDRIFromTypeParameter(symbol) + is KtConstructorSymbol -> getDRIFromConstructor(symbol) + is KtValueParameterSymbol -> getDRIFromValueParameter(symbol) + is KtVariableLikeSymbol -> getDRIFromVariableLike(symbol) + is KtFunctionLikeSymbol -> getDRIFromFunctionLike(symbol) + is KtClassLikeSymbol -> getDRIFromClassLike(symbol) + is KtPackageSymbol -> getDRIFromPackage(symbol) + else -> throw IllegalStateException("Unknown symbol while creating DRI ") + } + +private fun KtAnalysisSession.getDRIFromNonCallablePossibleLocalSymbol(symbol: KtSymbol): DRI { + if ((symbol as? KtSymbolWithKind)?.symbolKind == KtSymbolKind.LOCAL) { + return symbol.getContainingSymbol()?.let { getDRIFromNonCallablePossibleLocalSymbol(it) } + ?: throw IllegalStateException("Can't get containing symbol for local symbol") + } + return getDRIFromSymbol(symbol) +} + +/** + * Currently, it's used only for functions from enum entry, + * For its members: `memberSymbol.callableIdIfNonLocal=null` + */ +private fun KtAnalysisSession.getDRIFromLocalFunction(symbol: KtFunctionLikeSymbol): DRI { + /** + * A function is inside local object + */ + val containingSymbolDRI = symbol.getContainingSymbol()?.let { getDRIFromNonCallablePossibleLocalSymbol(it) } + ?: throw IllegalStateException("Can't get containing symbol for local function") + return containingSymbolDRI.copy( + callable = Callable( + (symbol as? KtNamedSymbol)?.name?.asString() ?: "", + params = symbol.valueParameters.map { getTypeReferenceFrom(it.returnType) }, + receiver = symbol.receiverType?.let { + getTypeReferenceFrom(it) + } + ) + ) +} + +// ----------- DRI => compiler identifiers ---------------------------------------------------------------------------- +internal fun getClassIdFromDRI(dri: DRI) = ClassId( + FqName(dri.packageName ?: ""), + FqName(dri.classNames ?: throw IllegalStateException("DRI must have `classNames` to get ClassID")), + false +) + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt new file mode 100644 index 00000000..0c79b8a0 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt @@ -0,0 +1,949 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + + +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.* +import com.intellij.psi.util.PsiLiteralUtil +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.java.parsers.JavadocParser +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.getGeneratedKDocDocumentationFrom +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.AnalysisContext +import org.jetbrains.dokka.analysis.kotlin.symbols.services.KtPsiDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.getJavaDocDocumentationFrom +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.getKDocDocumentationFrom +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.hasGeneratedKDocDocumentation +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.AnnotationTranslator.Companion.getPresentableName +import org.jetbrains.dokka.analysis.kotlin.symbols.utils.typeConstructorsBeingExceptions +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.Visibility +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.sources.AsyncSourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.analysis.api.* +import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotated +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithModality +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithVisibility +import org.jetbrains.kotlin.analysis.api.types.* +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.descriptors.java.JavaVisibilities +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier +import org.jetbrains.kotlin.psi.psiUtil.hasExpectModifier +import java.nio.file.Paths + +internal class DefaultSymbolToDocumentableTranslator(context: DokkaContext) : AsyncSourceToDocumentableTranslator { + private val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + private val javadocParser = JavadocParser( + docCommentParsers = context.plugin<JavaAnalysisPlugin>().query { docCommentParsers }, + docCommentFinder = context.plugin<JavaAnalysisPlugin>().docCommentFinder + ) + + override suspend fun invokeSuspending( + sourceSet: DokkaConfiguration.DokkaSourceSet, + context: DokkaContext + ): DModule { + val analysisContext = kotlinAnalysis[sourceSet] + @Suppress("unused") + return DokkaSymbolVisitor( + sourceSet = sourceSet, + moduleName = context.configuration.moduleName, + analysisContext = analysisContext, + logger = context.logger, + javadocParser = javadocParser + ).visitModule() + } +} + +internal fun <T : Bound> T.wrapWithVariance(variance: org.jetbrains.kotlin.types.Variance) = + when (variance) { + org.jetbrains.kotlin.types.Variance.INVARIANT -> Invariance(this) + org.jetbrains.kotlin.types.Variance.IN_VARIANCE -> Contravariance(this) + org.jetbrains.kotlin.types.Variance.OUT_VARIANCE -> Covariance(this) + } + +/** + * Maps [KtSymbol] to Documentable model [Documentable] + */ +internal class DokkaSymbolVisitor( + private val sourceSet: DokkaConfiguration.DokkaSourceSet, + private val moduleName: String, + private val analysisContext: AnalysisContext, + private val logger: DokkaLogger, + private val javadocParser: JavadocParser? = null +) { + private var annotationTranslator = AnnotationTranslator() + private var typeTranslator = TypeTranslator(sourceSet, annotationTranslator) + + /** + * To avoid recursive classes + * e.g. + * open class Klass() { + * object Default : Klass() + * } + */ + private val visitedNamedClassOrObjectSymbol: MutableSet<ClassId> = + mutableSetOf() + + private fun <T> T.toSourceSetDependent() = if (this != null) mapOf(sourceSet to this) else emptyMap() + + // KT-54846 will replace + private val KtDeclarationSymbol.isActual + get() = (psi as? KtModifierListOwner)?.hasActualModifier() == true + private val KtDeclarationSymbol.isExpect + get() = (psi as? KtModifierListOwner)?.hasExpectModifier() == true + + private fun <T : KtSymbol> Collection<T>.filterSymbolsInSourceSet() = filter { + it.psi?.containingFile?.virtualFile?.path?.let { path -> + path.isNotBlank() && sourceSet.sourceRoots.any { root -> + Paths.get(path).startsWith(root.toPath()) + } + } == true + } + + fun visitModule(): DModule { + val ktFiles: List<KtFile> = getPsiFilesFromPaths( + analysisContext.project, + getSourceFilePaths(sourceSet.sourceRoots.map { it.canonicalPath }) + ) + val processedPackages: MutableSet<FqName> = mutableSetOf() + return analyze(analysisContext.mainModule) { + val packageSymbols: List<DPackage> = ktFiles + .mapNotNull { + if (processedPackages.contains(it.packageFqName)) + return@mapNotNull null + processedPackages.add(it.packageFqName) + getPackageSymbolIfPackageExists(it.packageFqName)?.let { it1 -> + visitPackageSymbol( + it1 + ) + } + } + + DModule( + name = moduleName, + packages = packageSymbols, + documentation = emptyMap(), + expectPresentInSet = null, + sourceSets = setOf(sourceSet) + ) + } + } + + private fun KtAnalysisSession.visitPackageSymbol(packageSymbol: KtPackageSymbol): DPackage { + val dri = getDRIFromPackage(packageSymbol) + val scope = packageSymbol.getPackageScope() + val callables = scope.getCallableSymbols().toList().filterSymbolsInSourceSet() + val classifiers = scope.getClassifierSymbols().toList().filterSymbolsInSourceSet() + + val functions = callables.filterIsInstance<KtFunctionSymbol>().map { visitFunctionSymbol(it, dri) } + val properties = callables.filterIsInstance<KtPropertySymbol>().map { visitPropertySymbol(it, dri) } + val classlikes = + classifiers.filterIsInstance<KtNamedClassOrObjectSymbol>() + .map { visitNamedClassOrObjectSymbol(it, dri) } + val typealiases = classifiers.filterIsInstance<KtTypeAliasSymbol>().map { visitTypeAliasSymbol(it, dri) } + + return DPackage( + dri = dri, + functions = functions, + properties = properties, + classlikes = classlikes, + typealiases = typealiases, + documentation = emptyMap(), + sourceSets = setOf(sourceSet) + ) + } + + private fun KtAnalysisSession.visitTypeAliasSymbol( + typeAliasSymbol: KtTypeAliasSymbol, + parent: DRI + ): DTypeAlias = withExceptionCatcher(typeAliasSymbol) { + val name = typeAliasSymbol.name.asString() + val dri = parent.withClass(name) + + val ancestryInfo = with(typeTranslator) { buildAncestryInformationFrom(typeAliasSymbol.expandedType) } + + val generics = + typeAliasSymbol.typeParameters.mapIndexed { index, symbol -> visitVariantTypeParameter(index, symbol, dri) } + + return DTypeAlias( + dri = dri, + name = name, + type = GenericTypeConstructor( + dri = dri, + projections = generics.map { it.variantTypeParameter }), // this property can be removed in DTypeAlias + expectPresentInSet = null, + underlyingType = toBoundFrom(typeAliasSymbol.expandedType).toSourceSetDependent(), + visibility = typeAliasSymbol.getDokkaVisibility().toSourceSetDependent(), + documentation = getDocumentation(typeAliasSymbol)?.toSourceSetDependent() ?: emptyMap(), + sourceSets = setOf(sourceSet), + generics = generics, + sources = typeAliasSymbol.getSource(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(typeAliasSymbol)?.toSourceSetDependent()?.toAnnotations(), + ancestryInfo.exceptionInSupertypesOrNull(), + ) + ) + } + + fun KtAnalysisSession.visitNamedClassOrObjectSymbol( + namedClassOrObjectSymbol: KtNamedClassOrObjectSymbol, + parent: DRI + ): DClasslike = withExceptionCatcher(namedClassOrObjectSymbol) { + namedClassOrObjectSymbol.classIdIfNonLocal?.let { visitedNamedClassOrObjectSymbol.add(it) } + + val name = namedClassOrObjectSymbol.name.asString() + val dri = parent.withClass(name) + + val isExpect = namedClassOrObjectSymbol.isExpect + val isActual = namedClassOrObjectSymbol.isActual + val documentation = getDocumentation(namedClassOrObjectSymbol)?.toSourceSetDependent() ?: emptyMap() + + val (constructors, functions, properties, classlikes) = getDokkaScopeFrom(namedClassOrObjectSymbol, dri) + + val generics = namedClassOrObjectSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + val ancestryInfo = + with(typeTranslator) { buildAncestryInformationFrom(namedClassOrObjectSymbol.buildSelfClassType()) } + val supertypes = + //(ancestryInfo.interfaces.map{ it.typeConstructor } + listOfNotNull(ancestryInfo.superclass?.typeConstructor)) + namedClassOrObjectSymbol.superTypes.filterNot { it.isAny } + .map { with(typeTranslator) { toTypeConstructorWithKindFrom(it) } } + .toSourceSetDependent() + + return@withExceptionCatcher when (namedClassOrObjectSymbol.classKind) { + KtClassKind.OBJECT, KtClassKind.COMPANION_OBJECT -> + DObject( + dri = dri, + name = name, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + documentation = documentation, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent() + ?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()), + ancestryInfo.exceptionInSupertypesOrNull() + ) + ) + + KtClassKind.CLASS -> DClass( + dri = dri, + name = name, + constructors = constructors, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + generics = generics, + documentation = documentation, + modifier = namedClassOrObjectSymbol.getDokkaModality().toSourceSetDependent(), + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()), + ancestryInfo.exceptionInSupertypesOrNull() + ) + ) + + KtClassKind.INTERFACE -> DInterface( + dri = dri, + name = name, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), // + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + generics = generics, + documentation = documentation, + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()), + ancestryInfo.exceptionInSupertypesOrNull() + ) + ) + + KtClassKind.ANNOTATION_CLASS -> DAnnotation( + dri = dri, + name = name, + documentation = documentation, + functions = functions, + properties = properties, + classlikes = classlikes, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + generics = generics, + constructors = constructors, + sources = namedClassOrObjectSymbol.getSource(), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ) + ) + + KtClassKind.ANONYMOUS_OBJECT -> throw NotImplementedError("ANONYMOUS_OBJECT does not support") + KtClassKind.ENUM_CLASS -> { + val entries = namedClassOrObjectSymbol.getEnumEntries().map { visitEnumEntrySymbol(it) } + + DEnum( + dri = dri, + name = name, + entries = entries, + constructors = constructors, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + documentation = documentation, + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent() + ?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()) + ) + ) + } + } + } + + private data class DokkaScope( + val constructors: List<DFunction>, + val functions: List<DFunction>, + val properties: List<DProperty>, + val classlikes: List<DClasslike> + ) + private fun KtAnalysisSession.getDokkaScopeFrom( + namedClassOrObjectSymbol: KtNamedClassOrObjectSymbol, + dri: DRI + ): DokkaScope { + // e.g. getStaticMemberScope contains `valueOf`, `values` and `entries` members for Enum + val scope = listOf(namedClassOrObjectSymbol.getMemberScope(), namedClassOrObjectSymbol.getStaticMemberScope()).asCompositeScope() + val constructors = scope.getConstructors().map { visitConstructorSymbol(it) }.toList() + + val callables = scope.getCallableSymbols().toList() + val classifiers = scope.getClassifierSymbols().toList() + + val syntheticJavaProperties = + namedClassOrObjectSymbol.buildSelfClassType().getSyntheticJavaPropertiesScope()?.getCallableSignatures() + ?.map { it.symbol } + ?.filterIsInstance<KtSyntheticJavaPropertySymbol>() ?: emptySequence() + + fun List<KtJavaFieldSymbol>.filterOutSyntheticJavaPropBackingField() = + filterNot { javaField -> syntheticJavaProperties.any { it.hasBackingField && javaField.name == it.name } } + + val javaFields = callables.filterIsInstance<KtJavaFieldSymbol>() + .filterOutSyntheticJavaPropBackingField() + + fun List<KtFunctionSymbol>.filterOutSyntheticJavaPropAccessors() = filterNot { fn -> + if (fn.origin == KtSymbolOrigin.JAVA && fn.callableIdIfNonLocal != null) + syntheticJavaProperties.any { fn.callableIdIfNonLocal == it.javaGetterSymbol.callableIdIfNonLocal || fn.callableIdIfNonLocal == it.javaSetterSymbol?.callableIdIfNonLocal } + else false + } + + val functions = callables.filterIsInstance<KtFunctionSymbol>() + .filterOutSyntheticJavaPropAccessors().map { visitFunctionSymbol(it, dri) } + + + val properties = callables.filterIsInstance<KtPropertySymbol>().map { visitPropertySymbol(it, dri) } + + syntheticJavaProperties.map { visitPropertySymbol(it, dri) } + + javaFields.map { visitJavaFieldSymbol(it, dri) } + + + // hack, by default, compiler adds an empty companion object for enum + // TODO check if it is empty + fun List<KtNamedClassOrObjectSymbol>.filterOutEnumCompanion() = + if (namedClassOrObjectSymbol.classKind == KtClassKind.ENUM_CLASS) + filterNot { + it.name.asString() == "Companion" && it.classKind == KtClassKind.COMPANION_OBJECT + } else this + + fun List<KtNamedClassOrObjectSymbol>.filterOutAndMarkAlreadyVisited() = filterNot { symbol -> + visitedNamedClassOrObjectSymbol.contains(symbol.classIdIfNonLocal) + .also { + if (!it) symbol.classIdIfNonLocal?.let { classId -> + visitedNamedClassOrObjectSymbol.add( + classId + ) + } + } + } + + val classlikes = classifiers.filterIsInstance<KtNamedClassOrObjectSymbol>() + .filterOutEnumCompanion() // hack to filter out companion for enum + .filterOutAndMarkAlreadyVisited() + .map { visitNamedClassOrObjectSymbol(it, dri) } + + return DokkaScope( + constructors = constructors, + functions = functions, + properties = properties, + classlikes = classlikes + ) + } + + private fun KtAnalysisSession.visitEnumEntrySymbol( + enumEntrySymbol: KtEnumEntrySymbol + ): DEnumEntry = withExceptionCatcher(enumEntrySymbol) { + val dri = getDRIFromEnumEntry(enumEntrySymbol) + val isExpect = false + + val scope = enumEntrySymbol.getMemberScope() + val callables = scope.getCallableSymbols().toList() + val classifiers = scope.getClassifierSymbols().toList() + + val functions = callables.filterIsInstance<KtFunctionSymbol>().map { visitFunctionSymbol(it, dri) } + val properties = callables.filterIsInstance<KtPropertySymbol>().map { visitPropertySymbol(it, dri) } + val classlikes = + classifiers.filterIsInstance<KtNamedClassOrObjectSymbol>() + .map { visitNamedClassOrObjectSymbol(it, dri) } + + return DEnumEntry( + dri = dri, + name = enumEntrySymbol.name.asString(), + documentation = getDocumentation(enumEntrySymbol)?.toSourceSetDependent() ?: emptyMap(), + functions = functions, + properties = properties, + classlikes = classlikes, + sourceSets = setOf(sourceSet), + expectPresentInSet = sourceSet.takeIf { isExpect }, + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(enumEntrySymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + } + + private fun KtAnalysisSession.visitPropertySymbol(propertySymbol: KtPropertySymbol, parent: DRI): DProperty = + withExceptionCatcher(propertySymbol) { + val dri = createDRIWithOverridden(propertySymbol).origin + val inheritedFrom = dri.getInheritedFromDRI(parent) + val isExpect = propertySymbol.isExpect + val isActual = propertySymbol.isActual + val generics = + propertySymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DProperty( + dri = dri, + name = propertySymbol.name.asString(), + receiver = propertySymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + sources = propertySymbol.getSource(), + getter = propertySymbol.getter?.let { visitPropertyAccessor(it, propertySymbol, dri) }, + setter = propertySymbol.setter?.let { visitPropertyAccessor(it, propertySymbol, dri) }, + visibility = propertySymbol.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = getDocumentation(propertySymbol)?.toSourceSetDependent() ?: emptyMap(), // TODO + modifier = propertySymbol.modality.toDokkaModifier().toSourceSetDependent(), + type = toBoundFrom(propertySymbol.returnType), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + generics = generics, + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + propertySymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(propertySymbol)?.toSourceSetDependent()?.toAnnotations(), + propertySymbol.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) }, + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + takeUnless { propertySymbol.isVal }?.let { IsVar }, + takeIf { propertySymbol.psi is KtParameter }?.let { IsAlsoParameter(listOf(sourceSet)) } + ) + ) + } + + private fun KtAnalysisSession.visitJavaFieldSymbol( + javaFieldSymbol: KtJavaFieldSymbol, + parent: DRI + ): DProperty = + withExceptionCatcher(javaFieldSymbol) { + val dri = createDRIWithOverridden(javaFieldSymbol).origin + val inheritedFrom = dri.getInheritedFromDRI(parent) + val isExpect = false + val isActual = false + val generics = + javaFieldSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DProperty( + dri = dri, + name = javaFieldSymbol.name.asString(), + receiver = javaFieldSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + sources = javaFieldSymbol.getSource(), + getter = null, + setter = null, + visibility = javaFieldSymbol.getDokkaVisibility().toSourceSetDependent(), + documentation = getDocumentation(javaFieldSymbol)?.toSourceSetDependent() ?: emptyMap(), // TODO + modifier = javaFieldSymbol.modality.toDokkaModifier().toSourceSetDependent(), + type = toBoundFrom(javaFieldSymbol.returnType), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + generics = generics, + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + javaFieldSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(javaFieldSymbol)?.toSourceSetDependent()?.toAnnotations(), + //javaFieldSymbol.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) }, + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + // non-final java property should be var + takeUnless { javaFieldSymbol.isVal }?.let { IsVar } + ) + ) + } + + private fun KtAnalysisSession.visitPropertyAccessor( + propertyAccessorSymbol: KtPropertyAccessorSymbol, + propertySymbol: KtPropertySymbol, + propertyDRI: DRI + ): DFunction = withExceptionCatcher(propertyAccessorSymbol) { + val isGetter = propertyAccessorSymbol is KtPropertyGetterSymbol + // it also covers @JvmName annotation + val name = (if (isGetter) propertySymbol.javaGetterName else propertySymbol.javaSetterName)?.asString() ?: "" + + // SyntheticJavaProperty has callableIdIfNonLocal, propertyAccessorSymbol.origin = JAVA_SYNTHETIC_PROPERTY + // For Kotlin properties callableIdIfNonLocal=null + val dri = if (propertyAccessorSymbol.callableIdIfNonLocal != null) + getDRIFromFunctionLike(propertyAccessorSymbol) + else + propertyDRI.copy( + callable = Callable(name, null, propertyAccessorSymbol.valueParameters.map { getTypeReferenceFrom(it.returnType) }) + ) + // for SyntheticJavaProperty + val inheritedFrom = if(propertyAccessorSymbol.origin == KtSymbolOrigin.JAVA_SYNTHETIC_PROPERTY) dri.copy(callable = null) else null + + val isExpect = propertyAccessorSymbol.isExpect + val isActual = propertyAccessorSymbol.isActual + + val generics = propertyAccessorSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DFunction( + dri = dri, + name = name, + isConstructor = false, + receiver = propertyAccessorSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + parameters = propertyAccessorSymbol.valueParameters.mapIndexed { index, symbol -> + visitValueParameter(index, symbol, dri) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = propertyAccessorSymbol.getSource(), + visibility = propertyAccessorSymbol.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = generics, + documentation = getDocumentation(propertyAccessorSymbol)?.toSourceSetDependent() ?: emptyMap(), + modifier = propertyAccessorSymbol.modality.toDokkaModifier().toSourceSetDependent(), + type = toBoundFrom(propertyAccessorSymbol.returnType), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + propertyAccessorSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + getDokkaAnnotationsFrom(propertyAccessorSymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + } + + private fun KtAnalysisSession.visitConstructorSymbol( + constructorSymbol: KtConstructorSymbol + ): DFunction = withExceptionCatcher(constructorSymbol) { + val name = constructorSymbol.containingClassIdIfNonLocal?.shortClassName?.asString() + ?: throw IllegalStateException("Unknown containing class of constructor") + val dri = createDRIWithOverridden(constructorSymbol).origin + val isExpect = false // TODO + val isActual = false // TODO + + val generics = constructorSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + val documentation = getDocumentation(constructorSymbol)?.let { docNode -> + if (constructorSymbol.isPrimary) { + docNode.copy(children = (docNode.children.find { it is Constructor }?.root?.let { constructor -> + listOf(Description(constructor)) + } ?: emptyList<TagWrapper>()) + docNode.children.filterIsInstance<Param>()) + } else { + docNode + } + }?.toSourceSetDependent() + + return DFunction( + dri = dri, + name = name, + isConstructor = true, + receiver = constructorSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + parameters = constructorSymbol.valueParameters.mapIndexed { index, symbol -> + visitValueParameter(index, symbol, dri) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = constructorSymbol.getSource(), + visibility = constructorSymbol.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = generics, + documentation = documentation ?: emptyMap(), + modifier = KotlinModifier.Empty.toSourceSetDependent(), + type = toBoundFrom(constructorSymbol.returnType), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(constructorSymbol)?.toSourceSetDependent()?.toAnnotations(), + takeIf { constructorSymbol.isPrimary }?.let { PrimaryConstructorExtra } + ) + ) + } + + private fun KtAnalysisSession.visitFunctionSymbol(functionSymbol: KtFunctionSymbol, parent: DRI): DFunction = + withExceptionCatcher(functionSymbol) { + val dri = createDRIWithOverridden(functionSymbol).origin + val inheritedFrom = dri.getInheritedFromDRI(parent) + val isExpect = functionSymbol.isExpect + val isActual = functionSymbol.isActual + + val generics = + functionSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DFunction( + dri = dri, + name = functionSymbol.name.asString(), + isConstructor = false, + receiver = functionSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + parameters = functionSymbol.valueParameters.mapIndexed { index, symbol -> + visitValueParameter(index, symbol, dri) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = functionSymbol.getSource(), + visibility = functionSymbol.getDokkaVisibility().toSourceSetDependent(), + generics = generics, + documentation = getDocumentation(functionSymbol)?.toSourceSetDependent() ?: emptyMap(), + modifier = functionSymbol.getDokkaModality().toSourceSetDependent(), + type = toBoundFrom(functionSymbol.returnType), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + functionSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(functionSymbol) + ?.toSourceSetDependent()?.toAnnotations(), + ObviousMember.takeIf { isObvious(functionSymbol) }, + ) + ) + } + + private fun KtAnalysisSession.visitValueParameter( + index: Int, valueParameterSymbol: KtValueParameterSymbol, dri: DRI + ) = DParameter( + dri = dri.copy(target = PointingToCallableParameters(index)), + name = valueParameterSymbol.name.asString(), + type = toBoundFrom(valueParameterSymbol.returnType), + expectPresentInSet = null, + documentation = getDocumentation(valueParameterSymbol)?.toSourceSetDependent() ?: emptyMap(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + valueParameterSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(valueParameterSymbol)?.toSourceSetDependent()?.toAnnotations(), + valueParameterSymbol.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) } + ) + ) + + private fun KtAnalysisSession.visitReceiverParameter( + receiverParameterSymbol: KtReceiverParameterSymbol, dri: DRI + ) = DParameter( + dri = dri.copy(target = PointingToDeclaration), + name = null, + type = toBoundFrom(receiverParameterSymbol.type), + expectPresentInSet = null, + documentation = getDocumentation(receiverParameterSymbol)?.toSourceSetDependent() ?: emptyMap(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(receiverParameterSymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + private fun KtValueParameterSymbol.getDefaultValue(): Expression? = + if (origin == KtSymbolOrigin.SOURCE) (psi as? KtParameter)?.defaultValue?.toDefaultValueExpression() + else null + + private fun KtPropertySymbol.getDefaultValue(): Expression? = + (initializer?.initializerPsi as? KtConstantExpression)?.toDefaultValueExpression() // TODO consider [KtConstantInitializerValue], but should we keep an original format, e.g. 0xff or 0b101? + + private fun KtExpression.toDefaultValueExpression(): Expression? = when (node?.elementType) { + KtNodeTypes.INTEGER_CONSTANT -> PsiLiteralUtil.parseLong(node?.text)?.let { IntegerConstant(it) } + KtNodeTypes.FLOAT_CONSTANT -> if (node?.text?.toLowerCase()?.endsWith('f') == true) + PsiLiteralUtil.parseFloat(node?.text)?.let { FloatConstant(it) } + else PsiLiteralUtil.parseDouble(node?.text)?.let { DoubleConstant(it) } + + KtNodeTypes.BOOLEAN_CONSTANT -> BooleanConstant(node?.text == "true") + KtNodeTypes.STRING_TEMPLATE -> StringConstant(node.findChildByType(KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY)?.text.orEmpty()) + else -> node?.text?.let { ComplexExpression(it) } + } + + private fun KtAnalysisSession.visitVariantTypeParameter( + index: Int, + typeParameterSymbol: KtTypeParameterSymbol, + dri: DRI + ): DTypeParameter { + val upperBoundsOrNullableAny = + typeParameterSymbol.upperBounds.takeIf { it.isNotEmpty() } ?: listOf(this.builtinTypes.NULLABLE_ANY) + return DTypeParameter( + variantTypeParameter = TypeParameter( + dri = dri.copy(target = PointingToGenericParameters(index)), + name = typeParameterSymbol.name.asString(), + presentableName = typeParameterSymbol.getPresentableName() + ).wrapWithVariance(typeParameterSymbol.variance), + documentation = getDocumentation(typeParameterSymbol)?.toSourceSetDependent() ?: emptyMap(), + expectPresentInSet = null, + bounds = upperBoundsOrNullableAny.map { toBoundFrom(it) }, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(typeParameterSymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + } + // ----------- Utils ---------------------------------------------------------------------------- + + private fun KtAnalysisSession.getDokkaAnnotationsFrom(annotated: KtAnnotated): List<Annotations.Annotation>? = + with(annotationTranslator) { getAllAnnotationsFrom(annotated) }.takeUnless { it.isEmpty() } + + private fun KtAnalysisSession.toBoundFrom(type: KtType) = + with(typeTranslator) { toBoundFrom(type) } + + /** + * `createDRI` returns the DRI of the exact element and potential DRI of an element that is overriding it + * (It can be also FAKE_OVERRIDE which is in fact just inheritance of the symbol) + * + * Looking at what PSIs do, they give the DRI of the element within the classnames where it is actually + * declared and inheritedFrom as the same DRI but truncated callable part. + * Therefore, we set callable to null and take the DRI only if it is indeed coming from different class. + */ + private fun DRI.getInheritedFromDRI(dri: DRI): DRI? { + return this.copy(callable = null) + .takeIf { dri.classNames != this.classNames || dri.packageName != this.packageName } + } + + data class DRIWithOverridden(val origin: DRI, val overridden: DRI? = null) + + private fun KtAnalysisSession.createDRIWithOverridden( + callableSymbol: KtCallableSymbol, + wasOverriddenBy: DRI? = null + ): DRIWithOverridden { + if (callableSymbol is KtPropertySymbol && callableSymbol.isOverride + || callableSymbol is KtFunctionSymbol && callableSymbol.isOverride + ) { + return DRIWithOverridden(getDRIFromSymbol(callableSymbol), wasOverriddenBy) + } + + val overriddenSymbols = callableSymbol.getAllOverriddenSymbols() + // For already + return if (overriddenSymbols.isEmpty()) { + DRIWithOverridden(getDRIFromSymbol(callableSymbol), wasOverriddenBy) + } else { + createDRIWithOverridden(overriddenSymbols.first()) + } + } + + private fun KtAnalysisSession.getDocumentation(symbol: KtSymbol) = + if (symbol.origin == KtSymbolOrigin.SOURCE_MEMBER_GENERATED) + getGeneratedKDocDocumentationFrom(symbol) + else + getKDocDocumentationFrom(symbol, logger) ?: javadocParser?.let { getJavaDocDocumentationFrom(symbol, it) } + + private fun KtAnalysisSession.isObvious(functionSymbol: KtFunctionSymbol): Boolean { + return functionSymbol.origin == KtSymbolOrigin.SOURCE_MEMBER_GENERATED && !hasGeneratedKDocDocumentation(functionSymbol) || + !functionSymbol.isOverride && functionSymbol.callableIdIfNonLocal?.classId?.isObvious() == true + } + + private fun ClassId.isObvious(): Boolean = with(asString()) { + return this == "kotlin/Any" || this == "kotlin/Enum" + || this == "java.lang/Object" || this == "java.lang/Enum" + } + + private fun KtSymbol.getSource() = KtPsiDocumentableSource(psi).toSourceSetDependent() + + private fun AncestryNode.exceptionInSupertypesOrNull(): ExceptionInSupertypes? = + typeConstructorsBeingExceptions().takeIf { it.isNotEmpty() } + ?.let { ExceptionInSupertypes(it.toSourceSetDependent()) } + + + // ----------- Translators of modifiers ---------------------------------------------------------------------------- + private fun KtSymbolWithModality.getDokkaModality() = modality.toDokkaModifier() + private fun KtSymbolWithVisibility.getDokkaVisibility() = visibility.toDokkaVisibility() + private fun KtValueParameterSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.NoInline.takeIf { isNoinline }, + ExtraModifiers.KotlinOnlyModifiers.CrossInline.takeIf { isCrossinline }, + ExtraModifiers.KotlinOnlyModifiers.VarArg.takeIf { isVararg } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtPropertyAccessorSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, +//ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { isOverride } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtPropertySymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { (this as? KtKotlinPropertySymbol)?.isConst == true }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { (this as? KtKotlinPropertySymbol)?.isLateInit == true }, + //ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + //ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + //ExtraModifiers.KotlinOnlyModifiers.Static.takeIf { isStatic }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { isOverride } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtJavaFieldSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isStatic } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtFunctionSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Infix.takeIf { isInfix }, + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, + ExtraModifiers.KotlinOnlyModifiers.Suspend.takeIf { isSuspend }, + ExtraModifiers.KotlinOnlyModifiers.Operator.takeIf { isOperator }, +//ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.TailRec.takeIf { (psi as? KtNamedFunction)?.hasModifier(KtTokens.TAILREC_KEYWORD) == true }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { isOverride } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtNamedClassOrObjectSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { (this.psi as? KtClass)?.isInline() == true }, + ExtraModifiers.KotlinOnlyModifiers.Value.takeIf { (this.psi as? KtClass)?.isValue() == true }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Inner.takeIf { isInner }, + ExtraModifiers.KotlinOnlyModifiers.Data.takeIf { isData }, + ExtraModifiers.KotlinOnlyModifiers.Fun.takeIf { isFun }, + ).toSet().takeUnless { it.isEmpty() } + + private fun Modality.toDokkaModifier() = when (this) { + Modality.FINAL -> KotlinModifier.Final + Modality.SEALED -> KotlinModifier.Sealed + Modality.OPEN -> KotlinModifier.Open + Modality.ABSTRACT -> KotlinModifier.Abstract + } + + + private fun org.jetbrains.kotlin.descriptors.Visibility.toDokkaVisibility(): Visibility = when (this) { + Visibilities.Public -> KotlinVisibility.Public + Visibilities.Protected -> KotlinVisibility.Protected + Visibilities.Internal -> KotlinVisibility.Internal + Visibilities.Private, Visibilities.PrivateToThis -> KotlinVisibility.Private + JavaVisibilities.ProtectedAndPackage -> KotlinVisibility.Protected + JavaVisibilities.ProtectedStaticVisibility -> KotlinVisibility.Protected + JavaVisibilities.PackageVisibility -> JavaVisibility.Default + else -> KotlinVisibility.Public + } +} + + + + + + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TranslatorError.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TranslatorError.kt new file mode 100644 index 00000000..8a8e2261 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TranslatorError.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol + +internal class TranslatorError(message: String, cause: Throwable?) : IllegalStateException(message, cause) + +internal inline fun <R> KtAnalysisSession.withExceptionCatcher(symbol: KtSymbol, action: KtAnalysisSession.() -> R): R = + try { + action() + } catch (e: TranslatorError) { + throw e + } catch (e: Throwable) { + val file = try { + symbol.psi?.containingFile?.virtualFile?.path + } catch (e: Throwable) { + "[$e]" + } + val textRange = try { + symbol.psi?.textRange.toString() + } catch (e: Throwable) { + "[$e]" + } + throw TranslatorError( + "Error in translating of symbol (${(symbol as? KtNamedSymbol)?.name}) $symbol in file: $file, $textRange", + e + ) + }
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeReferenceFactory.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeReferenceFactory.kt new file mode 100644 index 00000000..5acfeb9e --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeReferenceFactory.kt @@ -0,0 +1,78 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.links.* +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.KtStarTypeProjection +import org.jetbrains.kotlin.analysis.api.KtTypeArgumentWithVariance +import org.jetbrains.kotlin.analysis.api.KtTypeProjection +import org.jetbrains.kotlin.analysis.api.types.* + +internal fun KtAnalysisSession.getTypeReferenceFrom(type: KtType): TypeReference = + getTypeReferenceFromPossiblyRecursive(type, emptyList()) + + +// see `deep recursive typebound #1342` test +private fun KtAnalysisSession.getTypeReferenceFromPossiblyRecursive( + type: KtType, + paramTrace: List<KtType> +): TypeReference { + if (type is KtTypeParameterType) { + // compare by symbol since, e.g. T? and T have the different KtType, but the same type parameter + paramTrace.indexOfFirst { it is KtTypeParameterType && type.symbol == it.symbol } + .takeIf { it >= 0 } + ?.let { return RecursiveType(it) } + } + + return when (type) { + is KtNonErrorClassType -> TypeConstructor( + type.classId.asFqNameString(), + type.ownTypeArguments.map { + getTypeReferenceFromTypeProjection( + it, + paramTrace + ) + } + ) + + is KtTypeParameterType -> { + val upperBoundsOrNullableAny = + type.symbol.upperBounds.takeIf { it.isNotEmpty() } ?: listOf(this.builtinTypes.NULLABLE_ANY) + + TypeParam(bounds = upperBoundsOrNullableAny.map { + getTypeReferenceFromPossiblyRecursive( + it, + paramTrace + type + ) + }) + } + + is KtClassErrorType -> TypeConstructor("$ERROR_CLASS_NAME $type", emptyList()) + is KtFlexibleType -> getTypeReferenceFromPossiblyRecursive( + type.upperBound, + paramTrace + ) + + is KtDefinitelyNotNullType -> getTypeReferenceFromPossiblyRecursive( + type.original, + paramTrace + ) + + is KtDynamicType -> TypeConstructor("[dynamic]", emptyList()) + is KtTypeErrorType -> TypeConstructor("$ERROR_CLASS_NAME $type", emptyList()) + is KtCapturedType -> throw NotImplementedError() + is KtIntegerLiteralType -> throw NotImplementedError() + is KtIntersectionType -> throw NotImplementedError() + }.let { + if (type.isMarkedNullable) Nullable(it) else it + } + +} + +private fun KtAnalysisSession.getTypeReferenceFromTypeProjection( + typeProjection: KtTypeProjection, + paramTrace: List<KtType> +): TypeReference = + when (typeProjection) { + is KtStarTypeProjection -> StarProjection + is KtTypeArgumentWithVariance -> getTypeReferenceFromPossiblyRecursive(typeProjection.type, paramTrace) + } diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeTranslator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeTranslator.kt new file mode 100644 index 00000000..2b79498d --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeTranslator.kt @@ -0,0 +1,186 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.AnnotationTranslator.Companion.getPresentableName +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.KtStarTypeProjection +import org.jetbrains.kotlin.analysis.api.KtTypeArgumentWithVariance +import org.jetbrains.kotlin.analysis.api.KtTypeProjection +import org.jetbrains.kotlin.analysis.api.annotations.* +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.types.* + +internal const val ERROR_CLASS_NAME = "<ERROR CLASS>" + +/** + * Maps [KtType] to Dokka [Bound] or [TypeConstructorWithKind]. + * + * Also, build [AncestryNode] tree from [KtType] + */ +internal class TypeTranslator( + private val sourceSet: DokkaConfiguration.DokkaSourceSet, + private val annotationTranslator: AnnotationTranslator +) { + + private fun <T> T.toSourceSetDependent() = if (this != null) mapOf(sourceSet to this) else emptyMap() + + private fun KtAnalysisSession.toProjection(typeProjection: KtTypeProjection): Projection = + when (typeProjection) { + is KtStarTypeProjection -> Star + is KtTypeArgumentWithVariance -> toBoundFrom(typeProjection.type).wrapWithVariance(typeProjection.variance) + } + + private fun KtAnalysisSession.toTypeConstructorFromTypeAliased(classType: KtUsualClassType): TypeAliased { + val classSymbol = classType.classSymbol + return if (classSymbol is KtTypeAliasSymbol) + TypeAliased( + typeAlias = GenericTypeConstructor( + dri = getDRIFromNonErrorClassType(classType), + projections = classType.ownTypeArguments.map { toProjection(it) }), + inner = toBoundFrom(classSymbol.expandedType), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(classType)?.toSourceSetDependent()?.toAnnotations() + ) + ) else + throw IllegalStateException("Expected type alias symbol in type") + } + + private fun KtAnalysisSession.toTypeConstructorFrom(classType: KtUsualClassType) = + GenericTypeConstructor( + dri = getDRIFromNonErrorClassType(classType), + projections = classType.ownTypeArguments.map { toProjection(it) }, + presentableName = classType.getPresentableName(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(classType)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + private fun KtAnalysisSession.toFunctionalTypeConstructorFrom(functionalType: KtFunctionalType) = + FunctionalTypeConstructor( + dri = getDRIFromNonErrorClassType(functionalType), + projections = functionalType.ownTypeArguments.map { toProjection(it) }, + isExtensionFunction = functionalType.receiverType != null, + isSuspendable = functionalType.isSuspend, + presentableName = functionalType.getPresentableName(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(functionalType)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + fun KtAnalysisSession.toBoundFrom(type: KtType): Bound = + when (type) { + is KtUsualClassType -> { + if (type.classSymbol is KtTypeAliasSymbol) toTypeConstructorFromTypeAliased(type) + else toTypeConstructorFrom(type) + } + + is KtTypeParameterType -> TypeParameter( + dri = getDRIFromTypeParameter(type.symbol), + name = type.name.asString(), + presentableName = type.getPresentableName(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(type)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + is KtClassErrorType -> UnresolvedBound(type.toString()) + is KtFunctionalType -> toFunctionalTypeConstructorFrom(type) + is KtDynamicType -> Dynamic + is KtDefinitelyNotNullType -> DefinitelyNonNullable( + toBoundFrom(type.original) + ) + + is KtFlexibleType -> TypeAliased( + toBoundFrom(type.upperBound), + toBoundFrom(type.lowerBound), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(type)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + is KtTypeErrorType -> UnresolvedBound(type.toString()) + is KtCapturedType -> throw NotImplementedError() + is KtIntegerLiteralType -> throw NotImplementedError() + is KtIntersectionType -> throw NotImplementedError() + }.let { + if (type.isMarkedNullable) Nullable(it) else it + } + + fun KtAnalysisSession.buildAncestryInformationFrom( + type: KtType + ): AncestryNode { + val (interfaces, superclass) = type.getDirectSuperTypes().filterNot { it.isAny } + .partition { + val typeConstructorWithKind = toTypeConstructorWithKindFrom(it) + typeConstructorWithKind.kind == KotlinClassKindTypes.INTERFACE || + typeConstructorWithKind.kind == JavaClassKindTypes.INTERFACE + } + + return AncestryNode( + typeConstructor = toTypeConstructorWithKindFrom(type).typeConstructor, + superclass = superclass.map { buildAncestryInformationFrom(it) }.singleOrNull(), + interfaces = interfaces.map { buildAncestryInformationFrom(it) } + ) + } + + internal fun KtAnalysisSession.toTypeConstructorWithKindFrom(type: KtType): TypeConstructorWithKind = when (type) { + is KtUsualClassType -> + when (val classSymbol = type.classSymbol) { + is KtNamedClassOrObjectSymbol -> TypeConstructorWithKind( + toTypeConstructorFrom(type), + classSymbol.classKind.toDokkaClassKind() + ) + + is KtAnonymousObjectSymbol -> throw NotImplementedError() + is KtTypeAliasSymbol -> toTypeConstructorWithKindFrom(classSymbol.expandedType) + } + + is KtClassErrorType -> TypeConstructorWithKind( + GenericTypeConstructor( + dri = DRI(packageName = "", classNames = "$ERROR_CLASS_NAME $type"), + projections = emptyList(), + + ), + KotlinClassKindTypes.CLASS + ) + + is KtTypeErrorType -> TypeConstructorWithKind( + GenericTypeConstructor( + dri = DRI(packageName = "", classNames = "$ERROR_CLASS_NAME $type"), + projections = emptyList(), + + ), + KotlinClassKindTypes.CLASS + ) + + is KtFunctionalType -> TypeConstructorWithKind( + toFunctionalTypeConstructorFrom(type), + KotlinClassKindTypes.CLASS + ) + + is KtDefinitelyNotNullType -> toTypeConstructorWithKindFrom(type.original) + + is KtCapturedType -> throw NotImplementedError() + is KtDynamicType -> throw NotImplementedError() + is KtFlexibleType -> throw NotImplementedError() + is KtIntegerLiteralType -> throw NotImplementedError() + is KtIntersectionType -> throw NotImplementedError() + is KtTypeParameterType -> throw NotImplementedError() + } + + private fun KtAnalysisSession.getDokkaAnnotationsFrom(annotated: KtAnnotated): List<Annotations.Annotation>? = + with(annotationTranslator) { getAllAnnotationsFrom(annotated) }.takeUnless { it.isEmpty() } + + private fun KtClassKind.toDokkaClassKind() = when (this) { + KtClassKind.CLASS -> KotlinClassKindTypes.CLASS + KtClassKind.ENUM_CLASS -> KotlinClassKindTypes.ENUM_CLASS + KtClassKind.ANNOTATION_CLASS -> KotlinClassKindTypes.ANNOTATION_CLASS + KtClassKind.OBJECT -> KotlinClassKindTypes.OBJECT + KtClassKind.COMPANION_OBJECT -> KotlinClassKindTypes.OBJECT + KtClassKind.INTERFACE -> KotlinClassKindTypes.INTERFACE + KtClassKind.ANONYMOUS_OBJECT -> KotlinClassKindTypes.OBJECT + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/utils/isException.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/utils/isException.kt new file mode 100644 index 00000000..9333878a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/utils/isException.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.utils + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.AncestryNode +import org.jetbrains.dokka.model.TypeConstructor + +internal fun AncestryNode.typeConstructorsBeingExceptions(): List<TypeConstructor> { + fun traverseSupertypes(ancestry: AncestryNode): List<TypeConstructor> = + listOf(ancestry.typeConstructor) + (ancestry.superclass?.let(::traverseSupertypes) ?: emptyList()) + + return traverseSupertypes(this).filter { type -> type.dri.isDirectlyAnException() } +} + +internal fun DRI.isDirectlyAnException(): Boolean = + toString().let { stringed -> + stringed == "kotlin/Exception///PointingToDeclaration/" || + stringed == "java.lang/Exception///PointingToDeclaration/" + } |