diff options
Diffstat (limited to 'dokka-subprojects/analysis-kotlin-symbols')
41 files changed, 3541 insertions, 0 deletions
diff --git a/dokka-subprojects/analysis-kotlin-symbols/README.md b/dokka-subprojects/analysis-kotlin-symbols/README.md new file mode 100644 index 00000000..7c84ffc5 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/README.md @@ -0,0 +1,18 @@ +# Analysis: Kotlin symbols + +An internal symbols-based implementation for [analysis-kotlin-api](../analysis-kotlin-api). This implementation is +also known as K2 or "the new compiler". + +Contains no stable public API and must not be used by anyone directly, only via [analysis-kotlin-api](../analysis-kotlin-api). + +Can be added as a runtime dependency by the runner. + +## Shadowing + +The `.jar` produced by this project shadows all dependencies. There are several reasons for it: + +1. Some of the artifacts Dokka depends on, like `com.jetbrains.intellij.java:java-psi`, are not + published to Maven Central, so the users would need to add custom repositories to their build scripts. +2. There are many intertwining transitive dependencies of different versions, as well as direct copy-paste, + that can lead to runtime errors due to classpath conflicts, so it's best to let Gradle take care of + dependency resolution, and then pack everything into a single jar in a single place that can be tuned. diff --git a/dokka-subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api b/dokka-subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api new file mode 100644 index 00000000..4bddfcf1 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api @@ -0,0 +1,19 @@ +public final class org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V +} + +public class org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider : org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun close ()V + public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; + public fun getSample (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/lang/String;)Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider$SampleSnippet; + protected fun processBody (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; + protected fun processImports (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProviderFactory : org/jetbrains/dokka/analysis/kotlin/internal/SampleProviderFactory { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun build ()Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider; + public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; +} + diff --git a/dokka-subprojects/analysis-kotlin-symbols/build.gradle.kts b/dokka-subprojects/analysis-kotlin-symbols/build.gradle.kts new file mode 100644 index 00000000..4aae4628 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/build.gradle.kts @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import dokkabuild.overridePublicationArtifactId + +plugins { + id("dokkabuild.kotlin-jvm") + id("dokkabuild.publish-shadow") +} + +overridePublicationArtifactId("analysis-kotlin-symbols") + +dependencies { + compileOnly(projects.dokkaSubprojects.dokkaCore) + compileOnly(projects.dokkaSubprojects.analysisKotlinApi) + + implementation(projects.dokkaSubprojects.analysisMarkdownJb) + implementation(projects.dokkaSubprojects.analysisJavaPsi) + + + // ----------- IDE dependencies ---------------------------------------------------------------------------- + + listOf( + libs.intellij.platform.util.rt, + libs.intellij.platform.util.api, + libs.intellij.java.psi.api, + libs.intellij.java.psi.impl + ).forEach { + runtimeOnly(it) { isTransitive = false } + } + + implementation(libs.intellij.java.psi.api) { isTransitive = false } + + + // TODO move to toml + listOf( + "com.jetbrains.intellij.platform:util-class-loader", + "com.jetbrains.intellij.platform:util-text-matching", + "com.jetbrains.intellij.platform:util-base", + "com.jetbrains.intellij.platform:util-xml-dom", + "com.jetbrains.intellij.platform:core-impl", + "com.jetbrains.intellij.platform:extensions", + ).forEach { + runtimeOnly("$it:213.7172.25") { isTransitive = false } + } + + implementation("com.jetbrains.intellij.platform:core:213.7172.25") { + isTransitive = false + } // for Standalone prototype + + // ----------- Analysis dependencies ---------------------------------------------------------------------------- + + listOf( + libs.kotlin.high.level.api.api, + libs.kotlin.analysis.api.standalone, + ).forEach { + implementation(it) { + isTransitive = false // see KTIJ-19820 + } + } + listOf( + libs.kotlin.high.level.api.impl, + libs.kotlin.high.level.api.fir, + libs.kotlin.high.level.api.fe10, + libs.kotlin.low.level.api.fir, + libs.kotlin.analysis.project.structure, + libs.kotlin.analysis.api.providers, + libs.kotlin.symbol.light.classes, + ).forEach { + runtimeOnly(it) { + isTransitive = false // see KTIJ-19820 + } + } + // copy-pasted from Analysis API https://github.com/JetBrains/kotlin/blob/a10042f9099e20a656dec3ecf1665eea340a3633/analysis/low-level-api-fir/build.gradle.kts#L37 + runtimeOnly("com.github.ben-manes.caffeine:caffeine:2.9.3") + + runtimeOnly(libs.kotlinx.collections.immutable) + implementation(libs.kotlin.compiler.k2) { + isTransitive = false + } + + // TODO [beresnev] get rid of it + compileOnly(libs.kotlinx.coroutines.core) +} + +tasks.shadowJar { + // service files are merged to make sure all Dokka plugins + // from the dependencies are loaded, and not just a single one. + mergeServiceFiles() +} diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt new file mode 100644 index 00000000..d8a4e476 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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).ifUnresolved { logger.logUnresolvedLink(link.getLinkText(), kdocLocation) } }, + 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/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt new file mode 100644 index 00000000..ad29f5f0 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt new file mode 100644 index 00000000..9a0b81bd --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 + +/** + * Util to print a message about unresolved [link] + */ +internal fun DokkaLogger.logUnresolvedLink(link: String, location: String?) { + warn("Couldn't resolve link for $link" + if (location != null) " in $location" else "") +} + +internal inline fun DRI?.ifUnresolved(action: () -> Unit): DRI? = this ?: run { + action() + null +} + +/** + * Resolves KDoc link via creating PSI. + * If the [link] is ambiguous, i.e. leads to more than one declaration, + * it returns deterministically any declaration. + * + * @return [DRI] or null if the [link] is unresolved + */ +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) } +} + +/** + * If the [link] is ambiguous, i.e. leads to more than one declaration, + * it returns deterministically any declaration. + * + * @return [DRI] or null if the [link] is unresolved + */ +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 + else getDRIFromSymbol(linkedSymbol) +} diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt b/dokka-subprojects/analysis-kotlin-symbols/ |
