diff options
author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-07-05 10:04:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-05 10:04:55 +0200 |
commit | 9559158bfeeb274e9ccf1b4563f1b23b42afc493 (patch) | |
tree | 3ece0887623cfe2b7148af23001867a1dd5e6597 /subprojects | |
parent | cbd9733d3dd2f52992e98e7cebd072091a572529 (diff) | |
download | dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.tar.gz dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.tar.bz2 dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.zip |
Decompose Kotlin/Java analysis (#3034)
* Extract analysis into separate modules
Diffstat (limited to 'subprojects')
145 files changed, 9771 insertions, 0 deletions
diff --git a/subprojects/analysis-java-psi/README.md b/subprojects/analysis-java-psi/README.md new file mode 100644 index 00000000..d2bbd080 --- /dev/null +++ b/subprojects/analysis-java-psi/README.md @@ -0,0 +1,5 @@ +# Analysis: Java PSI + +An internal module for parsing Java sources. Defines no stable public API and is not published anywhere. + +Used by the Kotlin analysis artifacts to provide support for mixed-language (Kotlin+Java) projects. diff --git a/subprojects/analysis-java-psi/api/analysis-java-psi.api b/subprojects/analysis-java-psi/api/analysis-java-psi.api new file mode 100644 index 00000000..404249f8 --- /dev/null +++ b/subprojects/analysis-java-psi/api/analysis-java-psi.api @@ -0,0 +1,148 @@ +public final class org/jetbrains/dokka/analysis/java/AuthorJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public static final field INSTANCE Lorg/jetbrains/dokka/analysis/java/AuthorJavadocTag; +} + +public abstract interface class org/jetbrains/dokka/analysis/java/BreakingAbstractionKotlinLightMethodChecker { + public abstract fun isLightAnnotation (Lcom/intellij/psi/PsiAnnotation;)Z + public abstract fun isLightAnnotationAttribute (Lcom/intellij/lang/jvm/annotation/JvmAnnotationAttribute;)Z +} + +public final class org/jetbrains/dokka/analysis/java/DeprecatedJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public static final field INSTANCE Lorg/jetbrains/dokka/analysis/java/DeprecatedJavadocTag; +} + +public final class org/jetbrains/dokka/analysis/java/DescriptionJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public static final field INSTANCE Lorg/jetbrains/dokka/analysis/java/DescriptionJavadocTag; +} + +public final class org/jetbrains/dokka/analysis/java/ExceptionJavadocTag : org/jetbrains/dokka/analysis/java/ThrowingExceptionJavadocTag { + public static final field Companion Lorg/jetbrains/dokka/analysis/java/ExceptionJavadocTag$Companion; + public static final field name Ljava/lang/String; + public fun <init> (Ljava/lang/String;)V +} + +public final class org/jetbrains/dokka/analysis/java/ExceptionJavadocTag$Companion { +} + +public final class org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V + public final fun getDocCommentCreators ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getDocCommentFinder ()Lorg/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder; + public final fun getDocCommentParsers ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getInheritDocTagContentProviders ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getKotlinLightMethodChecker ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getProjectProvider ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getSourceRootsExtractor ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; +} + +public abstract class org/jetbrains/dokka/analysis/java/JavadocTag { + public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getName ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/java/ParamJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public static final field Companion Lorg/jetbrains/dokka/analysis/java/ParamJavadocTag$Companion; + public static final field name Ljava/lang/String; + public fun <init> (Lcom/intellij/psi/PsiMethod;Ljava/lang/String;I)V + public final fun getMethod ()Lcom/intellij/psi/PsiMethod; + public final fun getParamIndex ()I + public final fun getParamName ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/java/ParamJavadocTag$Companion { +} + +public abstract interface class org/jetbrains/dokka/analysis/java/ProjectProvider { + public abstract fun getProject (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Lorg/jetbrains/dokka/plugability/DokkaContext;)Lcom/intellij/openapi/project/Project; +} + +public final class org/jetbrains/dokka/analysis/java/ReturnJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public static final field INSTANCE Lorg/jetbrains/dokka/analysis/java/ReturnJavadocTag; +} + +public final class org/jetbrains/dokka/analysis/java/SeeJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public static final field Companion Lorg/jetbrains/dokka/analysis/java/SeeJavadocTag$Companion; + public static final field name Ljava/lang/String; + public fun <init> (Ljava/lang/String;)V + public final fun getQualifiedReference ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/java/SeeJavadocTag$Companion { +} + +public final class org/jetbrains/dokka/analysis/java/SinceJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public static final field INSTANCE Lorg/jetbrains/dokka/analysis/java/SinceJavadocTag; +} + +public abstract interface class org/jetbrains/dokka/analysis/java/SourceRootsExtractor { + public abstract fun extract (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Lorg/jetbrains/dokka/plugability/DokkaContext;)Ljava/util/List; +} + +public abstract class org/jetbrains/dokka/analysis/java/ThrowingExceptionJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { + public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getExceptionQualifiedName ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/java/ThrowsJavadocTag : org/jetbrains/dokka/analysis/java/ThrowingExceptionJavadocTag { + public static final field Companion Lorg/jetbrains/dokka/analysis/java/ThrowsJavadocTag$Companion; + public static final field name Ljava/lang/String; + public fun <init> (Ljava/lang/String;)V +} + +public final class org/jetbrains/dokka/analysis/java/ThrowsJavadocTag$Companion { +} + +public abstract interface class org/jetbrains/dokka/analysis/java/doccomment/DocComment { + public abstract fun hasTag (Lorg/jetbrains/dokka/analysis/java/JavadocTag;)Z + public abstract fun resolveTag (Lorg/jetbrains/dokka/analysis/java/JavadocTag;)Ljava/util/List; +} + +public abstract interface class org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator { + public abstract fun create (Lcom/intellij/psi/PsiNamedElement;)Lorg/jetbrains/dokka/analysis/java/doccomment/DocComment; +} + +public final class org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory { + public fun <init> (Ljava/util/List;)V + public final fun fromElement (Lcom/intellij/psi/PsiNamedElement;)Lorg/jetbrains/dokka/analysis/java/doccomment/DocComment; +} + +public final class org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder { + public fun <init> (Lorg/jetbrains/dokka/utilities/DokkaLogger;Lorg/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory;)V + public final fun findClosestToElement (Lcom/intellij/psi/PsiNamedElement;)Lorg/jetbrains/dokka/analysis/java/doccomment/DocComment; +} + +public abstract interface class org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent { + public abstract fun getTag ()Lorg/jetbrains/dokka/analysis/java/JavadocTag; + public abstract fun resolveSiblings ()Ljava/util/List; +} + +public abstract interface class org/jetbrains/dokka/analysis/java/parsers/DocCommentParser { + public abstract fun canParse (Lorg/jetbrains/dokka/analysis/java/doccomment/DocComment;)Z + public abstract fun parse (Lorg/jetbrains/dokka/analysis/java/doccomment/DocComment;Lcom/intellij/psi/PsiNamedElement;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; +} + +public final class org/jetbrains/dokka/analysis/java/parsers/JavadocParser : org/jetbrains/dokka/analysis/java/parsers/JavaDocumentationParser { + public fun <init> (Ljava/util/List;Lorg/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder;)V + public fun parseDocumentation (Lcom/intellij/psi/PsiNamedElement;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; +} + +public final class org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext { + public fun <init> ()V + public final fun getDocumentationNode (Ljava/lang/String;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; + public final fun getDri (Ljava/lang/String;)Lorg/jetbrains/dokka/links/DRI; + public final fun store (Lorg/jetbrains/dokka/links/DRI;)Ljava/lang/String; + public final fun store (Lorg/jetbrains/dokka/model/doc/DocumentationNode;)Ljava/lang/String; +} + +public abstract interface class org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider { + public abstract fun canConvert (Lorg/jetbrains/dokka/analysis/java/doccomment/DocumentationContent;)Z + public abstract fun convertToHtml (Lorg/jetbrains/dokka/analysis/java/doccomment/DocumentationContent;Lorg/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext;)Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/java/util/PsiDocumentableSource : org/jetbrains/dokka/model/DocumentableSource { + public fun <init> (Lcom/intellij/psi/PsiNamedElement;)V + public fun computeLineNumber ()Ljava/lang/Integer; + public fun getPath ()Ljava/lang/String; + public final fun getPsi ()Lcom/intellij/psi/PsiNamedElement; +} + diff --git a/subprojects/analysis-java-psi/build.gradle.kts b/subprojects/analysis-java-psi/build.gradle.kts new file mode 100644 index 00000000..88d043ee --- /dev/null +++ b/subprojects/analysis-java-psi/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("org.jetbrains.conventions.kotlin-jvm") +} + +dependencies { + compileOnly(projects.core) + + api(libs.intellij.java.psi.api) + + implementation(projects.subprojects.analysisMarkdownJb) + + implementation(libs.intellij.java.psi.impl) + implementation(libs.intellij.platform.util.api) + implementation(libs.intellij.platform.util.rt) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.jsoup) +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt new file mode 100644 index 00000000..72cf45d9 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt @@ -0,0 +1,83 @@ +package org.jetbrains.dokka.analysis.java + +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiKeyword +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiModifierListOwner +import kotlinx.coroutines.coroutineScope +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.java.parsers.DokkaPsiParser +import org.jetbrains.dokka.analysis.java.parsers.JavaPsiDocCommentParser +import org.jetbrains.dokka.analysis.java.parsers.JavadocParser +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.JavaVisibility +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.parallelMap +import org.jetbrains.dokka.utilities.parallelMapNotNull + +internal class DefaultPsiToDocumentableTranslator : AsyncSourceToDocumentableTranslator { + + override suspend fun invokeSuspending(sourceSet: DokkaSourceSet, context: DokkaContext): DModule { + return coroutineScope { + val projectProvider = context.plugin<JavaAnalysisPlugin>().querySingle { projectProvider } + val project = projectProvider.getProject(sourceSet, context) + + val sourceRootsExtractor = context.plugin<JavaAnalysisPlugin>().querySingle { sourceRootsExtractor } + val sourceRoots = sourceRootsExtractor.extract(sourceSet, context) + + val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file") + + val psiFiles = sourceRoots.parallelMap { sourceRoot -> + sourceRoot.absoluteFile.walkTopDown().mapNotNull { + localFileSystem.findFileByPath(it.path)?.let { vFile -> + PsiManager.getInstance(project).findFile(vFile) as? PsiJavaFile + } + }.toList() + }.flatten() + + val docParser = createPsiParser(sourceSet, context) + + DModule( + name = context.configuration.moduleName, + packages = psiFiles.parallelMapNotNull { it }.groupBy { it.packageName }.toList() + .parallelMap { (packageName: String, psiFiles: List<PsiJavaFile>) -> + docParser.parsePackage(packageName, psiFiles) + }, + documentation = emptyMap(), + expectPresentInSet = null, + sourceSets = setOf(sourceSet) + ) + } + } + + private fun createPsiParser(sourceSet: DokkaSourceSet, context: DokkaContext): DokkaPsiParser { + val projectProvider = context.plugin<JavaAnalysisPlugin>().querySingle { projectProvider } + val docCommentParsers = context.plugin<JavaAnalysisPlugin>().query { docCommentParsers } + return DokkaPsiParser( + sourceSetData = sourceSet, + project = projectProvider.getProject(sourceSet, context), + logger = context.logger, + javadocParser = JavadocParser( + docCommentParsers = docCommentParsers, + docCommentFinder = context.plugin<JavaAnalysisPlugin>().docCommentFinder + ), + javaPsiDocCommentParser = docCommentParsers.single { it is JavaPsiDocCommentParser } as JavaPsiDocCommentParser, + lightMethodChecker = context.plugin<JavaAnalysisPlugin>().querySingle { kotlinLightMethodChecker } + ) + } +} + +internal fun PsiModifierListOwner.getVisibility() = modifierList?.let { + val ml = it.children.toList() + when { + ml.any { it.text == PsiKeyword.PUBLIC } || it.hasModifierProperty("public") -> JavaVisibility.Public + ml.any { it.text == PsiKeyword.PROTECTED } || it.hasModifierProperty("protected") -> JavaVisibility.Protected + ml.any { it.text == PsiKeyword.PRIVATE } || it.hasModifierProperty("private") -> JavaVisibility.Private + else -> JavaVisibility.Default + } +} ?: JavaVisibility.Default diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt new file mode 100644 index 00000000..8884d444 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt @@ -0,0 +1,106 @@ +package org.jetbrains.dokka.analysis.java + +import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.doccomment.DocCommentCreator +import org.jetbrains.dokka.analysis.java.doccomment.DocCommentFactory +import org.jetbrains.dokka.analysis.java.doccomment.DocCommentFinder +import org.jetbrains.dokka.analysis.java.doccomment.JavaDocCommentCreator +import org.jetbrains.dokka.analysis.java.parsers.DocCommentParser +import org.jetbrains.dokka.analysis.java.parsers.doctag.InheritDocTagContentProvider +import org.jetbrains.dokka.analysis.java.parsers.JavaPsiDocCommentParser +import org.jetbrains.dokka.analysis.java.parsers.doctag.InheritDocTagResolver +import org.jetbrains.dokka.analysis.java.parsers.doctag.PsiDocTagParser +import org.jetbrains.dokka.analysis.java.util.NoopIntellijLoggerFactory +import org.jetbrains.dokka.plugability.* +import java.io.File + + +@InternalDokkaApi +interface ProjectProvider { + fun getProject(sourceSet: DokkaSourceSet, context: DokkaContext): Project +} + +@InternalDokkaApi +interface SourceRootsExtractor { + fun extract(sourceSet: DokkaSourceSet, context: DokkaContext): List<File> +} + +@InternalDokkaApi +interface BreakingAbstractionKotlinLightMethodChecker { + // TODO [beresnev] not even sure it's needed, but left for compatibility and to preserve behaviour + fun isLightAnnotation(annotation: PsiAnnotation): Boolean + fun isLightAnnotationAttribute(attribute: JvmAnnotationAttribute): Boolean +} + +@InternalDokkaApi +class JavaAnalysisPlugin : DokkaPlugin() { + + // single + val projectProvider by extensionPoint<ProjectProvider>() + + // single + val sourceRootsExtractor by extensionPoint<SourceRootsExtractor>() + + // multiple + val docCommentCreators by extensionPoint<DocCommentCreator>() + + // multiple + val docCommentParsers by extensionPoint<DocCommentParser>() + + // none or more + val inheritDocTagContentProviders by extensionPoint<InheritDocTagContentProvider>() + + // TODO [beresnev] figure out a better way depending on what it's used for + val kotlinLightMethodChecker by extensionPoint<BreakingAbstractionKotlinLightMethodChecker>() + + private val docCommentFactory by lazy { + DocCommentFactory(query { docCommentCreators }.reversed()) + } + + val docCommentFinder by lazy { + DocCommentFinder(logger, docCommentFactory) + } + + internal val javaDocCommentCreator by extending { + docCommentCreators providing { JavaDocCommentCreator() } + } + + private val psiDocTagParser by lazy { + PsiDocTagParser( + inheritDocTagResolver = InheritDocTagResolver( + docCommentFactory = docCommentFactory, + docCommentFinder = docCommentFinder, + contentProviders = query { inheritDocTagContentProviders } + ) + ) + } + + internal val javaDocCommentParser by extending { + docCommentParsers providing { + JavaPsiDocCommentParser( + psiDocTagParser + ) + } + } + + internal val psiToDocumentableTranslator by extending { + CoreExtensions.sourceToDocumentableTranslator providing { DefaultPsiToDocumentableTranslator() } + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement + + private companion object { + init { + // Suppress messages emitted by the IntelliJ logger since + // there's not much the end user can do about it + Logger.setFactory(NoopIntellijLoggerFactory()) + } + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt new file mode 100644 index 00000000..f5cd550f --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt @@ -0,0 +1,48 @@ +package org.jetbrains.dokka.analysis.java + +import com.intellij.psi.PsiMethod +import org.jetbrains.dokka.InternalDokkaApi + +@InternalDokkaApi +sealed class JavadocTag(val name: String) + +object AuthorJavadocTag : JavadocTag("author") +object DeprecatedJavadocTag : JavadocTag("deprecated") +object DescriptionJavadocTag : JavadocTag("description") +object ReturnJavadocTag : JavadocTag("return") +object SinceJavadocTag : JavadocTag("since") + +class ParamJavadocTag( + val method: PsiMethod, + val paramName: String, + val paramIndex: Int +) : JavadocTag(name) { + companion object { + const val name: String = "param" + } +} + +class SeeJavadocTag( + val qualifiedReference: String +) : JavadocTag(name) { + companion object { + const val name: String = "see" + } +} + +sealed class ThrowingExceptionJavadocTag( + name: String, + val exceptionQualifiedName: String? +) : JavadocTag(name) + +class ThrowsJavadocTag(exceptionQualifiedName: String?) : ThrowingExceptionJavadocTag(name, exceptionQualifiedName) { + companion object { + const val name: String = "throws" + } +} + +class ExceptionJavadocTag(exceptionQualifiedName: String?) : ThrowingExceptionJavadocTag(name, exceptionQualifiedName) { + companion object { + const val name: String = "exception" + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt new file mode 100644 index 00000000..d780bb40 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.analysis.java + +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.SyntheticElement +import com.intellij.psi.javadoc.PsiDocComment +import org.jetbrains.dokka.analysis.java.parsers.JavaPsiDocCommentParser +import org.jetbrains.dokka.model.doc.DocumentationNode + +private const val ENUM_VALUEOF_TEMPLATE_PATH = "/dokka/docs/javadoc/EnumValueOf.java.template" +private const val ENUM_VALUES_TEMPLATE_PATH = "/dokka/docs/javadoc/EnumValues.java.template" + +internal class SyntheticElementDocumentationProvider( + private val javadocParser: JavaPsiDocCommentParser, + private val project: Project +) { + fun isDocumented(psiElement: PsiElement): Boolean = psiElement is PsiMethod + && (psiElement.isSyntheticEnumValuesMethod() || psiElement.isSyntheticEnumValueOfMethod()) + + fun getDocumentation(psiElement: PsiElement): DocumentationNode? { + val psiMethod = psiElement as? PsiMethod ?: return null + val templatePath = when { + psiMethod.isSyntheticEnumValuesMethod() -> ENUM_VALUES_TEMPLATE_PATH + psiMethod.isSyntheticEnumValueOfMethod() -> ENUM_VALUEOF_TEMPLATE_PATH + else -> return null + } + val docComment = loadSyntheticDoc(templatePath) ?: return null + return javadocParser.parsePsiDocComment(docComment, psiElement) + } + + private fun loadSyntheticDoc(path: String): PsiDocComment? { + val text = javaClass.getResource(path)?.readText() ?: return null + return JavaPsiFacade.getElementFactory(project).createDocCommentFromText(text) + } +} + +private fun PsiMethod.isSyntheticEnumValuesMethod() = this.isSyntheticEnumFunction() && this.name == "values" +private fun PsiMethod.isSyntheticEnumValueOfMethod() = this.isSyntheticEnumFunction() && this.name == "valueOf" +private fun PsiMethod.isSyntheticEnumFunction() = this is SyntheticElement && this.containingClass?.isEnum == true + diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt new file mode 100644 index 00000000..6cc32233 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.JavadocTag + +/** + * MUST override equals and hashcode + */ +@InternalDokkaApi +interface DocComment { + fun hasTag(tag: JavadocTag): Boolean + + fun resolveTag(tag: JavadocTag): List<DocumentationContent> +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt new file mode 100644 index 00000000..3d7d4247 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.InternalDokkaApi + +@InternalDokkaApi +interface DocCommentCreator { + fun create(element: PsiNamedElement): DocComment? +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt new file mode 100644 index 00000000..96245ac2 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.InternalDokkaApi + +@InternalDokkaApi +class DocCommentFactory( + private val docCommentCreators: List<DocCommentCreator> +) { + fun fromElement(element: PsiNamedElement): DocComment? { + docCommentCreators.forEach { creator -> + val comment = creator.create(element) + if (comment != null) { + return comment + } + } + return null + } +} + diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt new file mode 100644 index 00000000..32c8dc65 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt @@ -0,0 +1,64 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.javadoc.PsiDocComment +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.util.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.utilities.DokkaLogger + +@InternalDokkaApi +class DocCommentFinder( + private val logger: DokkaLogger, + private val docCommentFactory: DocCommentFactory, +) { + fun findClosestToElement(element: PsiNamedElement): DocComment? { + val docComment = docCommentFactory.fromElement(element) + if (docComment != null) { + return docComment + } + + return if (element is PsiMethod) { + findClosestToMethod(element) + } else { + element.children + .filterIsInstance<PsiDocComment>() + .firstOrNull() + ?.let { JavaDocComment(it) } + } + } + + private fun findClosestToMethod(method: PsiMethod): DocComment? { + val superMethods = method.findSuperMethods() + if (superMethods.isEmpty()) return null + + if (superMethods.size == 1) { + return findClosestToElement(superMethods.single()) + } + + val superMethodDocumentation = superMethods.map { superMethod -> findClosestToElement(superMethod) }.distinct() + if (superMethodDocumentation.size == 1) { + return superMethodDocumentation.single() + } + + logger.debug( + "Conflicting documentation for ${DRI.from(method)}" + + "${superMethods.map { DRI.from(it) }}" + ) + + /* Prioritize super class over interface */ + val indexOfSuperClass = superMethods.indexOfFirst { superMethod -> + val parent = superMethod.parent + if (parent is PsiClass) !parent.isInterface + else false + } + + return if (indexOfSuperClass >= 0) { + superMethodDocumentation[indexOfSuperClass] + } else { + superMethodDocumentation.first() + } + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt new file mode 100644 index 00000000..c06ed496 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.JavadocTag + +@InternalDokkaApi +interface DocumentationContent { + val tag: JavadocTag + + fun resolveSiblings(): List<DocumentationContent> +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt new file mode 100644 index 00000000..5c9be887 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt @@ -0,0 +1,84 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiElement +import com.intellij.psi.javadoc.PsiDocComment +import com.intellij.psi.javadoc.PsiDocTag +import org.jetbrains.dokka.analysis.java.* +import org.jetbrains.dokka.analysis.java.util.contentElementsWithSiblingIfNeeded +import org.jetbrains.dokka.analysis.java.util.getKotlinFqName +import org.jetbrains.dokka.analysis.java.util.hasTag +import org.jetbrains.dokka.analysis.java.util.resolveToElement +import org.jetbrains.dokka.utilities.firstIsInstanceOrNull + +internal class JavaDocComment(val comment: PsiDocComment) : DocComment { + override fun hasTag(tag: JavadocTag): Boolean { + return when (tag) { + is ThrowingExceptionJavadocTag -> hasTag(tag) + else -> comment.hasTag(tag) + } + } + + private fun hasTag(tag: ThrowingExceptionJavadocTag): Boolean = + comment.hasTag(tag) && comment.resolveTag(tag).firstIsInstanceOrNull<PsiDocTag>() + ?.resolveToElement() + ?.getKotlinFqName() == tag.exceptionQualifiedName + + override fun resolveTag(tag: JavadocTag): List<DocumentationContent> { + return when (tag) { + is ParamJavadocTag -> resolveParamTag(tag) + is ThrowingExceptionJavadocTag -> resolveThrowingTag(tag) + else -> comment.resolveTag(tag).map { PsiDocumentationContent(it, tag) } + } + } + + private fun resolveParamTag(tag: ParamJavadocTag): List<DocumentationContent> { + val resolvedParamElements = comment.resolveTag(tag) + .filterIsInstance<PsiDocTag>() + .map { it.contentElementsWithSiblingIfNeeded() } + .firstOrNull { + it.firstOrNull()?.text == tag.method.parameterList.parameters[tag.paramIndex].name + }.orEmpty() + + return resolvedParamElements + .withoutReferenceLink() + .map { PsiDocumentationContent(it, tag) } + } + + private fun resolveThrowingTag(tag: ThrowingExceptionJavadocTag): List<DocumentationContent> { + val resolvedElements = comment.resolveTag(tag) + .flatMap { + when (it) { + is PsiDocTag -> it.contentElementsWithSiblingIfNeeded() + else -> listOf(it) + } + } + + return resolvedElements + .withoutReferenceLink() + .map { PsiDocumentationContent(it, tag) } + } + + private fun PsiDocComment.resolveTag(tag: JavadocTag): List<PsiElement> { + return when (tag) { + DescriptionJavadocTag -> this.descriptionElements.toList() + else -> this.findTagsByName(tag.name).toList() + } + } + + private fun List<PsiElement>.withoutReferenceLink(): List<PsiElement> = drop(1) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JavaDocComment + + if (comment != other.comment) return false + + return true + } + + override fun hashCode(): Int { + return comment.hashCode() + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt new file mode 100644 index 00000000..00efeb0a --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiDocCommentOwner +import com.intellij.psi.PsiNamedElement + +internal class JavaDocCommentCreator : DocCommentCreator { + override fun create(element: PsiNamedElement): DocComment? { + val psiDocComment = (element as? PsiDocCommentOwner)?.docComment ?: return null + return JavaDocComment(psiDocComment) + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt new file mode 100644 index 00000000..c36ce50d --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt @@ -0,0 +1,22 @@ +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiElement +import com.intellij.psi.javadoc.PsiDocTag +import org.jetbrains.dokka.analysis.java.JavadocTag +import org.jetbrains.dokka.analysis.java.util.contentElementsWithSiblingIfNeeded + +internal data class PsiDocumentationContent( + val psiElement: PsiElement, + override val tag: JavadocTag +) : DocumentationContent { + + override fun resolveSiblings(): List<DocumentationContent> { + return if (psiElement is PsiDocTag) { + psiElement.contentElementsWithSiblingIfNeeded() + .map { content -> PsiDocumentationContent(content, tag) } + } else { + listOf(this) + } + } + +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt new file mode 100644 index 00000000..1e193648 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.analysis.java.parsers + +import com.intellij.psi.javadoc.PsiDocComment +import org.jetbrains.dokka.analysis.java.JavadocTag + +internal data class CommentResolutionContext( + val comment: PsiDocComment, + val tag: JavadocTag? +) diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt new file mode 100644 index 00000000..3f691799 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.java.parsers + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.model.doc.DocumentationNode + +@InternalDokkaApi +interface DocCommentParser { + fun canParse(docComment: DocComment): Boolean + fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt new file mode 100644 index 00000000..45f44338 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt @@ -0,0 +1,797 @@ +package org.jetbrains.dokka.analysis.java.parsers + +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute +import com.intellij.lang.jvm.annotation.JvmAnnotationAttributeValue +import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue +import com.intellij.lang.jvm.annotation.JvmAnnotationEnumFieldValue +import com.intellij.lang.jvm.types.JvmReferenceType +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.BreakingAbstractionKotlinLightMethodChecker +import org.jetbrains.dokka.analysis.java.SyntheticElementDocumentationProvider +import org.jetbrains.dokka.analysis.java.getVisibility +import org.jetbrains.dokka.analysis.java.util.* +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.nextTarget +import org.jetbrains.dokka.links.withClass +import org.jetbrains.dokka.links.withEnumEntryExtra +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.AnnotationTarget +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.Param +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.parallelForEach +import org.jetbrains.dokka.utilities.parallelMap +import org.jetbrains.dokka.utilities.parallelMapNotNull + +internal class DokkaPsiParser( + private val sourceSetData: DokkaConfiguration.DokkaSourceSet, + private val project: Project, + private val logger: DokkaLogger, + private val javadocParser: JavadocParser, + private val javaPsiDocCommentParser: JavaPsiDocCommentParser, + private val lightMethodChecker: BreakingAbstractionKotlinLightMethodChecker, +) { + private val syntheticDocProvider = SyntheticElementDocumentationProvider(javaPsiDocCommentParser, project) + + private val cachedBounds = hashMapOf<String, Bound>() + + private val PsiMethod.hash: Int + get() = "$returnType $name$parameterList".hashCode() + + private val PsiField.hash: Int + get() = "$type $name".hashCode() + + private val PsiClassType.shouldBeIgnored: Boolean + get() = isClass("java.lang.Enum") || isClass("java.lang.Object") + + private fun PsiClassType.isClass(qName: String): Boolean { + val shortName = qName.substringAfterLast('.') + if (className == shortName) { + val psiClass = resolve() + return psiClass?.qualifiedName == qName + } + return false + } + + private fun <T> T.toSourceSetDependent() = mapOf(sourceSetData to this) + + suspend fun parsePackage(packageName: String, psiFiles: List<PsiJavaFile>): DPackage = coroutineScope { + val dri = DRI(packageName = packageName) + val packageInfo = psiFiles.singleOrNull { it.name == "package-info.java" } + val documentation = packageInfo?.let { + javadocParser.parseDocumentation(it).toSourceSetDependent() + }.orEmpty() + val annotations = packageInfo?.packageStatement?.annotationList?.annotations + + DPackage( + dri = dri, + functions = emptyList(), + properties = emptyList(), + classlikes = psiFiles.parallelMap { psiFile -> + coroutineScope { + psiFile.classes.asIterable().parallelMap { parseClasslike(it, dri) } + } + }.flatten(), + typealiases = emptyList(), + documentation = documentation, + expectPresentInSet = null, + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( + annotations?.toList().orEmpty().toListOfAnnotations().toSourceSetDependent().toAnnotations() + ) + ) + } + + private suspend fun parseClasslike(psi: PsiClass, parent: DRI): DClasslike = coroutineScope { + with(psi) { + val dri = parent.withClass(name.toString()) + val superMethodsKeys = hashSetOf<Int>() + val superMethods = mutableListOf<Pair<PsiMethod, DRI>>() + val superFieldsKeys = hashSetOf<Int>() + val superFields = mutableListOf<Pair<PsiField, DRI>>() + methods.asIterable().parallelForEach { superMethodsKeys.add(it.hash) } + + /** + * Caution! This method mutates + * - superMethodsKeys + * - superMethods + * - superFieldsKeys + * - superKeys + */ + /** + * Caution! This method mutates + * - superMethodsKeys + * - superMethods + * - superFieldsKeys + * - superKeys + */ + fun Array<PsiClassType>.getSuperTypesPsiClasses(): List<Pair<PsiClass, JavaClassKindTypes>> { + forEach { type -> + (type as? PsiClassType)?.resolve()?.let { + val definedAt = DRI.from(it) + it.methods.forEach { method -> + val hash = method.hash + if (!method.isConstructor && !superMethodsKeys.contains(hash) && + method.getVisibility() != JavaVisibility.Private + ) { + superMethodsKeys.add(hash) + superMethods.add(Pair(method, definedAt)) + } + } + it.fields.forEach { field -> + val hash = field.hash + if (!superFieldsKeys.contains(hash)) { + superFieldsKeys.add(hash) + superFields.add(Pair(field, definedAt)) + } + } + } + } + return filter { !it.shouldBeIgnored }.mapNotNull { supertypePsi -> + supertypePsi.resolve()?.let { supertypePsiClass -> + val javaClassKind = when { + supertypePsiClass.isInterface -> JavaClassKindTypes.INTERFACE + else -> JavaClassKindTypes.CLASS + } + supertypePsiClass to javaClassKind + } + } + } + + fun traversePsiClassForAncestorsAndInheritedMembers(psiClass: PsiClass): AncestryNode { + val (classes, interfaces) = psiClass.superTypes.getSuperTypesPsiClasses() + .partition { it.second == JavaClassKindTypes.CLASS } + + return AncestryNode( + typeConstructor = GenericTypeConstructor( + DRI.from(psiClass), + psiClass.typeParameters.map { typeParameter -> + TypeParameter( + dri = DRI.from(typeParameter), + name = typeParameter.name.orEmpty(), + extra = typeParameter.annotations() + ) + } + ), + superclass = classes.singleOrNull()?.first?.let(::traversePsiClassForAncestorsAndInheritedMembers), + interfaces = interfaces.map { traversePsiClassForAncestorsAndInheritedMembers(it.first) } + ) + } + + val ancestry: AncestryNode = traversePsiClassForAncestorsAndInheritedMembers(this) + + val (regularFunctions, accessors) = splitFunctionsAndAccessors(psi.fields, psi.methods) + val (regularSuperFunctions, superAccessors) = splitFunctionsAndAccessors( + fields = superFields.map { it.first }.toTypedArray(), + methods = superMethods.map { it.first }.toTypedArray() + ) + + val regularSuperFunctionsKeys = regularSuperFunctions.map { it.hash }.toSet() + val regularSuperFunctionsWithDRI = superMethods.filter { it.first.hash in regularSuperFunctionsKeys } + + val superAccessorsWithDRI = superAccessors.mapValues { (field, methods) -> + val containsJvmField = field.annotations.mapNotNull { it.toAnnotation() }.any { it.isJvmField() } + if (containsJvmField) { + emptyList() + } else { + methods.mapNotNull { method -> superMethods.find { it.first.hash == method.hash } } + } + } + + val overridden = regularFunctions.flatMap { it.findSuperMethods().toList() } + val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent() + val allFunctions = async { + val parsedRegularFunctions = regularFunctions.parallelMapNotNull { + if (!it.isConstructor) parseFunction( + it, + parentDRI = dri + ) else null + } + val parsedSuperFunctions = regularSuperFunctionsWithDRI + .filter { it.first !in overridden } + .parallelMap { parseFunction(it.first, inheritedFrom = it.second) } + + parsedRegularFunctions + parsedSuperFunctions + } + val allFields = async { + val parsedFields = fields.toList().parallelMapNotNull { + parseField(it, accessors[it].orEmpty()) + } + val parsedSuperFields = superFields.parallelMapNotNull { (field, dri) -> + parseFieldWithInheritingAccessors( + field, + superAccessorsWithDRI[field].orEmpty(), + inheritedFrom = dri + ) + } + parsedFields + parsedSuperFields + } + val source = parseSources() + val classlikes = async { innerClasses.asIterable().parallelMap { parseClasslike(it, dri) } } + val visibility = getVisibility().toSourceSetDependent() + val ancestors = (listOfNotNull(ancestry.superclass?.let { + it.typeConstructor.let { typeConstructor -> + TypeConstructorWithKind( + typeConstructor, + JavaClassKindTypes.CLASS + ) + } + }) + ancestry.interfaces.map { + TypeConstructorWithKind( + it.typeConstructor, + JavaClassKindTypes.INTERFACE + ) + }).toSourceSetDependent() + val modifiers = getModifier().toSourceSetDependent() + val implementedInterfacesExtra = + ImplementedInterfaces(ancestry.allImplementedInterfaces().toSourceSetDependent()) + + when { + isAnnotationType -> + DAnnotation( + name = name.orEmpty(), + dri = dri, + documentation = documentation, + expectPresentInSet = null, + sources = source, + functions = allFunctions.await(), + properties = allFields.await(), + classlikes = classlikes.await(), + visibility = visibility, + companion = null, + constructors = parseConstructors(dri), + generics = mapTypeParameters(dri), + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( + implementedInterfacesExtra, + annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations() + ) + ) + + isEnum -> DEnum( + dri = dri, + name = name.orEmpty(), + entries = fields.filterIsInstance<PsiEnumConstant>().map { entry -> + DEnumEntry( + dri = dri.withClass(entry.name).withEnumEntryExtra(), + name = entry.name, + documentation = javadocParser.parseDocumentation(entry).toSourceSetDependent(), + expectPresentInSet = null, + functions = emptyList(), + properties = emptyList(), + classlikes = emptyList(), + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( + implementedInterfacesExtra, + annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations() + ) + ) + }, + documentation = documentation, + expectPresentInSet = null, + sources = source, + functions = allFunctions.await(), + properties = fields.filter { it !is PsiEnumConstant } + .map { parseField(it, accessors[it].orEmpty()) }, + classlikes = classlikes.await(), + visibility = visibility, + companion = null, + constructors = parseConstructors(dri), + supertypes = ancestors, + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( + implementedInterfacesExtra, + annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations() + ) + ) + + isInterface -> DInterface( + dri = dri, + name = name.orEmpty(), + documentation = documentation, + expectPresentInSet = null, + sources = source, + functions = allFunctions.await(), + properties = allFields.await(), + classlikes = classlikes.await(), + visibility = visibility, + companion = null, + generics = mapTypeParameters(dri), + supertypes = ancestors, + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( + implementedInterfacesExtra, + annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations() + ) + ) + + else -> DClass( + dri = dri, + name = name.orEmpty(), + constructors = parseConstructors(dri), + functions = allFunctions.await(), + properties = allFields.await(), + classlikes = classlikes.await(), + sources = source, + visibility = visibility, + companion = null, + generics = mapTypeParameters(dri), + supertypes = ancestors, + documentation = documentation, + expectPresentInSet = null, + modifier = modifiers, + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = PropertyContainer.withAll( + implementedInterfacesExtra, + annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations(), + ancestry.exceptionInSupertypesOrNull() + ) + ) + } + } + } + + /* + * Parameter `parentDRI` required for substitute package name: + * in the case of synthetic constructor, it will return empty from [DRI.Companion.from]. + */ + private fun PsiClass.parseConstructors(parentDRI: DRI): List<DFunction> { + val constructors = when { + isAnnotationType || isInterface -> emptyArray() + isEnum -> this.constructors + else -> this.constructors.takeIf { it.isNotEmpty() } ?: arrayOf(createDefaultConstructor()) + } + return constructors.map { parseFunction(psi = it, isConstructor = true, parentDRI = parentDRI) } + } + + /** + * PSI doesn't return a default constructor if class doesn't contain an explicit one. + * This method create synthetic constructor + * Visibility modifier is preserved from the class. + */ + private fun PsiClass.createDefaultConstructor(): PsiMethod { + val psiElementFactory = JavaPsiFacade.getElementFactory(project) + val signature = when (val classVisibility = getVisibility()) { + JavaVisibility.Default -> name.orEmpty() + else -> "${classVisibility.name} $name" + } + return psiElementFactory.createConstructor(signature, this) + } + + private fun AncestryNode.exceptionInSupertypesOrNull(): ExceptionInSupertypes? = + typeConstructorsBeingExceptions().takeIf { it.isNotEmpty() } + ?.let { ExceptionInSupertypes(it.toSourceSetDependent()) } + + private fun parseFunction( + psi: PsiMethod, + isConstructor: Boolean = false, + inheritedFrom: DRI? = null, + parentDRI: DRI? = null, + ): DFunction { + val dri = parentDRI?.let { dri -> + DRI.from(psi).copy(packageName = dri.packageName, classNames = dri.classNames) + } ?: DRI.from(psi) + val docs = psi.getDocumentation() + return DFunction( + dri = dri, + name = psi.name, + isConstructor = isConstructor, + parameters = psi.parameterList.parameters.map { psiParameter -> + DParameter( + dri = dri.copy(target = dri.target.nextTarget()), + name = psiParameter.name, + documentation = DocumentationNode( + listOfNotNull(docs.firstChildOfTypeOrNull<Param> { + it.name == psiParameter.name + }) + ).toSourceSetDependent(), + expectPresentInSet = null, + type = getBound(psiParameter.type), + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( + psiParameter.annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations() + ) + ) + }, + documentation = docs.toSourceSetDependent(), + expectPresentInSet = null, + sources = psi.parseSources(), + visibility = psi.getVisibility().toSourceSetDependent(), + type = psi.returnType?.let { getBound(type = it) } ?: Void, + generics = psi.mapTypeParameters(dri), + receiver = null, + modifier = psi.getModifier().toSourceSetDependent(), + sourceSets = setOf(sourceSetData), + isExpectActual = false, + extra = psi.additionalExtras().let { + PropertyContainer.withAll( + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + it.toSourceSetDependent().toAdditionalModifiers(), + (psi.annotations.toList() + .toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent() + .toAnnotations(), + ObviousMember.takeIf { psi.isObvious(inheritedFrom) }, + psi.throwsList.toDriList().takeIf { it.isNotEmpty() } + ?.let { CheckedExceptions(it.toSourceSetDependent()) } + ) + } + ) + } + + private fun PsiNamedElement.parseSources(): SourceSetDependent<DocumentableSource> { + return when { + // `isPhysical` detects the virtual declarations without real sources. + // Otherwise, `PsiDocumentableSource` initialization will fail: non-physical declarations doesn't have `virtualFile`. + // This check protects from accidentally requesting sources for synthetic / virtual declarations. + isPhysical -> PsiDocumentableSource(this).toSourceSetDependent() + else -> emptyMap() + } + } + + private fun PsiMethod.getDocumentation(): DocumentationNode = + this.takeIf { it is SyntheticElement }?.let { syntheticDocProvider.getDocumentation(it) } + ?: javadocParser.parseDocumentation(this) + + private fun PsiMethod.isObvious(inheritedFrom: DRI? = null): Boolean { + return (this is SyntheticElement && !syntheticDocProvider.isDocumented(this)) + || inheritedFrom?.isObvious() == true + } + + private fun DRI.isObvious(): Boolean { + return packageName == "java.lang" && (classNames == "Object" || classNames == "Enum") + } + + private fun PsiReferenceList.toDriList() = referenceElements.mapNotNull { it?.resolve()?.let { DRI.from(it) } } + + private fun PsiModifierListOwner.additionalExtras() = listOfNotNull( + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { hasModifier(JvmModifier.STATIC) }, + ExtraModifiers.JavaOnlyModifiers.Native.takeIf { hasModifier(JvmModifier.NATIVE) }, + ExtraModifiers.JavaOnlyModifiers.Synchronized.takeIf { hasModifier(JvmModifier.SYNCHRONIZED) }, + ExtraModifiers.JavaOnlyModifiers.StrictFP.takeIf { hasModifier(JvmModifier.STRICTFP) }, + ExtraModifiers.JavaOnlyModifiers.Transient.takeIf { hasModifier(JvmModifier.TRANSIENT) }, + ExtraModifiers.JavaOnlyModifiers.Volatile.takeIf { hasModifier(JvmModifier.VOLATILE) }, + ExtraModifiers.JavaOnlyModifiers.Transitive.takeIf { hasModifier(JvmModifier.TRANSITIVE) } + ).toSet() + + private fun Set<ExtraModifiers>.toListOfAnnotations() = map { + if (it !is ExtraModifiers.JavaOnlyModifiers.Static) + Annotations.Annotation(DRI("kotlin.jvm", it.name.toLowerCase().capitalize()), emptyMap()) + else + Annotations.Annotation(DRI("kotlin.jvm", "JvmStatic"), emptyMap()) + } + + /** + * Workaround for getting JvmField Kotlin annotation in PSIs + */ + private fun Collection<PsiAnnotation>.findJvmFieldAnnotation(): Annotations.Annotation? { + val anyJvmFieldAnnotation = this.any { + it.qualifiedName == "$JVM_FIELD_PACKAGE_NAME.$JVM_FIELD_CLASS_NAMES" + } + return if (anyJvmFieldAnnotation) { + Annotations.Annotation(DRI(JVM_FIELD_PACKAGE_NAME, JVM_FIELD_CLASS_NAMES), emptyMap()) + } else { + null + } + } + + private fun <T : AnnotationTarget> PsiTypeParameter.annotations(): PropertyContainer<T> = this.annotations.toList().toListOfAnnotations().annotations() + private fun <T : AnnotationTarget> PsiType.annotations(): PropertyContainer<T> = this.annotations.toList().toListOfAnnotations().annotations() + + private fun <T : AnnotationTarget> List<Annotations.Annotation>.annotations(): PropertyContainer<T> = + this.takeIf { it.isNotEmpty() }?.let { annotations -> + PropertyContainer.withAll(annotations.toSourceSetDependent().toAnnotations()) + } ?: PropertyContainer.empty() + + private fun getBound(type: PsiType): Bound { + //We would like to cache most of the bounds since it is not common to annotate them, + //but if this is the case, we treat them as 'one of' + fun PsiType.cacheBoundIfHasNoAnnotation(f: (List<Annotations.Annotation>) -> Bound): Bound { + val annotations = this.annotations.toList().toListOfAnnotations() + return if (annotations.isNotEmpty()) f(annotations) + else cachedBounds.getOrPut(canonicalText) { + f(annotations) + } + } + + return when (type) { + is PsiClassType -> + type.resolve()?.let { resolved -> + when { + resolved.qualifiedName == "java.lang.Object" -> type.cacheBoundIfHasNoAnnotation { annotations -> JavaObject(annotations.annotations()) } + resolved is PsiTypeParameter -> { + TypeParameter( + dri = DRI.from(resolved), + name = resolved.name.orEmpty(), + extra = type.annotations() + ) + } + + Regex("kotlin\\.jvm\\.functions\\.Function.*").matches(resolved.qualifiedName ?: "") || + Regex("java\\.util\\.function\\.Function.*").matches( + resolved.qualifiedName ?: "" + ) -> FunctionalTypeConstructor( + DRI.from(resolved), + type.parameters.map { getProjection(it) }, + extra = type.annotations() + ) + + else -> { + // cache types that have no annotation and no type parameter + // since we cache only by name and type parameters depend on context + val typeParameters = type.parameters.map { getProjection(it) } + if (typeParameters.isEmpty()) + type.cacheBoundIfHasNoAnnotation { annotations -> + GenericTypeConstructor( + DRI.from(resolved), + typeParameters, + extra = annotations.annotations() + ) + } + else + GenericTypeConstructor( + DRI.from(resolved), + typeParameters, + extra = type.annotations() + ) + } + } + } ?: UnresolvedBound(type.presentableText, type.annotations()) + + is PsiArrayType -> GenericTypeConstructor( + DRI("kotlin", "Array"), + listOf(getProjection(type.componentType)), + extra = type.annotations() + ) + + is PsiPrimitiveType -> if (type.name == "void") Void + else type.cacheBoundIfHasNoAnnotation { annotations -> PrimitiveJavaType(type.name, annotations.annotations()) } + else -> throw IllegalStateException("${type.presentableText} is not supported by PSI parser") + } + } + + + private fun getVariance(type: PsiWildcardType): Projection = when { + type.extendsBound != PsiType.NULL -> Covariance(getBound(type.extendsBound)) + type.superBound != PsiType.NULL -> Contravariance(getBound(type.superBound)) + else -> throw IllegalStateException("${type.presentableText} has incorrect bounds") + } + + private fun getProjection(type: PsiType): Projection = when (type) { + is PsiEllipsisType -> Star + is PsiWildcardType -> getVariance(type) + else -> getBound(type) + } + + private fun PsiModifierListOwner.getModifier() = when { + hasModifier(JvmModifier.ABSTRACT) -> JavaModifier.Abstract + hasModifier(JvmModifier.FINAL) -> JavaModifier.Final + else -> JavaModifier.Empty + } + + private fun PsiTypeParameterListOwner.mapTypeParameters(dri: DRI): List<DTypeParameter> { + fun mapBounds(bounds: Array<JvmReferenceType>): List<Bound> = + if (bounds.isEmpty()) emptyList() else bounds.mapNotNull { + (it as? PsiClassType)?.let { classType -> Nullable(getBound(classType)) } + } + return typeParameters.map { type -> + DTypeParameter( + dri = dri.copy(target = dri.target.nextTarget()), + name = type.name.orEmpty(), + presentableName = null, + documentation = javadocParser.parseDocumentation(type).toSourceSetDependent(), + expectPresentInSet = null, + bounds = mapBounds(type.bounds), + sourceSets = setOf(sourceSetData), + extra = PropertyContainer.withAll( + type.annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations() + ) + ) + } + } + + private fun parseFieldWithInheritingAccessors( + psi: PsiField, + accessors: List<Pair<PsiMethod, DRI>>, + inheritedFrom: DRI + ): DProperty { + val getter = accessors + .firstOrNull { (method, _) -> method.isGetterFor(psi) } + ?.let { (method, dri) -> parseFunction(method, inheritedFrom = dri) } + + val setter = accessors + .firstOrNull { (method, _) -> method.isSetterFor(psi) } + ?.let { (method, dri) -> parseFunction(method, inheritedFrom = dri) } + + return parseField( + psi = psi, + getter = getter, + setter = setter, + inheritedFrom = inheritedFrom + ) + } + + private fun parseField(psi: PsiField, accessors: List<PsiMethod>, inheritedFrom: DRI? = null): DProperty { + val getter = accessors.firstOrNull { it.isGetterFor(psi) }?.let { parseFunction(it) } + val setter = accessors.firstOrNull { it.isSetterFor(psi) }?.let { parseFunction(it) } + return parseField( + psi = psi, + getter = getter, + setter = setter, + inheritedFrom = inheritedFrom + ) + } + + private fun parseField(psi: PsiField, getter: DFunction?, setter: DFunction?, inheritedFrom: DRI? = null): DProperty { + val dri = DRI.from(psi) + + // non-final java field without accessors should be a var + // setter should be not null when inheriting kotlin vars + val isMutable = !psi.hasModifierProperty("final") + val isVar = (isMutable && getter == null && setter == null) || (getter != null && setter != null) + + return DProperty( + dri = dri, + name = psi.name, + documentation = javadocParser.parseDocumentation(psi).toSourceSetDependent(), + expectPresentInSet = null, + sources = psi.parseSources(), + visibility = psi.getVisibility(getter).toSourceSetDependent(), + type = getBound(psi.type), + receiver = null, + setter = setter, + getter = getter, + modifier = psi.getModifier().toSourceSetDependent(), + sourceSets = setOf(sourceSetData), + generics = emptyList(), + isExpectActual = false, + extra = psi.additionalExtras().let { + val psiAnnotations = psi.annotations.toList() + val parsedAnnotations = psiAnnotations.toListOfAnnotations() + val extraModifierAnnotations = it.toListOfAnnotations() + val jvmFieldAnnotation = psiAnnotations.findJvmFieldAnnotation() + val annotations = parsedAnnotations + extraModifierAnnotations + listOfNotNull(jvmFieldAnnotation) + + PropertyContainer.withAll( + inheritedFrom?.let { inheritedFrom -> InheritedMember(inheritedFrom.toSourceSetDependent()) }, + it.toSourceSetDependent().toAdditionalModifiers(), + annotations.toSourceSetDependent().toAnnotations(), + psi.getConstantExpression()?.let { DefaultValue(it.toSourceSetDependent()) }, + takeIf { isVar }?.let { IsVar } + ) + } + ) + } + + private fun PsiField.getVisibility(getter: DFunction?): Visibility { + return getter?.visibility?.get(sourceSetData) ?: this.getVisibility() + } + + private fun Collection<PsiAnnotation>.toListOfAnnotations() = + filter { !lightMethodChecker.isLightAnnotation(it) }.mapNotNull { it.toAnnotation() } + + private fun PsiField.getConstantExpression(): Expression? { + val constantValue = this.computeConstantValue() ?: return null + return when (constantValue) { + is Byte -> IntegerConstant(constantValue.toLong()) + is Short -> IntegerConstant(constantValue.toLong()) + is Int -> IntegerConstant(constantValue.toLong()) + is Long -> IntegerConstant(constantValue) + is Char -> StringConstant(constantValue.toString()) + is String -> StringConstant(constantValue) + is Double -> DoubleConstant(constantValue) + is Float -> FloatConstant(constantValue) + is Boolean -> BooleanConstant(constantValue) + else -> ComplexExpression(constantValue.toString()) + } + } + + private fun JvmAnnotationAttribute.toValue(): AnnotationParameterValue = when (this) { + is PsiNameValuePair -> value?.toValue() ?: attributeValue?.toValue() ?: StringValue("") + else -> StringValue(this.attributeName) + }.let { annotationValue -> + if (annotationValue is StringValue) annotationValue.copy(annotationValue.value.removeSurrounding("\"")) + else annotationValue + } + + /** + * This is a workaround for static imports from JDK like RetentionPolicy + * For some reason they are not represented in the same way than using normal import + */ + private fun JvmAnnotationAttributeValue.toValue(): AnnotationParameterValue? { + return when (this) { + is JvmAnnotationEnumFieldValue -> (field as? PsiElement)?.let { EnumValue(fieldName ?: "", DRI.from(it)) } + // static import of a constant is resolved to constant value instead of a field/link + is JvmAnnotationConstantValue -> this.constantValue?.toAnnotationLiteralValue() + else -> null + } + } + + private fun Any.toAnnotationLiteralValue() = when (this) { + is Byte -> IntValue(this.toInt()) + is Short -> IntValue(this.toInt()) + is Char -> StringValue(this.toString()) + is Int -> IntValue(this) + is Long -> LongValue(this) + is Boolean -> BooleanValue(this) + is Float -> FloatValue(this) + is Double -> DoubleValue(this) + else -> StringValue(this.toString()) + } + + private fun PsiAnnotationMemberValue.toValue(): AnnotationParameterValue? = when (this) { + is PsiAnnotation -> toAnnotation()?.let { AnnotationValue(it) } + is PsiArrayInitializerMemberValue -> ArrayValue(initializers.mapNotNull { it.toValue() }) + is PsiReferenceExpression -> psiReference?.let { EnumValue(text ?: "", DRI.from(it)) } + is PsiClassObjectAccessExpression -> { + val parameterType = (type as? PsiClassType)?.parameters?.firstOrNull() + val classType = when (parameterType) { + is PsiClassType -> parameterType.resolve() + // Notice: Array<String>::class will be passed down as String::class + // should probably be Array::class instead but this reflects behaviour for Kotlin sources + is PsiArrayType -> (parameterType.componentType as? PsiClassType)?.resolve() + else -> null + } + classType?.let { ClassValue(it.name ?: "", DRI.from(it)) } + } + is PsiLiteralExpression -> toValue() + else -> StringValue(text ?: "") + } + + private fun PsiLiteralExpression.toValue(): AnnotationParameterValue? = when (type) { + PsiType.INT -> (value as? Int)?.let { IntValue(it) } + PsiType.LONG -> (value as? Long)?.let { LongValue(it) } + PsiType.FLOAT -> (value as? Float)?.let { FloatValue(it) } + PsiType.DOUBLE -> (value as? Double)?.let { DoubleValue(it) } + PsiType.BOOLEAN -> (value as? Boolean)?.let { BooleanValue(it) } + PsiType.NULL -> NullValue + else -> StringValue(text ?: "") + } + + private fun PsiAnnotation.toAnnotation(): Annotations.Annotation? { + // TODO Mitigating workaround for issue https://github.com/Kotlin/dokka/issues/1341 + // Tracking https://youtrack.jetbrains.com/issue/KT-41234 + // Needs to be removed once this issue is fixed in light classes + fun PsiElement.getAnnotationsOrNull(): Array<PsiAnnotation>? { + this as PsiClass + return try { + this.annotations + } catch (e: Exception) { + logger.warn("Failed to get annotations from ${this.qualifiedName}") + null + } + } + + return psiReference?.let { psiElement -> + Annotations.Annotation( + dri = DRI.from(psiElement), + params = attributes + .filter { !lightMethodChecker.isLightAnnotationAttribute(it) } + .mapNotNull { it.attributeName to it.toValue() } + .toMap(), + mustBeDocumented = psiElement.getAnnotationsOrNull().orEmpty().any { annotation -> + annotation.hasQualifiedName("java.lang.annotation.Documented") + } + ) + } + } + + private val PsiElement.psiReference + get() = getChildOfType<PsiJavaCodeReferenceElement>()?.resolve() +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt new file mode 100644 index 00000000..9c475054 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt @@ -0,0 +1,228 @@ +package org.jetbrains.dokka.analysis.java.parsers + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.impl.source.tree.JavaDocElementType +import com.intellij.psi.impl.source.tree.LazyParseablePsiElement +import com.intellij.psi.javadoc.PsiDocComment +import com.intellij.psi.javadoc.PsiDocTag + +import org.jetbrains.dokka.analysis.java.* +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.analysis.java.doccomment.JavaDocComment +import org.jetbrains.dokka.analysis.java.parsers.doctag.PsiDocTagParser +import org.jetbrains.dokka.analysis.java.util.* +import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.Deprecated + +internal class JavaPsiDocCommentParser( + private val psiDocTagParser: PsiDocTagParser, +) : DocCommentParser { + + override fun canParse(docComment: DocComment): Boolean { + return docComment is JavaDocComment + } + + override fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode { + val javaDocComment = docComment as JavaDocComment + return parsePsiDocComment(javaDocComment.comment, context) + } + + internal fun parsePsiDocComment(docComment: PsiDocComment, context: PsiNamedElement): DocumentationNode { + val description = listOfNotNull(docComment.getDescription()) + val tags = docComment.tags.mapNotNull { tag -> + parseDocTag(tag, docComment, context) + } + return DocumentationNode(description + tags) + } + + private fun PsiDocComment.getDescription(): Description? { + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = descriptionElements.asIterable(), + commentResolutionContext = CommentResolutionContext(this, DescriptionJavadocTag), + ) + return docTags.takeIf { it.isNotEmpty() }?.let { + Description(wrapTagIfNecessary(it)) + } + } + + private fun parseDocTag(tag: PsiDocTag, docComment: PsiDocComment, analysedElement: PsiNamedElement): TagWrapper? { + return when (tag.name) { + ParamJavadocTag.name -> parseParamTag(tag, docComment, analysedElement) + ThrowsJavadocTag.name, ExceptionJavadocTag.name -> parseThrowsTag(tag, docComment) + ReturnJavadocTag.name -> parseReturnTag(tag, docComment) + SinceJavadocTag.name -> parseSinceTag(tag, docComment) + AuthorJavadocTag.name -> parseAuthorTag(tag, docComment) + SeeJavadocTag.name -> parseSeeTag(tag, docComment) + DeprecatedJavadocTag.name -> parseDeprecatedTag(tag, docComment) + else -> emptyTagWrapper(tag, docComment) + } + } + + private fun parseParamTag( + tag: PsiDocTag, + docComment: PsiDocComment, + analysedElement: PsiNamedElement + ): TagWrapper? { + val paramName = tag.dataElements.firstOrNull()?.text.orEmpty() + + // can be a PsiClass if @param is referencing class generics, like here: + // https://github.com/biojava/biojava/blob/2417c230be36e4ba73c62bb3631b60f876265623/biojava-core/src/main/java/org/biojava/nbio/core/alignment/SimpleProfilePair.java#L43 + // not supported at the moment + val method = analysedElement as? PsiMethod ?: return null + val paramIndex = method.parameterList.parameters.map { it.name }.indexOf(paramName) + + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = tag.contentElementsWithSiblingIfNeeded().drop(1), + commentResolutionContext = CommentResolutionContext( + comment = docComment, + tag = ParamJavadocTag(method, paramName, paramIndex) + ) + ) + return Param(root = wrapTagIfNecessary(docTags), name = paramName) + } + + private fun parseThrowsTag( + tag: PsiDocTag, + docComment: PsiDocComment + ): Throws { + val resolvedElement = tag.resolveToElement() + val exceptionAddress = resolvedElement?.let { DRI.from(it) } + + /* we always would like to have a fully qualified name as name, + * because it will be used as a display name later and we would like to have those unified + * even if documentation states shortened version + * Only if dri search fails we should use the provided phrase (since then we are not able to get a fq name) + */ + val fullyQualifiedExceptionName = + resolvedElement?.getKotlinFqName() ?: tag.dataElements.firstOrNull()?.text.orEmpty() + + val javadocTag = when (tag.name) { + ThrowsJavadocTag.name -> ThrowsJavadocTag(fullyQualifiedExceptionName) + ExceptionJavadocTag.name -> ExceptionJavadocTag(fullyQualifiedExceptionName) + else -> throw IllegalArgumentException("Expected @throws or @exception") + } + + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = tag.dataElements.drop(1), + commentResolutionContext = CommentResolutionContext( + comment = docComment, + tag = javadocTag + ), + ) + return Throws( + root = wrapTagIfNecessary(docTags), + name = fullyQualifiedExceptionName, + exceptionAddress = exceptionAddress + ) + } + + private fun parseReturnTag( + tag: PsiDocTag, + docComment: PsiDocComment + ): Return { + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = tag.contentElementsWithSiblingIfNeeded(), + commentResolutionContext = CommentResolutionContext(comment = docComment, tag = ReturnJavadocTag), + ) + return Return(root = wrapTagIfNecessary(docTags)) + } + + private fun parseSinceTag( + tag: PsiDocTag, + docComment: PsiDocComment + ): Since { + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = tag.contentElementsWithSiblingIfNeeded(), + commentResolutionContext = CommentResolutionContext(comment = docComment, tag = ReturnJavadocTag), + ) + return Since(root = wrapTagIfNecessary(docTags)) + } + + private fun parseAuthorTag( + tag: PsiDocTag, + docComment: PsiDocComment + ): Author { + // TODO [beresnev] see what the hell this is + // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, + // then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = tag.contentElementsWithSiblingIfNeeded(), + commentResolutionContext = CommentResolutionContext(comment = docComment, tag = AuthorJavadocTag), + ) + return Author(root = wrapTagIfNecessary(docTags)) + } + + private fun parseSeeTag( + tag: PsiDocTag, + docComment: PsiDocComment + ): See { + val referenceElement = tag.referenceElement() + val fullyQualifiedSeeReference = tag.resolveToElement()?.getKotlinFqName() + ?: referenceElement?.text.orEmpty().removePrefix("#") + + val context = CommentResolutionContext(comment = docComment, tag = SeeJavadocTag(fullyQualifiedSeeReference)) + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = tag.dataElements.dropWhile { + it is PsiWhiteSpace || it.isDocReferenceHolder() || it == referenceElement + }, + commentResolutionContext = context, + ) + + return See( + root = wrapTagIfNecessary(docTags), + name = fullyQualifiedSeeReference, + address = referenceElement?.toDocumentationLink(context = context)?.dri + ) + } + + private fun PsiElement.isDocReferenceHolder(): Boolean { + return (this as? LazyParseablePsiElement)?.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER + } + + private fun parseDeprecatedTag( + tag: PsiDocTag, + docComment: PsiDocComment + ): Deprecated { + val docTags = psiDocTagParser.parseAsParagraph( + tag.contentElementsWithSiblingIfNeeded(), + CommentResolutionContext(comment = docComment, tag = DeprecatedJavadocTag), + ) + return Deprecated(root = wrapTagIfNecessary(docTags)) + } + + private fun wrapTagIfNecessary(tags: List<DocTag>): CustomDocTag { + val isFile = (tags.singleOrNull() as? CustomDocTag)?.name == MARKDOWN_ELEMENT_FILE_NAME + return if (isFile) { + tags.first() as CustomDocTag + } else { + CustomDocTag(tags, name = MARKDOWN_ELEMENT_FILE_NAME) + } + } + + // Wrapper for unsupported tags https://github.com/Kotlin/dokka/issues/1618 + private fun emptyTagWrapper( + tag: PsiDocTag, + docComment: PsiDocComment, + ): CustomTagWrapper { + val docTags = psiDocTagParser.parseAsParagraph( + psiElements = tag.contentElementsWithSiblingIfNeeded(), + commentResolutionContext = CommentResolutionContext(docComment, null), + ) + return CustomTagWrapper( + root = wrapTagIfNecessary(docTags), + name = tag.name + ) + } + + private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null, context: CommentResolutionContext): DocumentationLink? { + val resolvedElement = this.resolveToGetDri() ?: return null + val label = labelElement ?: defaultLabel() + val docTags = psiDocTagParser.parse(listOfNotNull(label), context) + return DocumentationLink(dri = DRI.from(resolvedElement), children = docTags) + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt new file mode 100644 index 00000000..b8eba878 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt @@ -0,0 +1,24 @@ +package org.jetbrains.dokka.analysis.java.parsers + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.doccomment.DocCommentFinder +import org.jetbrains.dokka.model.doc.DocumentationNode + +internal fun interface JavaDocumentationParser { + fun parseDocumentation(element: PsiNamedElement): DocumentationNode +} + +@InternalDokkaApi +class JavadocParser( + private val docCommentParsers: List<DocCommentParser>, + private val docCommentFinder: DocCommentFinder +) : JavaDocumentationParser { + + override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { + val comment = docCommentFinder.findClosestToElement(element) ?: return DocumentationNode(emptyList()) + return docCommentParsers + .first { it.canParse(comment) } + .parse(comment, element) + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt new file mode 100644 index 00000000..050736f0 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt @@ -0,0 +1,47 @@ +package org.jetbrains.dokka.analysis.java.parsers.doctag + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocumentationNode +import java.util.* + +@InternalDokkaApi +class DocTagParserContext { + /** + * exists for resolving `@link element` links, where the referenced + * PSI element is mapped as DRI + * + * only used in the context of parsing to html and then from html to doctag + */ + private val driMap = mutableMapOf<String, DRI>() + + /** + * Cache created to make storing entries from kotlin easier. + * + * It has to be mutable to allow for adding entries when @inheritDoc resolves to kotlin code, + * from which we get a DocTags not descriptors. + */ + private val inheritDocSections = mutableMapOf<String, DocumentationNode>() + + /** + * @return key of the stored DRI + */ + fun store(dri: DRI): String { + val id = dri.toString() + driMap[id] = dri + return id + } + + /** + * @return key of the stored documentation node + */ + fun store(documentationNode: DocumentationNode): String { + val id = UUID.randomUUID().toString() + inheritDocSections[id] = documentationNode + return id + } + + fun getDri(id: String): DRI? = driMap[id] + + fun getDocumentationNode(id: String): DocumentationNode? = inheritDocSections[id] +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt new file mode 100644 index 00000000..ea1997b8 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt @@ -0,0 +1,114 @@ +package org.jetbrains.dokka.analysis.java.parsers.doctag + +import org.jetbrains.dokka.analysis.markdown.jb.parseHtmlEncodedWithNormalisedSpaces +import org.jetbrains.dokka.model.doc.* +import org.jsoup.Jsoup +import org.jsoup.nodes.Comment +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode + +internal class HtmlToDocTagConverter( + private val docTagParserContext: DocTagParserContext +) { + fun convertToDocTag(html: String): List<DocTag> { + return Jsoup.parseBodyFragment(html) + .body() + .childNodes() + .flatMap { convertHtmlNode(it) } + } + + private fun convertHtmlNode(node: Node, keepFormatting: Boolean = false): List<DocTag> = when (node) { + is TextNode -> (if (keepFormatting) { + node.wholeText.takeIf { it.isNotBlank() }?.let { listOf(Text(body = it)) } + } else { + node.wholeText.parseHtmlEncodedWithNormalisedSpaces(renderWhiteCharactersAsSpaces = true) + }).orEmpty() + is Comment -> listOf(Text(body = node.outerHtml(), params = DocTag.contentTypeParam("html"))) + is Element -> createBlock(node, keepFormatting) + else -> emptyList() + } + + private fun createBlock(element: Element, keepFormatting: Boolean = false): List<DocTag> { + val tagName = element.tagName() + val children = element.childNodes() + .flatMap { convertHtmlNode(it, keepFormatting = keepFormatting || tagName == "pre" || tagName == "code") } + + fun ifChildrenPresent(operation: () -> DocTag): List<DocTag> { + return if (children.isNotEmpty()) listOf(operation()) else emptyList() + } + return when (tagName) { + "blockquote" -> ifChildrenPresent { BlockQuote(children) } + "p" -> ifChildrenPresent { P(children) } + "b" -> ifChildrenPresent { B(children) } + "strong" -> ifChildrenPresent { Strong(children) } + "index" -> listOf(Index(children)) + "i" -> ifChildrenPresent { I(children) } + "img" -> listOf( + Img( + children, + element.attributes().associate { (if (it.key == "src") "href" else it.key) to it.value }) + ) + "em" -> listOf(Em(children)) + "code" -> ifChildrenPresent { if(keepFormatting) CodeBlock(children) else CodeInline(children) } + "pre" -> if(children.size == 1) { + when(children.first()) { + is CodeInline -> listOf(CodeBlock(children.first().children)) + is CodeBlock -> listOf(children.first()) + else -> listOf(Pre(children)) + } + } else { + listOf(Pre(children)) + } + "ul" -> ifChildrenPresent { Ul(children) } + "ol" -> ifChildrenPresent { Ol(children) } + "li" -> listOf(Li(children)) + "dl" -> ifChildrenPresent { Dl(children) } + "dt" -> listOf(Dt(children)) + "dd" -> listOf(Dd(children)) + "a" -> listOf(createLink(element, children)) + "table" -> ifChildrenPresent { Table(children) } + "tr" -> ifChildrenPresent { Tr(children) } + "td" -> listOf(Td(children)) + "thead" -> listOf(THead(children)) + "tbody" -> listOf(TBody(children)) + "tfoot" -> listOf(TFoot(children)) + "caption" -> ifChildrenPresent { Caption(children) } + "inheritdoc" -> { + // TODO [beresnev] describe how it works + val id = element.attr("id") + val section = docTagParserContext.getDocumentationNode(id) + val parsed = section?.children?.flatMap { it.root.children }.orEmpty() + if(parsed.size == 1 && parsed.first() is P){ + parsed.first().children + } else { + parsed + } + } + "h1" -> ifChildrenPresent { H1(children) } + "h2" -> ifChildrenPresent { H2(children) } + "h3" -> ifChildrenPresent { H3(children) } + "var" -> ifChildrenPresent { Var(children) } + "u" -> ifChildrenPresent { U(children) } + else -> listOf(Text(body = element.ownText())) + } + } + + private fun createLink(element: Element, children: List<DocTag>): DocTag { + return when { + element.hasAttr("docref") -> + A(children, params = mapOf("docref" to element.attr("docref"))) + element.hasAttr("href") -> + A(children, params = mapOf("href" to element.attr("href"))) + element.hasAttr("data-dri") && docTagParserContext.getDri(element.attr("data-dri")) != null -> { + val referencedDriId = element.attr("data-dri") + DocumentationLink( + dri = docTagParserContext.getDri(referencedDriId) + ?: error("docTagParserContext.getDri is null, TODO"), // TODO [beresnev] handle + children = children + ) + } + else -> Text(body = children.filterIsInstance<Text>().joinToString { it.body }) + } + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt new file mode 100644 index 00000000..31149898 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.analysis.java.parsers.doctag + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent + +@InternalDokkaApi +interface InheritDocTagContentProvider { + fun canConvert(content: DocumentationContent): Boolean + fun convertToHtml(content: DocumentationContent, docTagParserContext: DocTagParserContext): String +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt new file mode 100644 index 00000000..031a7b32 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt @@ -0,0 +1,114 @@ +package org.jetbrains.dokka.analysis.java.parsers.doctag + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import com.intellij.psi.javadoc.PsiDocComment +import org.jetbrains.dokka.analysis.java.* +import org.jetbrains.dokka.analysis.java.doccomment.* +import org.jetbrains.dokka.analysis.java.doccomment.JavaDocComment +import org.jetbrains.dokka.analysis.java.parsers.CommentResolutionContext + +internal class InheritDocTagResolver( + private val docCommentFactory: DocCommentFactory, + private val docCommentFinder: DocCommentFinder, + private val contentProviders: List<InheritDocTagContentProvider> +) { + internal fun convertToHtml(content: DocumentationContent, docTagParserContext: DocTagParserContext): String? { + return contentProviders + .firstOrNull { it.canConvert(content) } + ?.convertToHtml(content, docTagParserContext) + } + + internal fun resolveContent(context: CommentResolutionContext): List<DocumentationContent>? { + val javadocTag = context.tag ?: return null + + return when (javadocTag) { + is ThrowingExceptionJavadocTag -> { + javadocTag.exceptionQualifiedName?.let { _ -> + resolveThrowsTag( + javadocTag, + context.comment, + ) + } ?: return null + } + is ParamJavadocTag -> resolveParamTag(context.comment, javadocTag) + is DeprecatedJavadocTag -> resolveGenericTag(context.comment, DescriptionJavadocTag) + is SeeJavadocTag -> emptyList() + else -> resolveGenericTag(context.comment, javadocTag) + } + } + + private fun resolveGenericTag(currentElement: PsiDocComment, tag: JavadocTag): List<DocumentationContent> { + val docComment = when (val owner = currentElement.owner) { + is PsiClass -> lowestClassWithTag(owner, tag) + is PsiMethod -> lowestMethodWithTag(owner, tag) + else -> null + } + return docComment?.resolveTag(tag)?.flatMap { + it.resolveSiblings() + }.orEmpty() + } + + /** + * Main resolution point for exception like tags + * + * This should be used only with [ThrowsJavadocTag] or [ExceptionJavadocTag] as their resolution path should be the same + */ + private fun resolveThrowsTag( + tag: ThrowingExceptionJavadocTag, + currentElement: PsiDocComment, + ): List<DocumentationContent> { + val closestDocsWithThrows = + (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, tag) } + .orEmpty().firstOrNull { + docCommentFinder.findClosestToElement(it)?.hasTag(tag) == true + } ?: return emptyList() + + return docCommentFactory.fromElement(closestDocsWithThrows) + ?.resolveTag(tag) + ?: emptyList() + } + + private fun resolveParamTag( + currentElement: PsiDocComment, + paramTag: ParamJavadocTag, + ): List<DocumentationContent> { + val parameterIndex = paramTag.paramIndex + + val methods = (currentElement.owner as? PsiMethod) + ?.let { method -> lowestMethodsWithTag(method, paramTag) } + .orEmpty() + + return methods.flatMap { + if (parameterIndex >= it.parameterList.parametersCount || parameterIndex < 0) { + return@flatMap emptyList() + } + + val closestTag = docCommentFinder.findClosestToElement(it) + val hasTag = closestTag?.hasTag(paramTag) ?: false + closestTag?.takeIf { hasTag }?.resolveTag(ParamJavadocTag(it, "", parameterIndex)) ?: emptyList() + } + } + + //if we are in psi class javadoc only inherits docs from classes and not from interfaces + private fun lowestClassWithTag(baseClass: PsiClass, javadocTag: JavadocTag): DocComment? = + baseClass.superClass?.let { + docCommentFinder.findClosestToElement(it)?.takeIf { tag -> tag.hasTag(javadocTag) } ?: lowestClassWithTag( + it, + javadocTag + ) + } + + private fun lowestMethodWithTag( + baseMethod: PsiMethod, + javadocTag: JavadocTag, + ): DocComment? { + val methodsWithTag = lowestMethodsWithTag(baseMethod, javadocTag).firstOrNull() + return methodsWithTag?.let { + it.docComment?.let { JavaDocComment(it) } ?: docCommentFinder.findClosestToElement(it) + } + } + + private fun lowestMethodsWithTag(baseMethod: PsiMethod, javadocTag: JavadocTag): List<PsiMethod> = + baseMethod.findSuperMethods().filter { docCommentFinder.findClosestToElement(it)?.hasTag(javadocTag) == true } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt new file mode 100644 index 00000000..803eabcb --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt @@ -0,0 +1,39 @@ +package org.jetbrains.dokka.analysis.java.parsers.doctag + +import com.intellij.psi.PsiElement +import com.intellij.psi.javadoc.PsiDocTag +import org.jetbrains.dokka.analysis.java.parsers.CommentResolutionContext +import org.jetbrains.dokka.model.doc.* + +/** + * Parses [PsiElement] of [PsiDocTag] into Dokka's [DocTag] + */ +internal class PsiDocTagParser( + private val inheritDocTagResolver: InheritDocTagResolver +) { + fun parse( + psiElements: Iterable<PsiElement>, + commentResolutionContext: CommentResolutionContext + ): List<DocTag> = parse(asParagraph = false, psiElements, commentResolutionContext) + + fun parseAsParagraph( + psiElements: Iterable<PsiElement>, + commentResolutionContext: CommentResolutionContext + ): List<DocTag> = parse(asParagraph = true, psiElements, commentResolutionContext) + + private fun parse( + asParagraph: Boolean, + psiElements: Iterable<PsiElement>, + commentResolutionContext: CommentResolutionContext + ): List<DocTag> { + val docTagParserContext = DocTagParserContext() + + val psiToHtmlConverter = PsiElementToHtmlConverter(inheritDocTagResolver) + val elementsHtml = psiToHtmlConverter.convert(psiElements, docTagParserContext, commentResolutionContext) + ?: return emptyList() + + val htmlToDocTagConverter = HtmlToDocTagConverter(docTagParserContext) + val html = if (asParagraph) "<p>$elementsHtml</p>" else elementsHtml + return htmlToDocTagConverter.convertToDocTag(html) + } +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt new file mode 100644 index 00000000..0c20a9b7 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt @@ -0,0 +1,214 @@ +package org.jetbrains.dokka.analysis.java.parsers.doctag + +import com.intellij.lexer.JavaDocTokenTypes +import com.intellij.psi.* +import com.intellij.psi.impl.source.javadoc.PsiDocParamRef +import com.intellij.psi.impl.source.tree.LeafPsiElement +import com.intellij.psi.javadoc.PsiDocTagValue +import com.intellij.psi.javadoc.PsiDocToken +import com.intellij.psi.javadoc.PsiInlineDocTag +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.dokka.analysis.java.JavadocTag +import org.jetbrains.dokka.analysis.java.doccomment.PsiDocumentationContent +import org.jetbrains.dokka.analysis.java.parsers.CommentResolutionContext +import org.jetbrains.dokka.analysis.java.util.* +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.utilities.htmlEscape + +private const val UNRESOLVED_PSI_ELEMENT = "UNRESOLVED_PSI_ELEMENT" + +private data class HtmlParserState( + val currentJavadocTag: JavadocTag?, + val previousElement: PsiElement? = null, + val openPreTags: Int = 0, + val closedPreTags: Int = 0 +) + +private data class HtmlParsingResult(val newState: HtmlParserState, val parsedLine: String? = null) { + constructor(tag: JavadocTag?) : this(HtmlParserState(tag)) + + operator fun plus(other: HtmlParsingResult): HtmlParsingResult { + return HtmlParsingResult( + newState = other.newState, + parsedLine = listOfNotNull(parsedLine, other.parsedLine).joinToString(separator = "") + ) + } +} + +internal class PsiElementToHtmlConverter( + private val inheritDocTagResolver: InheritDocTagResolver +) { + private val preOpeningTagRegex = "<pre(\\s+.*)?>".toRegex() + private val preClosingTagRegex = "</pre>".toRegex() + + fun convert( + psiElements: Iterable<PsiElement>, + docTagParserContext: DocTagParserContext, + commentResolutionContext: CommentResolutionContext + ): String? { + return WithContext(docTagParserContext, commentResolutionContext) + .convert(psiElements) + } + + private inner class WithContext( + private val docTagParserContext: DocTagParserContext, + private val commentResolutionContext: CommentResolutionContext + ) { + fun convert(psiElements: Iterable<PsiElement>): String? { + val parsingResult = + psiElements.fold(HtmlParsingResult(commentResolutionContext.tag)) { resultAccumulator, psiElement -> + resultAccumulator + parseHtml(psiElement, resultAccumulator.newState) + } + return parsingResult.parsedLine?.trim() + } + + private fun parseHtml(psiElement: PsiElement, state: HtmlParserState): HtmlParsingResult = + when (psiElement) { + is PsiReference -> psiElement.children.fold(HtmlParsingResult(state)) { acc, e -> + acc + parseHtml(e, acc.newState) + } + else -> parseHtmlOfSimpleElement(psiElement, state) + } + + private fun parseHtmlOfSimpleElement(psiElement: PsiElement, state: HtmlParserState): HtmlParsingResult { + val text = psiElement.text + + val openPre = state.openPreTags + preOpeningTagRegex.findAll(text).count() + val closedPre = state.closedPreTags + preClosingTagRegex.findAll(text).count() + val isInsidePre = openPre > closedPre + + val parsed = when (psiElement) { + is PsiInlineDocTag -> psiElement.toHtml(state.currentJavadocTag) + is PsiDocParamRef -> psiElement.toDocumentationLinkString() + is PsiDocTagValue, is LeafPsiElement -> { + psiElement.stringifyElementAsText(isInsidePre, state.previousElement) + } + else -> null + } + val previousElement = if (text.trim() == "") state.previousElement else psiElement + return HtmlParsingResult( + state.copy( + previousElement = previousElement, + closedPreTags = closedPre, + openPreTags = openPre + ), parsed + ) + } + + /** + * Inline tags can be met in the middle of some text. Example of an inline tag usage: + * + * ```java + * Use the {@link #getComponentAt(int, int) getComponentAt} method. + * ``` + */ + private fun PsiInlineDocTag.toHtml(javadocTag: JavadocTag?): String? = + when (this.name) { + "link", "linkplain" -> this.referenceElement() + ?.toDocumentationLinkString(this.dataElements.filterIsInstance<PsiDocToken>().joinToString(" ") { + it.stringifyElementAsText(keepFormatting = false).orEmpty() + }) + + "code" -> "<code data-inline>${dataElementsAsText(this)}</code>" + "literal" -> "<literal>${dataElementsAsText(this)}</literal>" + "index" -> "<index>${this.children.filterIsInstance<PsiDocTagValue>().joinToString { it.text }}</index>" + "inheritDoc" -> { + val inheritDocContent = inheritDocTagResolver.resolveContent(commentResolutionContext) + val html = inheritDocContent?.fold(HtmlParsingResult(javadocTag)) { result, content -> + result + content.toInheritDocHtml(result.newState, docTagParserContext) + }?.parsedLine.orEmpty() + html + } + + else -> this.text + } + + private fun DocumentationContent.toInheritDocHtml( + parserState: HtmlParserState, + docTagParserContext: DocTagParserContext + ): HtmlParsingResult { + // TODO [beresnev] comment + return if (this is PsiDocumentationContent) { + parseHtml(this.psiElement, parserState) + } else { + HtmlParsingResult(parserState, inheritDocTagResolver.convertToHtml(this, docTagParserContext)) + } + } + + private fun dataElementsAsText(tag: PsiInlineDocTag): String { + return tag.dataElements.joinToString("") { + it.stringifyElementAsText(keepFormatting = true).orEmpty() + }.htmlEscape() + } + + private fun PsiElement.toDocumentationLinkString(label: String = ""): String { + val driId = reference?.resolve()?.takeIf { it !is PsiParameter }?.let { + val dri = DRI.from(it) + val id = docTagParserContext.store(dri) + id + } ?: UNRESOLVED_PSI_ELEMENT // TODO [beresnev] log this somewhere maybe? + + // TODO [beresnev] data-dri into a constant + return """<a data-dri="${driId.htmlEscape()}">${label.ifBlank { defaultLabel().text }}</a>""" + } + } +} + +private fun PsiElement.stringifyElementAsText(keepFormatting: Boolean, previousElement: PsiElement? = null) = + if (keepFormatting) { + /* + For values in the <pre> tag we try to keep formatting, so only the leading space is trimmed, + since it is there because it separates this line from the leading asterisk + */ + text.let { + if (((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true || (prevSibling as? PsiDocToken)?.isTagName() == true) && it.firstOrNull() == ' ') + it.drop(1) else it + }.let { + if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it.dropLastWhile { it == ' ' } else it + } + } else { + /* + Outside of the <pre> we would like to trim everything from the start and end of a line since + javadoc doesn't care about it. + */ + text.let { + if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text.isNotBlank() && previousElement !is PsiInlineDocTag) it?.trimStart() else it + }?.let { + if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text.isNotBlank()) it.trimEnd() else it + }?.let { + if (shouldHaveSpaceAtTheEnd()) "$it " else it + } + } + +private fun PsiDocToken.isLeadingAsterisk() = tokenType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS + +private fun PsiDocToken.isTagName() = tokenType == JavaDocTokenType.DOC_TAG_NAME + +/** + * We would like to know if we need to have a space after a this tag + * + * The space is required when: + * - tag spans multiple lines, between every line we would need a space + * + * We wouldn't like to render a space if: + * - tag is followed by an end of comment + * - after a tag there is another tag (eg. multiple @author tags) + * - they end with an html tag like: <a href="...">Something</a> since then the space will be displayed in the following text + * - next line starts with a <p> or <pre> token + */ +private fun PsiElement.shouldHaveSpaceAtTheEnd(): Boolean { + val siblings = siblings(withItself = false).toList().filterNot { it.text.trim() == "" } + val nextNotEmptySibling = (siblings.firstOrNull() as? PsiDocToken) + val furtherNotEmptySibling = + (siblings.drop(1).firstOrNull { it is PsiDocToken && !it.isLeadingAsterisk() } as? PsiDocToken) + val lastHtmlTag = text.trim().substringAfterLast("<") + val endsWithAnUnclosedTag = lastHtmlTag.endsWith(">") && !lastHtmlTag.startsWith("</") + + return (nextSibling as? PsiWhiteSpace)?.text?.startsWith("\n ") == true && + (getNextSiblingIgnoringWhitespace() as? PsiDocToken)?.tokenType != JavaDocTokenTypes.INSTANCE.commentEnd() && + nextNotEmptySibling?.isLeadingAsterisk() == true && + furtherNotEmptySibling?.tokenType == JavaDocTokenTypes.INSTANCE.commentData() && + !endsWithAnUnclosedTag +} + + diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt new file mode 100644 index 00000000..d8702336 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.analysis.java.util + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.AncestryNode +import org.jetbrains.dokka.model.TypeConstructor + +// TODO [beresnev] copy-pasted +internal fun AncestryNode.typeConstructorsBeingExceptions(): List<TypeConstructor> { + fun traverseSupertypes(ancestry: AncestryNode): List<TypeConstructor> = + listOf(ancestry.typeConstructor) + (ancestry.superclass?.let(::traverseSupertypes) ?: emptyList()) + + return superclass?.let(::traverseSupertypes)?.filter { type -> type.dri.isDirectlyAnException() } ?: emptyList() +} + +// TODO [beresnev] copy-pasted +internal fun DRI.isDirectlyAnException(): Boolean = + toString().let { stringed -> + stringed == "kotlin/Exception///PointingToDeclaration/" || + stringed == "java.lang/Exception///PointingToDeclaration/" + } diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt new file mode 100644 index 00000000..82482e35 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt @@ -0,0 +1,43 @@ +package org.jetbrains.dokka.analysis.java.util + +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.DefaultLogger +import com.intellij.openapi.diagnostic.Logger + +internal class NoopIntellijLoggerFactory : Logger.Factory { + override fun getLoggerInstance(p0: String): Logger = NoopIntellijLogger +} + +/** + * Ignores all messages passed to it + */ +internal object NoopIntellijLogger : DefaultLogger(null) { + override fun isDebugEnabled(): Boolean = false + override fun isTraceEnabled(): Boolean = false + + override fun debug(message: String?) {} + override fun debug(t: Throwable?) {} + override fun debug(message: String?, t: Throwable?) {} + override fun debug(message: String, vararg details: Any?) {} + override fun debugValues(header: String, values: MutableCollection<*>) {} + + override fun trace(message: String?) {} + override fun trace(t: Throwable?) {} + + override fun info(message: String?) {} + override fun info(message: String?, t: Throwable?) {} + override fun info(t: Throwable) {} + + override fun warn(message: String?, t: Throwable?) {} + override fun warn(message: String?) {} + override fun warn(t: Throwable) {} + + override fun error(message: String?, t: Throwable?, vararg details: String?) {} + override fun error(message: String?) {} + override fun error(message: Any?) {} + override fun error(message: String?, vararg attachments: Attachment?) {} + override fun error(message: String?, t: Throwable?, vararg attachments: Attachment?) {} + override fun error(message: String?, vararg details: String?) {} + override fun error(message: String?, t: Throwable?) {} + override fun error(t: Throwable) {} +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt new file mode 100644 index 00000000..137f0792 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt @@ -0,0 +1,101 @@ +package org.jetbrains.dokka.analysis.java.util + +// TODO [beresnev] copy-paste + +internal fun propertyNamesBySetMethodName(methodName: String): List<String> = + listOfNotNull(propertyNameBySetMethodName(methodName, false), propertyNameBySetMethodName(methodName, true)) + +internal fun propertyNameByGetMethodName(methodName: String): String? = + propertyNameFromAccessorMethodName(methodName, "get") ?: propertyNameFromAccessorMethodName(methodName, "is", removePrefix = false) + +private fun propertyNameBySetMethodName(methodName: String, withIsPrefix: Boolean): String? = + propertyNameFromAccessorMethodName(methodName, "set", addPrefix = if (withIsPrefix) "is" else null) + +private fun propertyNameFromAccessorMethodName( + methodName: String, + prefix: String, + removePrefix: Boolean = true, + addPrefix: String? = null +): String? { + val isSpecial = methodName.startsWith("<") // see special in org.jetbrains.kotlin.Name + if (isSpecial) return null + if (!methodName.startsWith(prefix)) return null + if (methodName.length == prefix.length) return null + if (methodName[prefix.length] in 'a'..'z') return null + + if (addPrefix != null) { + assert(removePrefix) + return addPrefix + methodName.removePrefix(prefix) + } + + if (!removePrefix) return methodName + val name = methodName.removePrefix(prefix).decapitalizeSmartForCompiler(asciiOnly = true) + if (!isValidIdentifier(name)) return null + return name +} + +/** + * "FooBar" -> "fooBar" + * "FOOBar" -> "fooBar" + * "FOO" -> "foo" + * "FOO_BAR" -> "foO_BAR" + */ +private fun String.decapitalizeSmartForCompiler(asciiOnly: Boolean = false): String { + if (isEmpty() || !isUpperCaseCharAt(0, asciiOnly)) return this + + if (length == 1 || !isUpperCaseCharAt(1, asciiOnly)) { + return if (asciiOnly) decapitalizeAsciiOnly() else replaceFirstChar(Char::lowercaseChar) + } + + val secondWordStart = (indices.firstOrNull { !isUpperCaseCharAt(it, asciiOnly) } ?: return toLowerCase(this, asciiOnly)) - 1 + + return toLowerCase(substring(0, secondWordStart), asciiOnly) + substring(secondWordStart) +} + +private fun String.isUpperCaseCharAt(index: Int, asciiOnly: Boolean): Boolean { + val c = this[index] + return if (asciiOnly) c in 'A'..'Z' else c.isUpperCase() +} + +private fun toLowerCase(string: String, asciiOnly: Boolean): String { + return if (asciiOnly) string.toLowerCaseAsciiOnly() else string.lowercase() +} + +private fun toUpperCase(string: String, asciiOnly: Boolean): String { + return if (asciiOnly) string.toUpperCaseAsciiOnly() else string.uppercase() +} + +private fun String.decapitalizeAsciiOnly(): String { + if (isEmpty()) return this + val c = this[0] + return if (c in 'A'..'Z') + c.lowercaseChar() + substring(1) + else + this +} + +private fun String.toLowerCaseAsciiOnly(): String { + val builder = StringBuilder(length) + for (c in this) { + builder.append(if (c in 'A'..'Z') c.lowercaseChar() else c) + } + return builder.toString() +} + +private fun String.toUpperCaseAsciiOnly(): String { + val builder = StringBuilder(length) + for (c in this) { + builder.append(if (c in 'a'..'z') c.uppercaseChar() else c) + } + return builder.toString() +} + +private fun isValidIdentifier(name: String): Boolean { + if (name.isEmpty() || name.startsWith("<")) return false + for (element in name) { + if (element == '.' || element == '/' || element == '\\') { + return false + } + } + return true +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt new file mode 100644 index 00000000..1424244d --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt @@ -0,0 +1,98 @@ +package org.jetbrains.dokka.analysis.java.util + +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import org.jetbrains.dokka.analysis.java.getVisibility +import org.jetbrains.dokka.model.JavaVisibility +import org.jetbrains.dokka.model.KotlinVisibility +import org.jetbrains.dokka.model.Visibility + + +internal data class PsiFunctionsHolder( + val regularFunctions: List<PsiMethod>, + val accessors: Map<PsiField, List<PsiMethod>> +) + +internal fun splitFunctionsAndAccessors(fields: Array<PsiField>, methods: Array<PsiMethod>): PsiFunctionsHolder { + val fieldsByName = fields.associateBy { it.name } + val regularFunctions = mutableListOf<PsiMethod>() + val accessors = mutableMapOf<PsiField, MutableList<PsiMethod>>() + methods.forEach { method -> + val possiblePropertyNamesForFunction = method.getPossiblePropertyNamesForFunction() + val field = possiblePropertyNamesForFunction.firstNotNullOfOrNull { fieldsByName[it] } + if (field != null && method.isAccessorFor(field)) { + accessors.getOrPut(field, ::mutableListOf).add(method) + } else { + regularFunctions.add(method) + } + } + + val accessorLookalikes = removeNonAccessorsReturning(accessors) + regularFunctions.addAll(accessorLookalikes) + + return PsiFunctionsHolder(regularFunctions, accessors) +} + +private fun PsiMethod.getPossiblePropertyNamesForFunction(): List<String> { + val jvmName = getAnnotation("kotlin.jvm.JvmName")?.findAttributeValue("name")?.text + if (jvmName != null) return listOf(jvmName) + + return when { + isGetterName(name) -> listOfNotNull( + propertyNameByGetMethodName(name) + ) + isSetterName(name) -> { + propertyNamesBySetMethodName(name) + } + else -> listOf() + } +} + +private fun isGetterName(name: String): Boolean { + return name.startsWith("get") || name.startsWith("is") +} + +private fun isSetterName(name: String): Boolean { + return name.startsWith("set") +} + +/** + * If a field has no getter, it's not accessible as a property from Kotlin's perspective, + * but it still might have a setter. In this case, this "setter" should be just a regular function + */ +private fun removeNonAccessorsReturning( + fieldAccessors: MutableMap<PsiField, MutableList<PsiMethod>> +): List<PsiMethod> { + val nonAccessors = mutableListOf<PsiMethod>() + fieldAccessors.entries.removeIf { (field, methods) -> + if (methods.size == 1 && methods[0].isSetterFor(field)) { + nonAccessors.add(methods[0]) + true + } else { + false + } + } + return nonAccessors +} + +internal fun PsiMethod.isAccessorFor(field: PsiField): Boolean { + return (this.isGetterFor(field) || this.isSetterFor(field)) + && !field.getVisibility().isPublicAPI() + && this.getVisibility().isPublicAPI() +} + +internal fun PsiMethod.isGetterFor(field: PsiField): Boolean { + return this.returnType == field.type && !this.hasParameters() +} + +internal fun PsiMethod.isSetterFor(field: PsiField): Boolean { + return parameterList.getParameter(0)?.type == field.type && parameterList.getParametersCount() == 1 +} + +private fun Visibility.isPublicAPI() = when(this) { + KotlinVisibility.Public, + KotlinVisibility.Protected, + JavaVisibility.Public, + JavaVisibility.Protected -> true + else -> false +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt new file mode 100644 index 00000000..10bb79c7 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt @@ -0,0 +1,49 @@ +package org.jetbrains.dokka.analysis.java.util + +import com.intellij.psi.JavaDocTokenType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiJavaCodeReferenceElement +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.impl.source.tree.JavaDocElementType +import com.intellij.psi.javadoc.PsiDocComment +import com.intellij.psi.javadoc.PsiDocTag +import com.intellij.psi.javadoc.PsiDocToken +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.analysis.java.DescriptionJavadocTag +import org.jetbrains.dokka.analysis.java.JavadocTag + +internal fun PsiDocComment.hasTag(tag: JavadocTag): Boolean = + when (tag) { + DescriptionJavadocTag -> descriptionElements.isNotEmpty() + else -> findTagByName(tag.name) != null + } + +internal fun PsiDocTag.contentElementsWithSiblingIfNeeded(): List<PsiElement> = if (dataElements.isNotEmpty()) { + listOfNotNull( + dataElements[0], + dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text }, + *dataElements.drop(1).toTypedArray() + ) +} else { + emptyList() +} + +internal fun PsiDocTag.resolveToElement(): PsiElement? = + dataElements.firstOrNull()?.firstChild?.referenceElementOrSelf()?.resolveToGetDri() + +internal fun PsiDocTag.referenceElement(): PsiElement? = + linkElement()?.referenceElementOrSelf() + +internal fun PsiElement.referenceElementOrSelf(): PsiElement? = + if (node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { + PsiTreeUtil.findChildOfType(this, PsiJavaCodeReferenceElement::class.java) + } else this + +internal fun PsiDocTag.linkElement(): PsiElement? = + valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } + +internal fun PsiElement.defaultLabel() = children.firstOrNull { + it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken() +} ?: this + +internal fun PsiDocToken.isSharpToken() = tokenType == JavaDocTokenType.DOC_TAG_VALUE_SHARP_TOKEN diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt new file mode 100644 index 00000000..ed58eb56 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt @@ -0,0 +1,119 @@ +package org.jetbrains.dokka.analysis.java.util + +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.model.DocumentableSource +import org.jetbrains.dokka.utilities.firstIsInstanceOrNull + +// TODO [beresnev] copy-paste + +internal val PsiElement.parentsWithSelf: Sequence<PsiElement> + get() = generateSequence(this) { if (it is PsiFile) null else it.parent } + +internal fun DRI.Companion.from(psi: PsiElement) = psi.parentsWithSelf.run { + val psiMethod = firstIsInstanceOrNull<PsiMethod>() + val psiField = firstIsInstanceOrNull<PsiField>() + val classes = filterIsInstance<PsiClass>().filterNot { it is PsiTypeParameter } + .toList() // We only want exact PsiClass types, not PsiTypeParameter subtype + val additionalClasses = if (psi is PsiEnumConstant) listOfNotNull(psiField?.name) else emptyList() + DRI( + packageName = classes.lastOrNull()?.qualifiedName?.substringBeforeLast('.', "") ?: "", + classNames = (additionalClasses + classes.mapNotNull { it.name }).takeIf { it.isNotEmpty() } + ?.asReversed()?.joinToString("."), + // The fallback strategy test whether psi is not `PsiEnumConstant`. The reason behind this is that + // we need unified DRI for both Java and Kotlin enums, so we can link them properly and treat them alike. + // To achieve that, we append enum name to classNames list and leave the callable part set to null. For Kotlin enums + // it is by default, while for Java enums we have to explicitly test for that in this `takeUnless` condition. + callable = psiMethod?.let { Callable.from(it) } ?: psiField?.takeUnless { psi is PsiEnumConstant }?.let { Callable.from(it) }, + target = DriTarget.from(psi), + extra = if (psi is PsiEnumConstant) + DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode() + else null + ) +} + +internal fun Callable.Companion.from(psi: PsiMethod) = with(psi) { + Callable( + name, + null, + parameterList.parameters.map { param -> JavaClassReference(param.type.canonicalText) }) +} + +internal fun Callable.Companion.from(psi: PsiField): Callable { + return Callable( + name = psi.name, + receiver = null, + params = emptyList() + ) +} + +internal fun DriTarget.Companion.from(psi: PsiElement): DriTarget = psi.parentsWithSelf.run { + return when (psi) { + is PsiTypeParameter -> PointingToGenericParameters(psi.index) + else -> firstIsInstanceOrNull<PsiParameter>()?.let { + val callable = firstIsInstanceOrNull<PsiMethod>() + val params = (callable?.parameterList?.parameters).orEmpty() + PointingToCallableParameters(params.indexOf(it)) + } ?: PointingToDeclaration + } +} + +// TODO [beresnev] copy-paste +internal fun PsiElement.siblings(forward: Boolean = true, withItself: Boolean = true): Sequence<PsiElement> { + return object : Sequence<PsiElement> { + override fun iterator(): Iterator<PsiElement> { + var next: PsiElement? = this@siblings + return object : Iterator<PsiElement> { + init { + if (!withItself) next() + } + + override fun hasNext(): Boolean = next != null + override fun next(): PsiElement { + val result = next ?: throw NoSuchElementException() + next = if (forward) result.nextSibling else result.prevSibling + return result + } + } + } + } +} + +// TODO [beresnev] copy-paste +internal fun PsiElement.getNextSiblingIgnoringWhitespace(withItself: Boolean = false): PsiElement? { + return siblings(withItself = withItself).filter { it !is PsiWhiteSpace }.firstOrNull() +} + +@InternalDokkaApi +class PsiDocumentableSource(val psi: PsiNamedElement) : DocumentableSource { + override val path = psi.containingFile.virtualFile.path + + override fun computeLineNumber(): Int? { + val range = psi.getChildOfType<PsiIdentifier>()?.textRange ?: psi.textRange + val doc = PsiDocumentManager.getInstance(psi.project).getDocument(psi.containingFile) + // IJ uses 0-based line-numbers; external source browsers use 1-based + return doc?.getLineNumber(range.startOffset)?.plus(1) + } +} + +inline fun <reified T : PsiElement> PsiElement.getChildOfType(): T? { + return PsiTreeUtil.getChildOfType(this, T::class.java) +} + +internal fun PsiElement.getKotlinFqName(): String? = this.kotlinFqNameProp + +//// from import org.jetbrains.kotlin.idea.base.psi.kotlinFqName +internal val PsiElement.kotlinFqNameProp: String? + get() = when (val element = this) { + is PsiPackage -> element.qualifiedName + is PsiClass -> element.qualifiedName + is PsiMember -> element.name?.let { name -> + val prefix = element.containingClass?.qualifiedName + if (prefix != null) "$prefix.$name" else name + } +// is KtNamedDeclaration -> element.fqName TODO [beresnev] decide what to do with it + is PsiQualifiedNamedElement -> element.qualifiedName + else -> null + } diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt new file mode 100644 index 00000000..cce76ce6 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt @@ -0,0 +1,33 @@ +package org.jetbrains.dokka.analysis.java.util + +import java.util.* + +// TODO [beresnev] copy-paste + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal fun <T, R : Any> Iterable<T>.firstNotNullOfOrNull(transform: (T) -> R?): R? { + for (element in this) { + val result = transform(element) + if (result != null) { + return result + } + } + return null +} + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal fun Char.uppercaseChar(): Char = Character.toUpperCase(this) + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal fun Char.lowercaseChar(): Char = Character.toLowerCase(this) + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal fun String.lowercase(): String = this.toLowerCase(Locale.ROOT) + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal fun String.uppercase(): String = this.toUpperCase(Locale.ROOT) + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal fun String.replaceFirstChar(transform: (Char) -> Char): String { + return if (isNotEmpty()) transform(this[0]) + substring(1) else this +} diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt new file mode 100644 index 00000000..2972c009 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.analysis.java.util + +import com.intellij.psi.PsiElement + +// TODO [beresnev] get rid of +internal fun PsiElement.resolveToGetDri(): PsiElement? = + reference?.resolve() diff --git a/subprojects/analysis-java-psi/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-java-psi/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..51d36899 --- /dev/null +++ b/subprojects/analysis-java-psi/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin diff --git a/subprojects/analysis-kotlin-api/README.md b/subprojects/analysis-kotlin-api/README.md new file mode 100644 index 00000000..5b03b297 --- /dev/null +++ b/subprojects/analysis-kotlin-api/README.md @@ -0,0 +1,10 @@ +# Analysis: Kotlin API + +Public API for interacting with Kotlin analysis, regardless of implementation. Contains no business logic. + +Can be used to request additional information about Kotlin declarations. + +Has to be used as a `compileOnly` dependency as Dokka bundles it by default in all runners. + +The actual implementation (K1/K2/etc) will be resolved and bootstrapped during runtime, so the +user must not think about it. diff --git a/subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api b/subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api new file mode 100644 index 00000000..74bd7da9 --- /dev/null +++ b/subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api @@ -0,0 +1,70 @@ +public final class org/jetbrains/kotlin/analysis/kotlin/KotlinAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V +} + +public final class org/jetbrains/kotlin/analysis/kotlin/internal/DocumentableLanguage : java/lang/Enum { + public static final field JAVA Lorg/jetbrains/kotlin/analysis/kotlin/internal/DocumentableLanguage; + public static final field KOTLIN Lorg/jetbrains/kotlin/analysis/kotlin/internal/DocumentableLanguage; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/kotlin/analysis/kotlin/internal/DocumentableLanguage; + public static fun values ()[Lorg/jetbrains/kotlin/analysis/kotlin/internal/DocumentableLanguage; +} + +public abstract interface class org/jetbrains/kotlin/analysis/kotlin/internal/DocumentableSourceLanguageParser { + public abstract fun getLanguage (Lorg/jetbrains/dokka/model/Documentable;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Lorg/jetbrains/kotlin/analysis/kotlin/internal/DocumentableLanguage; +} + +public abstract interface class org/jetbrains/kotlin/analysis/kotlin/internal/ExternalDocumentablesProvider { + public abstract fun findClasslike (Lorg/jetbrains/dokka/links/DRI;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Lorg/jetbrains/dokka/model/DClasslike; +} + +public abstract interface class org/jetbrains/kotlin/analysis/kotlin/internal/FullClassHierarchyBuilder { + public abstract fun build (Lorg/jetbrains/dokka/model/DModule;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class org/jetbrains/kotlin/analysis/kotlin/internal/InheritanceBuilder { + public abstract fun build (Ljava/util/Map;)Ljava/util/List; +} + +public final class org/jetbrains/kotlin/analysis/kotlin/internal/InheritanceNode { + public fun <init> (Lorg/jetbrains/dokka/links/DRI;Ljava/util/List;Ljava/util/List;Z)V + public synthetic fun <init> (Lorg/jetbrains/dokka/links/DRI;Ljava/util/List;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lorg/jetbrains/dokka/links/DRI; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Z + public final fun copy (Lorg/jetbrains/dokka/links/DRI;Ljava/util/List;Ljava/util/List;Z)Lorg/jetbrains/kotlin/analysis/kotlin/internal/InheritanceNode; + public static synthetic fun copy$default (Lorg/jetbrains/kotlin/analysis/kotlin/internal/InheritanceNode;Lorg/jetbrains/dokka/links/DRI;Ljava/util/List;Ljava/util/List;ZILjava/lang/Object;)Lorg/jetbrains/kotlin/analysis/kotlin/internal/InheritanceNode; + public fun equals (Ljava/lang/Object;)Z + public final fun getChildren ()Ljava/util/List; + public final fun getDri ()Lorg/jetbrains/dokka/links/DRI; + public final fun getInterfaces ()Ljava/util/List; + public fun hashCode ()I + public final fun isInterface ()Z + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/kotlin/analysis/kotlin/internal/InternalKotlinAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V + public final fun getDocumentableSourceLanguageParser ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getExternalDocumentablesProvider ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getFullClassHierarchyBuilder ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getInheritanceBuilder ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getKotlinToJavaService ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getModuleAndPackageDocumentationReader ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getSyntheticDocumentableDetector ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; +} + +public abstract interface class org/jetbrains/kotlin/analysis/kotlin/internal/KotlinToJavaService { + public abstract fun findAsJava (Lorg/jetbrains/dokka/links/DRI;)Lorg/jetbrains/dokka/links/DRI; +} + +public abstract interface class org/jetbrains/kotlin/analysis/kotlin/internal/ModuleAndPackageDocumentationReader { + public abstract fun read (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaModuleDescription;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; + public abstract fun read (Lorg/jetbrains/dokka/model/DModule;)Ljava/util/Map; + public abstract fun read (Lorg/jetbrains/dokka/model/DPackage;)Ljava/util/Map; +} + +public abstract interface class org/jetbrains/kotlin/analysis/kotlin/internal/SyntheticDocumentableDetector { + public abstract fun isSynthetic (Lorg/jetbrains/dokka/model/Documentable;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Z +} + diff --git a/subprojects/analysis-kotlin-api/build.gradle.kts b/subprojects/analysis-kotlin-api/build.gradle.kts new file mode 100644 index 00000000..3a10ff55 --- /dev/null +++ b/subprojects/analysis-kotlin-api/build.gradle.kts @@ -0,0 +1,14 @@ +import org.jetbrains.registerDokkaArtifactPublication + +plugins { + id("org.jetbrains.conventions.kotlin-jvm") + id("org.jetbrains.conventions.maven-publish") +} + +dependencies { + compileOnly(projects.core) +} + +registerDokkaArtifactPublication("analysisKotlinApi") { + artifactId = "analysis-kotlin-api" +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/KotlinAnalysisPlugin.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/KotlinAnalysisPlugin.kt new file mode 100644 index 00000000..3138e17b --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/KotlinAnalysisPlugin.kt @@ -0,0 +1,17 @@ +package org.jetbrains.kotlin.analysis.kotlin + +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement + +class KotlinAnalysisPlugin : DokkaPlugin() { + + /* + * This is where stable public API will go. + * + * No stable public API for now. + */ + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/DocumentableSourceLanguageParser.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/DocumentableSourceLanguageParser.kt new file mode 100644 index 00000000..08a465c0 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/DocumentableSourceLanguageParser.kt @@ -0,0 +1,16 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.WithSources + +@InternalDokkaApi +enum class DocumentableLanguage { + JAVA, KOTLIN +} + +@InternalDokkaApi +interface DocumentableSourceLanguageParser { + fun getLanguage(documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet): DocumentableLanguage? +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/ExternalDocumentablesProvider.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/ExternalDocumentablesProvider.kt new file mode 100644 index 00000000..ea418fba --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/ExternalDocumentablesProvider.kt @@ -0,0 +1,24 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DClasslike + +/** + * Service that can be queried with [DRI] and source set to obtain a documentable for classlike. + * + * There are some cases when there is a need to process documentables of classlikes that were not defined + * in the project itself but are somehow related to the symbols defined in the documented project (e.g. are supertypes + * of classes defined in project). + */ +@InternalDokkaApi +fun interface ExternalDocumentablesProvider { + + /** + * Returns [DClasslike] matching provided [DRI] in specified source set. + * + * Result is null if compiler haven't generated matching class descriptor. + */ + fun findClasslike(dri: DRI, sourceSet: DokkaConfiguration.DokkaSourceSet): DClasslike? +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/FullClassHierarchyBuilder.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/FullClassHierarchyBuilder.kt new file mode 100644 index 00000000..91460002 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/FullClassHierarchyBuilder.kt @@ -0,0 +1,17 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.SourceSetDependent + +@InternalDokkaApi +typealias Supertypes = List<DRI> + +@InternalDokkaApi +typealias ClassHierarchy = SourceSetDependent<Map<DRI, Supertypes>> + +@InternalDokkaApi +interface FullClassHierarchyBuilder { + suspend fun build(module: DModule): ClassHierarchy +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/InheritanceBuilder.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/InheritanceBuilder.kt new file mode 100644 index 00000000..7f0313ea --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/InheritanceBuilder.kt @@ -0,0 +1,21 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Documentable + +@InternalDokkaApi +interface InheritanceBuilder { + fun build(documentables: Map<DRI, Documentable>): List<InheritanceNode> +} + +@InternalDokkaApi +data class InheritanceNode( + val dri: DRI, + val children: List<InheritanceNode> = emptyList(), + val interfaces: List<DRI> = emptyList(), + val isInterface: Boolean = false +) { + override fun equals(other: Any?): Boolean = other is InheritanceNode && other.dri == dri + override fun hashCode(): Int = dri.hashCode() +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/InternalKotlinAnalysisPlugin.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/InternalKotlinAnalysisPlugin.kt new file mode 100644 index 00000000..e2335474 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/InternalKotlinAnalysisPlugin.kt @@ -0,0 +1,31 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement + +/** + * A plugin for internal use, has no stable public API and thus must not be used by third party, + * external plugins. If you need any of the given API stabilized, please create an issue describing your use case. + */ +@InternalDokkaApi +class InternalKotlinAnalysisPlugin : DokkaPlugin() { + + val fullClassHierarchyBuilder by extensionPoint<FullClassHierarchyBuilder>() + + val syntheticDocumentableDetector by extensionPoint<SyntheticDocumentableDetector>() + + val moduleAndPackageDocumentationReader by extensionPoint<ModuleAndPackageDocumentationReader>() + + val kotlinToJavaService by extensionPoint<KotlinToJavaService>() + + val inheritanceBuilder by extensionPoint<InheritanceBuilder>() + + val externalDocumentablesProvider by extensionPoint<ExternalDocumentablesProvider>() + + val documentableSourceLanguageParser by extensionPoint<DocumentableSourceLanguageParser>() + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/KotlinToJavaService.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/KotlinToJavaService.kt new file mode 100644 index 00000000..3631fac2 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/KotlinToJavaService.kt @@ -0,0 +1,9 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.links.DRI + +@InternalDokkaApi +interface KotlinToJavaService { + fun findAsJava(kotlinDri: DRI): DRI? +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/ModuleAndPackageDocumentationReader.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/ModuleAndPackageDocumentationReader.kt new file mode 100644 index 00000000..6e641c3a --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/ModuleAndPackageDocumentationReader.kt @@ -0,0 +1,15 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.DocumentationNode + +@InternalDokkaApi +interface ModuleAndPackageDocumentationReader { + fun read(module: DModule): SourceSetDependent<DocumentationNode> + fun read(pkg: DPackage): SourceSetDependent<DocumentationNode> + fun read(module: DokkaConfiguration.DokkaModuleDescription): DocumentationNode? +} diff --git a/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/SyntheticDocumentableDetector.kt b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/SyntheticDocumentableDetector.kt new file mode 100644 index 00000000..3dc8afa5 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/kotlin/analysis/kotlin/internal/SyntheticDocumentableDetector.kt @@ -0,0 +1,11 @@ +package org.jetbrains.kotlin.analysis.kotlin.internal + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.model.Documentable + +// TODO [beresnev] isSynthetic could be a property of Documentable +@InternalDokkaApi +interface SyntheticDocumentableDetector { + fun isSynthetic(documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet): Boolean +} diff --git a/subprojects/analysis-kotlin-descriptors/README.md b/subprojects/analysis-kotlin-descriptors/README.md new file mode 100644 index 00000000..fbfd1c8b --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/README.md @@ -0,0 +1,8 @@ +# Analysis: Kotlin descriptors + +An internal descriptor-based implementation for [analysis-kotlin-api](../analysis-kotlin-api), also known as K1 or +"the old 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. diff --git a/subprojects/analysis-kotlin-descriptors/api/analysis-kotlin-descriptors.api b/subprojects/analysis-kotlin-descriptors/api/analysis-kotlin-descriptors.api new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/api/analysis-kotlin-descriptors.api diff --git a/subprojects/analysis-kotlin-descriptors/build.gradle.kts b/subprojects/analysis-kotlin-descriptors/build.gradle.kts new file mode 100644 index 00000000..3f028090 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/build.gradle.kts @@ -0,0 +1,43 @@ +import org.jetbrains.DokkaPublicationBuilder +import org.jetbrains.registerDokkaArtifactPublication + +plugins { + id("org.jetbrains.conventions.kotlin-jvm") + id("org.jetbrains.conventions.maven-publish") + id("com.github.johnrengelman.shadow") +} + +dependencies { + implementation(projects.subprojects.analysisKotlinApi) + implementation(projects.subprojects.analysisKotlinDescriptors.compiler) + implementation(projects.subprojects.analysisKotlinDescriptors.ide) +} + +tasks { + // There are several reasons for shadowing all dependencies in one place: + // 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. + // 3. The compiler and ide modules are internal details that are likely to change, so packing everything into + // a single jar provides some stability for the CLI users, while not exposing too many internals. Publishing + // the compiler, ide and other subprojects separately would make it difficult to refactor the project structure. + shadowJar { + val dokka_version: String by project + + // cannot be named exactly like the artifact (i.e analysis-kotlin-descriptors-VER.jar), + // otherwise leads to obscure test failures when run via CLI, but not via IJ + archiveFileName.set("analysis-kotlin-descriptors-all-$dokka_version.jar") + archiveClassifier.set("") + + // service files are merged to make sure all Dokka plugins + // from the dependencies are loaded, and not just a single one. + mergeServiceFiles() + } +} + +registerDokkaArtifactPublication("analysisKotlinDescriptors") { + artifactId = "analysis-kotlin-descriptors" + component = DokkaPublicationBuilder.Component.Shadow +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/README.md b/subprojects/analysis-kotlin-descriptors/compiler/README.md new file mode 100644 index 00000000..5676fbf2 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/README.md @@ -0,0 +1,9 @@ +# Descriptors: compiler + +An internal module that encapsulates external compiler (`org.jetbrains.kotlin:kotlin-compiler`) dependencies. + +Parses Kotlin sources. + +Exists primarily to make sure that unreliable and coupled external dependencies are somewhat abstracted away, +otherwise everything gets tangled together and breaking changes in such dependencies become very +difficult to resolve. diff --git a/subprojects/analysis-kotlin-descriptors/compiler/api/compiler.api b/subprojects/analysis-kotlin-descriptors/compiler/api/compiler.api new file mode 100644 index 00000000..6c642ad6 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/api/compiler.api @@ -0,0 +1,68 @@ +public abstract interface class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/AnalysisContextCreator { + public abstract fun create (Lcom/intellij/mock/MockProject;Lorg/jetbrains/kotlin/descriptors/ModuleDescriptor;Lorg/jetbrains/kotlin/analyzer/ResolverForModule;Lorg/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment;Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisEnvironment;)Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisContext; +} + +public final class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V + public final fun getAnalysisContextCreator ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getCompilerExtensionPointProvider ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getDescriptorFinder ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getKdocFinder ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getKlibService ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getKotlinAnalysis ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getMockApplicationHack ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; +} + +public abstract interface class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerExtensionPointProvider { + public abstract fun get ()Ljava/util/List; +} + +public final class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerExtensionPointProvider$CompilerExtensionPoint { + public fun <init> (Lorg/jetbrains/kotlin/extensions/ApplicationExtensionDescriptor;Ljava/util/List;)V + public final fun getExtensionDescriptor ()Lorg/jetbrains/kotlin/extensions/ApplicationExtensionDescriptor; + public final fun getExtensions ()Ljava/util/List; +} + +public abstract interface class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/DescriptorFinder { + public abstract fun findDescriptor (Lorg/jetbrains/kotlin/psi/KtDeclaration;)Lorg/jetbrains/kotlin/descriptors/DeclarationDescriptor; +} + +public abstract interface class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KDocFinder { + public abstract fun find (Lorg/jetbrains/kotlin/descriptors/DeclarationDescriptor;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlin/kdoc/psi/impl/KDocTag; + public abstract fun findKDoc (Lorg/jetbrains/kotlin/psi/KtElement;)Lorg/jetbrains/kotlin/kdoc/psi/impl/KDocTag; + public abstract fun resolveKDocLink (Lorg/jetbrains/kotlin/descriptors/DeclarationDescriptor;Ljava/lang/String;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Z)Ljava/util/Collection; +} + +public final class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KDocFinder$DefaultImpls { + public static synthetic fun find$default (Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KDocFinder;Lorg/jetbrains/kotlin/descriptors/DeclarationDescriptor;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/kotlin/kdoc/psi/impl/KDocTag; + public static synthetic fun resolveKDocLink$default (Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KDocFinder;Lorg/jetbrains/kotlin/descriptors/DeclarationDescriptor;Ljava/lang/String;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;ZILjava/lang/Object;)Ljava/util/Collection; +} + +public abstract interface class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KLibService { + public abstract fun createPackageFragmentProvider (Lorg/jetbrains/kotlin/library/KotlinLibrary;Lorg/jetbrains/kotlin/storage/StorageManager;Lorg/jetbrains/kotlin/backend/common/serialization/metadata/KlibMetadataModuleDescriptorFactory;Lorg/jetbrains/kotlin/config/LanguageVersionSettings;Lorg/jetbrains/kotlin/descriptors/ModuleDescriptor;Lorg/jetbrains/kotlin/incremental/components/LookupTracker;)Lorg/jetbrains/kotlin/descriptors/PackageFragmentProvider; + public abstract fun isAnalysisCompatible (Lorg/jetbrains/kotlin/library/KotlinLibrary;)Z +} + +public abstract interface class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/MockApplicationHack { + public abstract fun hack (Lcom/intellij/mock/MockApplication;)V +} + +public abstract interface class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisContext : java/io/Closeable { + public abstract fun getEnvironment ()Lorg/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment; + public abstract fun getModuleDescriptor ()Lorg/jetbrains/kotlin/descriptors/ModuleDescriptor; + public abstract fun getProject ()Lcom/intellij/openapi/project/Project; + public abstract fun getResolveSession ()Lorg/jetbrains/kotlin/resolve/lazy/ResolveSession; +} + +public final class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisEnvironment : com/intellij/openapi/Disposable { + public fun <init> (Lorg/jetbrains/kotlin/cli/common/messages/MessageCollector;Lorg/jetbrains/dokka/Platform;Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerExtensionPointProvider;Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/MockApplicationHack;Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KLibService;)V + public fun dispose ()V +} + +public abstract class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinAnalysis : java/io/Closeable { + public fun <init> ()V + public fun <init> (Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinAnalysis;)V + public synthetic fun <init> (Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinAnalysis;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun get (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisContext; +} + diff --git a/subprojects/analysis-kotlin-descriptors/compiler/build.gradle.kts b/subprojects/analysis-kotlin-descriptors/compiler/build.gradle.kts new file mode 100644 index 00000000..1b40027d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("org.jetbrains.conventions.kotlin-jvm") +} + +dependencies { + compileOnly(projects.core) + compileOnly(projects.subprojects.analysisKotlinApi) + + api(libs.kotlin.compiler) + + implementation(projects.subprojects.analysisMarkdownJb) + implementation(projects.subprojects.analysisJavaPsi) + + testImplementation(projects.core.contentMatcherTestUtils) + testImplementation(projects.core.testApi) + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.junit.jupiter) + + // TODO [beresnev] get rid of it + compileOnly(libs.kotlinx.coroutines.core) +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/AnalysisContextCreator.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/AnalysisContextCreator.kt new file mode 100644 index 00000000..dfba2b3a --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/AnalysisContextCreator.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import com.intellij.mock.MockProject +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.AnalysisContext +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.AnalysisEnvironment +import org.jetbrains.kotlin.analyzer.ResolverForModule +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.descriptors.ModuleDescriptor + +@InternalDokkaApi +interface AnalysisContextCreator { + fun create( + project: MockProject, + moduleDescriptor: ModuleDescriptor, + moduleResolver: ResolverForModule, + kotlinEnvironment: KotlinCoreEnvironment, + analysisEnvironment: AnalysisEnvironment, + ): AnalysisContext +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin.kt new file mode 100644 index 00000000..535e628e --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin.kt @@ -0,0 +1,135 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute +import com.intellij.psi.PsiAnnotation +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.BreakingAbstractionKotlinLightMethodChecker +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.ProjectKotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.* +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs.ModuleAndPackageDocumentationReader +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.java.* +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator.DefaultDescriptorToDocumentableTranslator +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator.DefaultExternalDocumentablesProvider +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.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation + +@InternalDokkaApi +class CompilerDescriptorAnalysisPlugin : DokkaPlugin() { + + val kdocFinder by extensionPoint<KDocFinder>() + + val descriptorFinder by extensionPoint<DescriptorFinder>() + + val klibService by extensionPoint<KLibService>() + + val compilerExtensionPointProvider by extensionPoint<CompilerExtensionPointProvider>() + + val mockApplicationHack by extensionPoint<MockApplicationHack>() + + val analysisContextCreator by extensionPoint<AnalysisContextCreator>() + + val kotlinAnalysis by extensionPoint<KotlinAnalysis>() + + internal val documentableAnalyzerImpl by extending { + plugin<InternalKotlinAnalysisPlugin>().documentableSourceLanguageParser providing { CompilerDocumentableSourceLanguageParser() } + } + + internal val defaultKotlinAnalysis by extending { + kotlinAnalysis providing { ctx -> + ProjectKotlinAnalysis( + sourceSets = ctx.configuration.sourceSets, + context = ctx + ) + } + } + + internal val descriptorToDocumentableTranslator by extending { + CoreExtensions.sourceToDocumentableTranslator providing ::DefaultDescriptorToDocumentableTranslator + } + + internal val defaultSamplesTransformer by extending { + CoreExtensions.pageTransformer providing ::DefaultSamplesTransformer + } + + internal val descriptorFullClassHierarchyBuilder by extending { + plugin<InternalKotlinAnalysisPlugin>().fullClassHierarchyBuilder providing { DescriptorFullClassHierarchyBuilder() } + } + + internal val descriptorSyntheticDocumentableDetector by extending { + plugin<InternalKotlinAnalysisPlugin>().syntheticDocumentableDetector providing { DescriptorSyntheticDocumentableDetector() } + } + + internal val moduleAndPackageDocumentationReader by extending { + plugin<InternalKotlinAnalysisPlugin>().moduleAndPackageDocumentationReader providing ::ModuleAndPackageDocumentationReader + } + + internal val kotlinToJavaMapper by extending { + plugin<InternalKotlinAnalysisPlugin>().kotlinToJavaService providing { DescriptorKotlinToJavaMapper() } + } + + internal val descriptorInheritanceBuilder by extending { + plugin<InternalKotlinAnalysisPlugin>().inheritanceBuilder providing { DescriptorInheritanceBuilder() } + } + + internal val defaultExternalDocumentablesProvider by extending { + plugin<InternalKotlinAnalysisPlugin>().externalDocumentablesProvider providing ::DefaultExternalDocumentablesProvider + } + + 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(querySingle { kdocFinder }, querySingle { descriptorFinder }) + } + } + + internal val kotlinDocCommentParser by extending { + javaAnalysisPlugin.docCommentParsers providing { context -> + DescriptorKotlinDocCommentParser( + context, + context.logger + ) + } + } + + 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 disposeKotlinAnalysisPostAction by extending { + CoreExtensions.postActions with PostAction { querySingle { kotlinAnalysis }.close() } + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDocumentableSourceLanguageParser.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDocumentableSourceLanguageParser.kt new file mode 100644 index 00000000..888ccfa9 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDocumentableSourceLanguageParser.kt @@ -0,0 +1,23 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.DescriptorDocumentableSource +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.WithSources +import org.jetbrains.kotlin.analysis.kotlin.internal.DocumentableLanguage +import org.jetbrains.kotlin.analysis.kotlin.internal.DocumentableSourceLanguageParser + +internal class CompilerDocumentableSourceLanguageParser : DocumentableSourceLanguageParser { + 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 DescriptorDocumentableSource -> DocumentableLanguage.KOTLIN + else -> error("Unknown language sources: ${documentableSource::class}") + } + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerExtensionPointProvider.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerExtensionPointProvider.kt new file mode 100644 index 00000000..50bdbb2c --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerExtensionPointProvider.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.kotlin.extensions.ApplicationExtensionDescriptor + +@InternalDokkaApi +interface CompilerExtensionPointProvider { + fun get(): List<CompilerExtensionPoint> + + class CompilerExtensionPoint( + val extensionDescriptor: ApplicationExtensionDescriptor<Any>, + val extensions: List<Any> + ) +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/DescriptorFinder.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/DescriptorFinder.kt new file mode 100644 index 00000000..eed62fa3 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/DescriptorFinder.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.psi.KtDeclaration + +@InternalDokkaApi +interface DescriptorFinder { + fun KtDeclaration.findDescriptor(): DeclarationDescriptor? +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KDocFinder.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KDocFinder.kt new file mode 100644 index 00000000..23d1acfe --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KDocFinder.kt @@ -0,0 +1,30 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils + +@InternalDokkaApi +interface KDocFinder { + fun KtElement.findKDoc(): KDocTag? + + fun DeclarationDescriptor.find( + descriptorToPsi: (DeclarationDescriptorWithSource) -> PsiElement? = { + DescriptorToSourceUtils.descriptorToDeclaration( + it + ) + } + ): KDocTag? + + fun resolveKDocLink( + fromDescriptor: DeclarationDescriptor, + qualifiedName: String, + sourceSet: DokkaConfiguration.DokkaSourceSet, + emptyBindingContext: Boolean = false + ): Collection<DeclarationDescriptor> +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KLibService.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KLibService.kt new file mode 100644 index 00000000..ceb5536a --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/KLibService.kt @@ -0,0 +1,23 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.kotlin.backend.common.serialization.metadata.KlibMetadataModuleDescriptorFactory +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.PackageFragmentProvider +import org.jetbrains.kotlin.incremental.components.LookupTracker +import org.jetbrains.kotlin.library.KotlinLibrary +import org.jetbrains.kotlin.storage.StorageManager + +@InternalDokkaApi +interface KLibService { + fun KotlinLibrary.createPackageFragmentProvider( + storageManager: StorageManager, + metadataModuleDescriptorFactory: KlibMetadataModuleDescriptorFactory, + languageVersionSettings: LanguageVersionSettings, + moduleDescriptor: ModuleDescriptor, + lookupTracker: LookupTracker + ): PackageFragmentProvider? + + fun isAnalysisCompatible(kotlinLibrary: KotlinLibrary): Boolean +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/MockApplicationHack.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/MockApplicationHack.kt new file mode 100644 index 00000000..77a4e083 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/MockApplicationHack.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import com.intellij.mock.MockApplication +import org.jetbrains.dokka.InternalDokkaApi + +@InternalDokkaApi +interface MockApplicationHack { // ¯\_(ツ)_/¯ + fun hack(mockApplication: MockApplication) +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AbsolutePathString.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AbsolutePathString.kt new file mode 100644 index 00000000..f1d35752 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AbsolutePathString.kt @@ -0,0 +1,3 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +internal typealias AbsolutePathString = String diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisContext.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisContext.kt new file mode 100644 index 00000000..89ae8810 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisContext.kt @@ -0,0 +1,94 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.openapi.project.Project +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.AnalysisContextCreator +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import java.io.Closeable +import java.io.File + +internal fun createAnalysisContext( + context: DokkaContext, + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + sourceSet: DokkaConfiguration.DokkaSourceSet, + analysisConfiguration: DokkaAnalysisConfiguration +): 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( + context = context, + classpath = classpath, + sourceRoots = sources, + sourceSet = sourceSet, + analysisConfiguration = analysisConfiguration + ) +} + +internal fun createAnalysisContext( + context: DokkaContext, + classpath: List<File>, + sourceRoots: Set<File>, + sourceSet: DokkaConfiguration.DokkaSourceSet, + analysisConfiguration: DokkaAnalysisConfiguration +): AnalysisContext { + val analysisEnvironment = AnalysisEnvironment( + DokkaMessageCollector(context.logger), + sourceSet.analysisPlatform, + context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { compilerExtensionPointProvider }, + context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { mockApplicationHack }, + context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { klibService }, + ).apply { + if (analysisPlatform == Platform.jvm) { + configureJdkClasspathRoots() + } + addClasspath(classpath) + addSources(sourceRoots) + + loadLanguageVersionSettings(sourceSet.languageVersion, sourceSet.apiVersion) + } + + val environment = analysisEnvironment.createCoreEnvironment() + return analysisEnvironment.createResolutionFacade( + environment, + context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle<CompilerDescriptorAnalysisPlugin, AnalysisContextCreator> { analysisContextCreator }, + analysisConfiguration.ignoreCommonBuiltIns + ) +} + +internal class DokkaMessageCollector(private val logger: DokkaLogger) : MessageCollector { + override fun clear() { + seenErrors = false + } + + private var seenErrors = false + + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) { + if (severity == CompilerMessageSeverity.ERROR) { + seenErrors = true + } + logger.info(MessageRenderer.PLAIN_FULL_PATHS.render(severity, message, location)) + } + + override fun hasErrors() = seenErrors +} + +interface AnalysisContext : Closeable { + val environment: KotlinCoreEnvironment + val resolveSession: ResolveSession + val moduleDescriptor: ModuleDescriptor + val project: Project +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisEnvironment.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisEnvironment.kt new file mode 100644 index 00000000..611d1325 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisEnvironment.kt @@ -0,0 +1,599 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.core.CoreApplicationEnvironment +import com.intellij.mock.MockApplication +import com.intellij.mock.MockComponentManager +import com.intellij.mock.MockProject +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.extensions.Extensions +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.psi.PsiNameHelper +import com.intellij.psi.impl.PsiNameHelperImpl +import com.intellij.psi.impl.source.javadoc.JavadocManagerImpl +import com.intellij.psi.javadoc.CustomJavadocTagProvider +import com.intellij.psi.javadoc.JavadocManager +import com.intellij.psi.javadoc.JavadocTagInfo +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.AnalysisContextCreator +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerExtensionPointProvider +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KLibService +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.MockApplicationHack +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve.* +import org.jetbrains.kotlin.analyzer.* +import org.jetbrains.kotlin.analyzer.common.CommonAnalysisParameters +import org.jetbrains.kotlin.analyzer.common.CommonDependenciesContainer +import org.jetbrains.kotlin.analyzer.common.CommonPlatformAnalyzerServices +import org.jetbrains.kotlin.analyzer.common.CommonResolverForModuleFactory +import org.jetbrains.kotlin.builtins.DefaultBuiltIns +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.builtins.jvm.JvmBuiltIns +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.config.ContentRoot +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.cli.jvm.config.* +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.context.ProjectContext +import org.jetbrains.kotlin.context.withModule +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl +import org.jetbrains.kotlin.extensions.ApplicationExtensionDescriptor +import org.jetbrains.kotlin.js.config.JSConfigurationKeys +import org.jetbrains.kotlin.js.resolve.JsPlatformAnalyzerServices +import org.jetbrains.kotlin.library.KLIB_FILE_EXTENSION +import org.jetbrains.kotlin.library.KotlinLibrary +import org.jetbrains.kotlin.library.ToolingSingleFileKlibResolveStrategy +import org.jetbrains.kotlin.library.resolveSingleFileKlib +import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.CommonPlatforms +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.js.JsPlatforms +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms.unspecifiedJvmPlatform +import org.jetbrains.kotlin.platform.konan.NativePlatforms +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.CliSealedClassInheritorsProvider +import org.jetbrains.kotlin.resolve.CompilerEnvironment +import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices +import org.jetbrains.kotlin.resolve.jvm.JvmPlatformParameters +import org.jetbrains.kotlin.resolve.jvm.JvmResolverForModuleFactory +import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatformAnalyzerServices +import org.jetbrains.kotlin.resolve.konan.platform.NativePlatformAnalyzerServices +import org.jetbrains.kotlin.storage.LockBasedStorageManager +import java.io.File +import org.jetbrains.kotlin.konan.file.File as KFile + +internal const val JAR_SEPARATOR = "!/" + +/** + * Kotlin as a service entry point + * + * Configures environment, analyses files and provides facilities to perform code processing without emitting bytecode + * + * $messageCollector: required by compiler infrastructure and will receive all compiler messages + * $body: optional and can be used to configure environment without creating local variable + */ +@InternalDokkaApi +class AnalysisEnvironment( + private val messageCollector: MessageCollector, + internal val analysisPlatform: Platform, + private val compilerExtensionPointProvider: CompilerExtensionPointProvider, + private val mockApplicationHack: MockApplicationHack, + private val kLibService: KLibService, +) : Disposable { + private val configuration = CompilerConfiguration() + + init { + configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) + } + + internal fun createCoreEnvironment(): KotlinCoreEnvironment { + System.setProperty("idea.io.use.nio2", "true") + System.setProperty("idea.ignore.disabled.plugins", "true") + + val configFiles = when (analysisPlatform) { + Platform.jvm, Platform.common -> EnvironmentConfigFiles.JVM_CONFIG_FILES + Platform.native -> EnvironmentConfigFiles.NATIVE_CONFIG_FILES + Platform.js, Platform.wasm -> EnvironmentConfigFiles.JS_CONFIG_FILES + } + + val environment = KotlinCoreEnvironment.createForProduction(this, configuration, configFiles) + val projectComponentManager = environment.project as MockComponentManager + + CoreApplicationEnvironment.registerExtensionPoint( + environment.project.extensionArea, + JavadocTagInfo.EP_NAME, JavadocTagInfo::class.java + ) + + @Suppress("DEPRECATION") + val extensionArea = Extensions.getRootArea() + + CoreApplicationEnvironment.registerExtensionPoint( + extensionArea, + CustomJavadocTagProvider.EP_NAME, CustomJavadocTagProvider::class.java + ) + + // TODO: figure out why compilation fails with unresolved `CoreApplicationEnvironment.registerApplicationService(...)` + // call, fix it appropriately + with(ApplicationManager.getApplication() as MockApplication) { + mockApplicationHack.hack(this) + } + + projectComponentManager.registerService( + JavadocManager::class.java, + JavadocManagerImpl(environment.project) + ) + + projectComponentManager.registerService( + PsiNameHelper::class.java, + PsiNameHelperImpl(environment.project) + ) + + projectComponentManager.registerService( + CustomJavadocTagProvider::class.java, + CustomJavadocTagProvider { emptyList() } + ) + + compilerExtensionPointProvider.get().forEach { extension -> + registerExtensionPoint(extension.extensionDescriptor, extension.extensions, this) + } + return environment + } + + private fun createSourceModuleSearchScope(project: Project, sourceFiles: List<KtFile>): GlobalSearchScope = + when (analysisPlatform) { + Platform.jvm -> TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, sourceFiles) + Platform.js, Platform.common, Platform.native, Platform.wasm -> GlobalSearchScope.filesScope( + project, + sourceFiles.map { it.virtualFile }.toSet() + ) + } + + internal fun createResolutionFacade( + environment: KotlinCoreEnvironment, + analysisContextCreator: AnalysisContextCreator, + ignoreCommonBuiltIns: Boolean = false + ): AnalysisContext { + val projectContext = ProjectContext(environment.project, "Dokka") + val sourceFiles = environment.getSourceFiles() + + val targetPlatform = when (analysisPlatform) { + Platform.js, Platform.wasm -> JsPlatforms.defaultJsPlatform + Platform.common -> CommonPlatforms.defaultCommonPlatform + Platform.native -> NativePlatforms.unspecifiedNativePlatform + Platform.jvm -> JvmPlatforms.defaultJvmPlatform + } + + val kotlinLibraries: Map<AbsolutePathString, KotlinLibrary> = resolveKotlinLibraries() + + val commonDependencyContainer = if (analysisPlatform == Platform.common) DokkaKlibMetadataCommonDependencyContainer( + kotlinLibraries.values.toList(), + environment.configuration, + LockBasedStorageManager("DokkaKlibMetadata") + ) else null + + val extraModuleDependencies = kotlinLibraries.values.registerLibraries() + commonDependencyContainer?.moduleInfos.orEmpty() + + val library = object : LibraryModuleInfo { + override val analyzerServices: PlatformDependentAnalyzerServices = + analysisPlatform.analyzerServices() + override val name: Name = Name.special("<library>") + override val platform: TargetPlatform = targetPlatform + override fun dependencies(): List<ModuleInfo> = listOf(this) + override fun getLibraryRoots(): Collection<String> = classpath + .map { libraryFile -> libraryFile.absolutePath } + .filter { path -> path !in kotlinLibraries } + } + + val module = object : ModuleInfo { + override val analyzerServices: PlatformDependentAnalyzerServices = + analysisPlatform.analyzerServices() + override val name: Name = Name.special("<module>") + override val platform: TargetPlatform = targetPlatform + override fun dependencies(): List<ModuleInfo> = + listOf(this, library) + extraModuleDependencies + + /** + * Only for common platform ignore BuiltIns for StdLib since it can cause a conflict + * between BuiltIns from a compiler and ones from source code. + */ + override fun dependencyOnBuiltIns(): ModuleInfo.DependencyOnBuiltIns { + return if (analysisPlatform == Platform.common && ignoreCommonBuiltIns) ModuleInfo.DependencyOnBuiltIns.NONE + else super.dependencyOnBuiltIns() + } + } + + val sourcesScope = createSourceModuleSearchScope(environment.project, sourceFiles) + val modulesContent: (ModuleInfo) -> ModuleContent<ModuleInfo> = { + when (it) { + library -> ModuleContent(it, emptyList(), GlobalSearchScope.notScope(sourcesScope)) + module -> ModuleContent(it, emptyList(), GlobalSearchScope.allScope(environment.project)) + is DokkaKlibLibraryInfo -> { + if (it.libraryRoot in kotlinLibraries) + ModuleContent(it, emptyList(), GlobalSearchScope.notScope(sourcesScope)) + else null + } + is CommonKlibModuleInfo -> ModuleContent(it, emptyList(), GlobalSearchScope.notScope(sourcesScope)) + else -> null + } ?: throw IllegalArgumentException("Unexpected module info") + } + + var builtIns: JvmBuiltIns? = null + + val resolverForProject = when (analysisPlatform) { + Platform.jvm -> { + builtIns = JvmBuiltIns( + projectContext.storageManager, + JvmBuiltIns.Kind.FROM_CLASS_LOADER + ) // TODO we should use FROM_DEPENDENCIES + createJvmResolverForProject( + projectContext, + module, + library, + modulesContent, + sourcesScope, + builtIns + ) + } + Platform.common -> createCommonResolverForProject( + projectContext, + module, + modulesContent, + environment, + commonDependencyContainer + ) + Platform.js, Platform.wasm -> createJsResolverForProject(projectContext, module, modulesContent) + Platform.native -> createNativeResolverForProject(projectContext, module, modulesContent) + + } + @Suppress("UNUSED_VARIABLE") // BEWARE!!!! IT's UNUSED, but without it some things don't work + val libraryModuleDescriptor = resolverForProject.descriptorForModule(library) + + val moduleDescriptor = resolverForProject.descriptorForModule(module) + builtIns?.initialize(moduleDescriptor, true) + + @Suppress("UNUSED_VARIABLE") // BEWARE!!!! IT's UNUSED, but without it some things don't work + val resolverForLibrary = resolverForProject.resolverForModule(library) // Required before module to initialize library properly + + val resolverForModule = resolverForProject.resolverForModule(module) + + return analysisContextCreator.create( + environment.project as MockProject, + moduleDescriptor, + resolverForModule, + environment, + this + ) + } + + private fun Platform.analyzerServices() = when (this) { + Platform.js, Platform.wasm -> JsPlatformAnalyzerServices + Platform.common -> CommonPlatformAnalyzerServices + Platform.native -> NativePlatformAnalyzerServices + Platform.jvm -> JvmPlatformAnalyzerServices + } + + private fun Collection<KotlinLibrary>.registerLibraries(): List<DokkaKlibLibraryInfo> { + if (analysisPlatform != Platform.native && analysisPlatform != Platform.js && analysisPlatform != Platform.wasm) return emptyList() + val dependencyResolver = DokkaKlibLibraryDependencyResolver() + val analyzerServices = analysisPlatform.analyzerServices() + + return map { kotlinLibrary -> + if (analysisPlatform == org.jetbrains.dokka.Platform.native) DokkaNativeKlibLibraryInfo( + kotlinLibrary, + analyzerServices, + dependencyResolver + ) + else DokkaJsKlibLibraryInfo(kotlinLibrary, analyzerServices, dependencyResolver) + } + } + + @OptIn(ExperimentalStdlibApi::class) + private fun resolveKotlinLibraries(): Map<AbsolutePathString, KotlinLibrary> { + return if (analysisPlatform == Platform.jvm) emptyMap() else buildMap { + classpath + .filter { it.isDirectory || it.extension == KLIB_FILE_EXTENSION } + .forEach { libraryFile -> + try { + val kotlinLibrary = resolveSingleFileKlib( + libraryFile = KFile(libraryFile.absolutePath), + strategy = ToolingSingleFileKlibResolveStrategy + ) + if (kLibService.isAnalysisCompatible(kotlinLibrary)) { + // exists, is KLIB, has compatible format + put( + libraryFile.absolutePath, + kotlinLibrary + ) + } + } catch (e: Throwable) { + configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY) + .report(CompilerMessageSeverity.WARNING, "Can not resolve KLIB. " + e.message) + } + } + } + } + + private fun createCommonResolverForProject( + projectContext: ProjectContext, + module: ModuleInfo, + modulesContent: (ModuleInfo) -> ModuleContent<ModuleInfo>, + environment: KotlinCoreEnvironment, + dependencyContainer: CommonDependenciesContainer? + ): ResolverForProject<ModuleInfo> { + return object : AbstractResolverForProject<ModuleInfo>( + "Dokka", + projectContext, + modules = module.dependencies() + ) { + override fun modulesContent(module: ModuleInfo): ModuleContent<ModuleInfo> = modulesContent(module) + + override fun builtInsForModule(module: ModuleInfo): KotlinBuiltIns = DefaultBuiltIns.Instance + + override fun createResolverForModule( + descriptor: ModuleDescriptor, + moduleInfo: ModuleInfo + ): ResolverForModule = + CommonResolverForModuleFactory( + CommonAnalysisParameters( + metadataPartProviderFactory = { content -> + environment.createPackagePartProvider(content.moduleContentScope) + } + ), + CompilerEnvironment, + unspecifiedJvmPlatform, + true, + dependencyContainer + ).createResolverForModule( + descriptor as ModuleDescriptorImpl, + projectContext.withModule(descriptor), + modulesContent(moduleInfo), + this, + LanguageVersionSettingsImpl.DEFAULT, + CliSealedClassInheritorsProvider, + ) + + override fun sdkDependency(module: ModuleInfo): ModuleInfo? = null + } + } + + private fun createJsResolverForProject( + projectContext: ProjectContext, + module: ModuleInfo, + modulesContent: (ModuleInfo) -> ModuleContent<ModuleInfo> + ): ResolverForProject<ModuleInfo> { + return object : AbstractResolverForProject<ModuleInfo>( + "Dokka", + projectContext, + modules = module.dependencies() + ) { + override fun modulesContent(module: ModuleInfo): ModuleContent<ModuleInfo> = modulesContent(module) + override fun createResolverForModule( + descriptor: ModuleDescriptor, + moduleInfo: ModuleInfo + ): ResolverForModule = DokkaJsResolverForModuleFactory(CompilerEnvironment, kLibService).createResolverForModule( + descriptor as ModuleDescriptorImpl, + projectContext.withModule(descriptor), + modulesContent(moduleInfo), + this, + LanguageVersionSettingsImpl.DEFAULT, + CliSealedClassInheritorsProvider, + ) + + override fun builtInsForModule(module: ModuleInfo): KotlinBuiltIns = DefaultBuiltIns.Instance + + override fun sdkDependency(module: ModuleInfo): ModuleInfo? = null + } + } + + private fun createNativeResolverForProject( + projectContext: ProjectContext, + module: ModuleInfo, + modulesContent: (ModuleInfo) -> ModuleContent<ModuleInfo> + ): ResolverForProject<ModuleInfo> { + return object : AbstractResolverForProject<ModuleInfo>( + "Dokka", + projectContext, + modules = module.dependencies() + ) { + override fun modulesContent(module: ModuleInfo): ModuleContent<ModuleInfo> = modulesContent(module) + override fun createResolverForModule( + descriptor: ModuleDescriptor, + moduleInfo: ModuleInfo + ): ResolverForModule { + + return DokkaNativeResolverForModuleFactory(CompilerEnvironment, kLibService).createResolverForModule( + descriptor as ModuleDescriptorImpl, + projectContext.withModule(descriptor), + modulesContent(moduleInfo), + this, + LanguageVersionSettingsImpl.DEFAULT, + CliSealedClassInheritorsProvider, + ) + } + + override fun builtInsForModule(module: ModuleInfo): KotlinBuiltIns = DefaultBuiltIns.Instance + + override fun sdkDependency(module: ModuleInfo): ModuleInfo? = null + } + } + + private fun createJvmResolverForProject( + projectContext: ProjectContext, + module: ModuleInfo, + library: LibraryModuleInfo, + modulesContent: (ModuleInfo) -> ModuleContent<ModuleInfo>, + sourcesScope: GlobalSearchScope, + builtIns: KotlinBuiltIns + ): ResolverForProject<ModuleInfo> { + val javaRoots = classpath + .mapNotNull { file -> + val rootFile = when (file.extension) { + "jar" -> StandardFileSystems.jar().findFileByPath("${file.absolutePath}$JAR_SEPARATOR") + else -> StandardFileSystems.local().findFileByPath(file.absolutePath) + } + rootFile?.let { JavaRoot(it, JavaRoot.RootType.BINARY) } + } + + return object : AbstractResolverForProject<ModuleInfo>( + "Dokka", + projectContext, + modules = listOf(module, library) + ) { + override fun modulesContent(module: ModuleInfo): ModuleContent<ModuleInfo> = + when (module) { + library -> ModuleContent(module, emptyList(), GlobalSearchScope.notScope(sourcesScope)) + module -> ModuleContent(module, emptyList(), sourcesScope) + else -> throw IllegalArgumentException("Unexpected module info") + } + + override fun builtInsForModule(module: ModuleInfo): KotlinBuiltIns = builtIns + + override fun createResolverForModule( + descriptor: ModuleDescriptor, + moduleInfo: ModuleInfo + ): ResolverForModule = JvmResolverForModuleFactory( + JvmPlatformParameters(packagePartProviderFactory = { content -> + JvmPackagePartProvider( + configuration.languageVersionSettings, + content.moduleContentScope + ) + .apply { + addRoots(javaRoots, messageCollector) + } + }, moduleByJavaClass = { + val file = + (it as? BinaryJavaClass)?.virtualFile ?: (it as JavaClassImpl).psi.containingFile.virtualFile + if (file in sourcesScope) + module + else + library + }, resolverForReferencedModule = null, + useBuiltinsProviderForModule = { false }), + CompilerEnvironment, + unspecifiedJvmPlatform + ).createResolverForModule( + descriptor as ModuleDescriptorImpl, + projectContext.withModule(descriptor), + modulesContent(moduleInfo), + this, + configuration.languageVersionSettings, + CliSealedClassInheritorsProvider, + ) + + override fun sdkDependency(module: ModuleInfo): ModuleInfo? = null + } + } + + internal fun loadLanguageVersionSettings(languageVersionString: String?, apiVersionString: String?) { + val languageVersion = LanguageVersion.fromVersionString(languageVersionString) ?: LanguageVersion.LATEST_STABLE + val apiVersion = + apiVersionString?.let { ApiVersion.parse(it) } ?: ApiVersion.createByLanguageVersion(languageVersion) + configuration.languageVersionSettings = LanguageVersionSettingsImpl( + languageVersion = languageVersion, + apiVersion = apiVersion, analysisFlags = hashMapOf( + // force to resolve light classes (lazily by default) + AnalysisFlags.eagerResolveOfLightClasses to true + ) + ) + } + + /** + * Classpath for this environment. + */ + private val classpath: List<File> + get() = configuration.jvmClasspathRoots + configuration.getList(JSConfigurationKeys.LIBRARIES) + .mapNotNull { File(it) } + + /** + * Adds list of paths to classpath. + * $paths: collection of files to add + */ + internal fun addClasspath(paths: List<File>) { + if (analysisPlatform == Platform.js || analysisPlatform == Platform.wasm) { + configuration.addAll(JSConfigurationKeys.LIBRARIES, paths.map { it.absolutePath }) + } else { + configuration.addJvmClasspathRoots(paths) + } + } + + // Set up JDK classpath roots explicitly because of https://github.com/JetBrains/kotlin/commit/f89765eb33dd95c8de33a919cca83651b326b246 + internal fun configureJdkClasspathRoots() = configuration.configureJdkClasspathRoots() + /** + * Adds path to classpath. + * $path: path to add + */ + internal fun addClasspath(path: File) { + if (analysisPlatform == Platform.js || analysisPlatform == Platform.wasm) { + configuration.add(JSConfigurationKeys.LIBRARIES, path.absolutePath) + } else { + configuration.addJvmClasspathRoot(path) + } + } + + /** + * List of source roots for this environment. + */ + internal val sources: List<String> + get() = configuration.get(CLIConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<KotlinSourceRoot>() + ?.map { it.path } ?: emptyList() + + /** + * Adds list of paths to source roots. + * $list: collection of files to add + */ + internal fun addSources(sourceDirectories: Iterable<File>) { + sourceDirectories.forEach { directory -> + configuration.addKotlinSourceRoot(directory.path) + if (directory.isDirectory || directory.extension == "java") { + configuration.addJavaSourceRoot(directory) + } + } + } + + internal fun addRoots(list: List<ContentRoot>) { + configuration.addAll(CLIConfigurationKeys.CONTENT_ROOTS, list) + } + + /** + * Disposes the environment and frees all associated resources. + */ + override fun dispose() { + Disposer.dispose(this) + } + + private companion object { + private fun <T : Any> registerExtensionPoint( + appExtension: ApplicationExtensionDescriptor<T>, + instances: List<T>, + disposable: Disposable + ) { + @Suppress("DEPRECATION") + val extensionArea = Extensions.getRootArea() + + if (extensionArea.hasExtensionPoint(appExtension.extensionPointName)) { + return + } + + appExtension.registerExtensionPoint() + instances.forEach { extension -> appExtension.registerExtension(extension, disposable) } + } + } +} + + diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/CallableFactory.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/CallableFactory.kt new file mode 100644 index 00000000..0cc17219 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/CallableFactory.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.JavaClassReference +import org.jetbrains.dokka.links.TypeReference +import org.jetbrains.kotlin.descriptors.CallableDescriptor + +internal fun Callable.Companion.from(descriptor: CallableDescriptor, name: String? = null) = with(descriptor) { + Callable( + name ?: descriptor.name.asString(), + extensionReceiverParameter?.let { TypeReference.from(it) }, + valueParameters.mapNotNull { TypeReference.from(it) } + ) +} + +internal fun Callable.Companion.from(psi: PsiMethod) = with(psi) { + Callable( + name, + null, + parameterList.parameters.map { param -> JavaClassReference(param.type.canonicalText) }) +} + +internal fun Callable.Companion.from(psi: PsiField): Callable { + return Callable( + name = psi.name, + receiver = null, + params = emptyList() + ) +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/DRIFactory.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/DRIFactory.kt new file mode 100644 index 00000000..726d6dbc --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/DRIFactory.kt @@ -0,0 +1,49 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.psi.* +import org.jetbrains.dokka.links.* +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor +import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf +import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull + +internal fun DRI.Companion.from(descriptor: DeclarationDescriptor) = descriptor.parentsWithSelf.run { + val parameter = firstIsInstanceOrNull<ValueParameterDescriptor>() + val callable = parameter?.containingDeclaration ?: firstIsInstanceOrNull<CallableDescriptor>() + + DRI( + packageName = firstIsInstanceOrNull<PackageFragmentDescriptor>()?.fqName?.asString() ?: "", + classNames = (filterIsInstance<ClassDescriptor>() + filterIsInstance<TypeAliasDescriptor>()).toList() + .takeIf { it.isNotEmpty() } + ?.asReversed() + ?.joinToString(separator = ".") { it.name.asString() }, + callable = callable?.let { Callable.from(it) }, + target = DriTarget.from(parameter ?: descriptor), + extra = if (descriptor is EnumEntrySyntheticClassDescriptor || (descriptor as? ClassDescriptor)?.kind == ClassKind.ENUM_ENTRY) + DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode() + else null + ) +} + +internal fun DRI.Companion.from(psi: PsiElement) = psi.parentsWithSelf.run { + val psiMethod = firstIsInstanceOrNull<PsiMethod>() + val psiField = firstIsInstanceOrNull<PsiField>() + val classes = filterIsInstance<PsiClass>().filterNot { it is PsiTypeParameter } + .toList() // We only want exact PsiClass types, not PsiTypeParameter subtype + val additionalClasses = if (psi is PsiEnumConstant) listOfNotNull(psiField?.name) else emptyList() + DRI( + packageName = classes.lastOrNull()?.qualifiedName?.substringBeforeLast('.', "") ?: "", + classNames = (additionalClasses + classes.mapNotNull { it.name }).takeIf { it.isNotEmpty() } + ?.asReversed()?.joinToString("."), + // The fallback strategy test whether psi is not `PsiEnumConstant`. The reason behind this is that + // we need unified DRI for both Java and Kotlin enums, so we can link them properly and treat them alike. + // To achieve that, we append enum name to classNames list and leave the callable part set to null. For Kotlin enums + // it is by default, while for Java enums we have to explicitly test for that in this `takeUnless` condition. + callable = psiMethod?.let { Callable.from(it) } ?: psiField?.takeUnless { psi is PsiEnumConstant }?.let { Callable.from(it) }, + target = DriTarget.from(psi), + extra = if (psi is PsiEnumConstant) + DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode() + else null + ) +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/DRITargetFactory.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/DRITargetFactory.kt new file mode 100644 index 00000000..7f39cf94 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/DRITargetFactory.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiTypeParameter +import org.jetbrains.dokka.links.DriTarget +import org.jetbrains.dokka.links.PointingToCallableParameters +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.links.PointingToGenericParameters +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf +import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull + +internal fun DriTarget.Companion.from(descriptor: DeclarationDescriptor): DriTarget = descriptor.parentsWithSelf.run { + return when (descriptor) { + is TypeParameterDescriptor -> PointingToGenericParameters(descriptor.index) + is ValueParameterDescriptor -> PointingToCallableParameters(descriptor.index) + else -> { + val callable = firstIsInstanceOrNull<CallableDescriptor>() + val params = + callable?.let { listOfNotNull(it.extensionReceiverParameter) + it.valueParameters }.orEmpty() + val parameterDescriptor = firstIsInstanceOrNull<ParameterDescriptor>() + + parameterDescriptor?.let { PointingToCallableParameters(params.indexOf(it)) } + ?: PointingToDeclaration + } + } +} + + +internal fun DriTarget.Companion.from(psi: PsiElement): DriTarget = psi.parentsWithSelf.run { + return when (psi) { + is PsiTypeParameter -> PointingToGenericParameters(psi.index) + else -> firstIsInstanceOrNull<PsiParameter>()?.let { + val callable = firstIsInstanceOrNull<PsiMethod>() + val params = (callable?.parameterList?.parameters).orEmpty() + PointingToCallableParameters(params.indexOf(it)) + } ?: PointingToDeclaration + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/Documentable.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/Documentable.kt new file mode 100644 index 00000000..55a93ad5 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/Documentable.kt @@ -0,0 +1,24 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.psi.PsiDocumentManager +import org.jetbrains.dokka.model.DocumentableSource +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.resolve.source.getPsi + +internal class DescriptorDocumentableSource(val descriptor: DeclarationDescriptor) : DocumentableSource { + override val path = descriptor.toSourceElement.containingFile.toString() + + override fun computeLineNumber(): Int? { + return (this.descriptor as DeclarationDescriptorWithSource) + .source.getPsi() + ?.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) + } + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/JvmDependenciesIndexImpl.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/JvmDependenciesIndexImpl.kt new file mode 100644 index 00000000..42fda615 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/JvmDependenciesIndexImpl.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.ide.highlighter.JavaClassFileType +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import gnu.trove.THashMap +import it.unimi.dsi.fastutil.ints.IntArrayList +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +// speeds up finding files/classes in classpath/java source roots +// NOT THREADSAFE, needs to be adapted/removed if we want compiler to be multithreaded +// the main idea of this class is for each package to store roots which contains it to avoid excessive file system traversal +internal class JvmDependenciesIndexImpl(_roots: List<JavaRoot>) : JvmDependenciesIndex { + //these fields are computed based on _roots passed to constructor which are filled in later + private val roots: List<JavaRoot> by lazy { _roots.toList() } + + private val maxIndex: Int + get() = roots.size + + // each "Cache" object corresponds to a package + private class Cache { + private val innerPackageCaches = HashMap<String, Cache>() + + operator fun get(name: String) = innerPackageCaches.getOrPut(name, ::Cache) + + // indices of roots that are known to contain this package + // if this list contains [1, 3, 5] then roots with indices 1, 3 and 5 are known to contain this package, 2 and 4 are known not to (no information about roots 6 or higher) + // if this list contains maxIndex that means that all roots containing this package are known + val rootIndices = IntArrayList(2) + } + + // root "Cache" object corresponds to DefaultPackage which exists in every root. Roots with non-default fqname are also listed here but + // they will be ignored on requests with invalid fqname prefix. + private val rootCache: Cache by lazy { + Cache().apply { + roots.indices.forEach(rootIndices::add) + rootIndices.add(maxIndex) + rootIndices.trimToSize(0) + } + } + + // holds the request and the result last time we searched for class + // helps improve several scenarios, LazyJavaResolverContext.findClassInJava being the most important + private var lastClassSearch: Pair<FindClassRequest, SearchResult>? = null + + override val indexedRoots by lazy { roots.asSequence() } + + private val packageCache: Array<out MutableMap<String, VirtualFile?>> by lazy { + Array(roots.size) { THashMap<String, VirtualFile?>() } + } + + override fun traverseDirectoriesInPackage( + packageFqName: FqName, + acceptedRootTypes: Set<JavaRoot.RootType>, + continueSearch: (VirtualFile, JavaRoot.RootType) -> Boolean + ) { + search(TraverseRequest(packageFqName, acceptedRootTypes)) { dir, rootType -> + if (continueSearch(dir, rootType)) null else Unit + } + } + + // findClassGivenDirectory MUST check whether the class with this classId exists in given package + override fun <T : Any> findClass( + classId: ClassId, + acceptedRootTypes: Set<JavaRoot.RootType>, + findClassGivenDirectory: (VirtualFile, JavaRoot.RootType) -> T? + ): T? { + // make a decision based on information saved from last class search + if (lastClassSearch?.first?.classId != classId) { + return search(FindClassRequest(classId, acceptedRootTypes), findClassGivenDirectory) + } + + val (cachedRequest, cachedResult) = lastClassSearch!! + return when (cachedResult) { + is SearchResult.NotFound -> { + val limitedRootTypes = acceptedRootTypes - cachedRequest.acceptedRootTypes + if (limitedRootTypes.isEmpty()) { + null + } else { + search(FindClassRequest(classId, limitedRootTypes), findClassGivenDirectory) + } + } + is SearchResult.Found -> { + if (cachedRequest.acceptedRootTypes == acceptedRootTypes) { + findClassGivenDirectory(cachedResult.packageDirectory, cachedResult.root.type) + } else { + search(FindClassRequest(classId, acceptedRootTypes), findClassGivenDirectory) + } + } + } + } + + private fun <T : Any> search(request: SearchRequest, handler: (VirtualFile, JavaRoot.RootType) -> T?): T? { + // a list of package sub names, ["org", "jb", "kotlin"] + val packagesPath = request.packageFqName.pathSegments().map { it.identifier } + // a list of caches corresponding to packages, [default, "org", "org.jb", "org.jb.kotlin"] + val caches = cachesPath(packagesPath) + + var processedRootsUpTo = -1 + // traverse caches starting from last, which contains most specific information + + // NOTE: indices manipulation instead of using caches.reversed() is here for performance reasons + for (cacheIndex in caches.lastIndex downTo 0) { + val cacheRootIndices = caches[cacheIndex].rootIndices + for (i in 0 until cacheRootIndices.size) { + val rootIndex = cacheRootIndices.getInt(i) + if (rootIndex <= processedRootsUpTo) continue // roots with those indices have been processed by now + + val directoryInRoot = + travelPath(rootIndex, request.packageFqName, packagesPath, cacheIndex, caches) ?: continue + val root = roots[rootIndex] + if (root.type in request.acceptedRootTypes) { + val result = handler(directoryInRoot, root.type) + if (result != null) { + if (request is FindClassRequest) { + lastClassSearch = Pair(request, SearchResult.Found(directoryInRoot, root)) + } + return result + } + } + } + processedRootsUpTo = + if (cacheRootIndices.isEmpty()) { + processedRootsUpTo + } else { + cacheRootIndices.getInt(cacheRootIndices.size - 1) + } + } + + if (request is FindClassRequest) { + lastClassSearch = Pair(request, SearchResult.NotFound) + } + return null + } + + // try to find a target directory corresponding to package represented by packagesPath in a given root represented by index + // possibly filling "Cache" objects with new information + private fun travelPath( + rootIndex: Int, + packageFqName: FqName, + packagesPath: List<String>, + fillCachesAfter: Int, + cachesPath: List<Cache> + ): VirtualFile? { + if (rootIndex >= maxIndex) { + for (i in (fillCachesAfter + 1) until cachesPath.size) { + // we all know roots that contain this package by now + cachesPath[i].rootIndices.add(maxIndex) + cachesPath[i].rootIndices.trimToSize(0) + } + return null + } + + return synchronized(packageCache) { + packageCache[rootIndex].getOrPut(packageFqName.asString()) { + doTravelPath(rootIndex, packagesPath, fillCachesAfter, cachesPath) + } + } + } + + private fun doTravelPath(rootIndex: Int, packagesPath: List<String>, fillCachesAfter: Int, cachesPath: List<Cache>): VirtualFile? { + val pathRoot = roots[rootIndex] + val prefixPathSegments = pathRoot.prefixFqName?.pathSegments() + + var currentFile = pathRoot.file + + for (pathIndex in packagesPath.indices) { + val subPackageName = packagesPath[pathIndex] + if (prefixPathSegments != null && pathIndex < prefixPathSegments.size) { + // Traverse prefix first instead of traversing real directories + if (prefixPathSegments[pathIndex].identifier != subPackageName) { + return null + } + } else { + currentFile = currentFile.findChildPackage(subPackageName, pathRoot.type) ?: return null + } + + val correspondingCacheIndex = pathIndex + 1 + if (correspondingCacheIndex > fillCachesAfter) { + // subPackageName exists in this root + cachesPath[correspondingCacheIndex].rootIndices.add(rootIndex) + } + } + + return currentFile + } + + private fun VirtualFile.findChildPackage(subPackageName: String, rootType: JavaRoot.RootType): VirtualFile? { + val childDirectory = findChild(subPackageName) ?: return null + + val fileExtension = when (rootType) { + JavaRoot.RootType.BINARY -> JavaClassFileType.INSTANCE.defaultExtension + JavaRoot.RootType.BINARY_SIG -> "sig" + JavaRoot.RootType.SOURCE -> JavaFileType.INSTANCE.defaultExtension + } + + // If in addition to a directory "foo" there's a class file "foo.class" AND there are no classes anywhere in the directory "foo", + // then we ignore the directory and let the resolution choose the class "foo" instead. + if (findChild("$subPackageName.$fileExtension")?.isDirectory == false) { + if (VfsUtilCore.processFilesRecursively(childDirectory) { file -> file.extension != fileExtension }) { + return null + } + } + + return childDirectory + } + + private fun cachesPath(path: List<String>): List<Cache> { + val caches = ArrayList<Cache>(path.size + 1) + caches.add(rootCache) + var currentCache = rootCache + for (subPackageName in path) { + currentCache = currentCache[subPackageName] + caches.add(currentCache) + } + return caches + } + + private fun <E> MutableList<E>.trimToSize(newSize: Int) { + subList(newSize, size).clear() + } + + private data class FindClassRequest(val classId: ClassId, override val acceptedRootTypes: Set<JavaRoot.RootType>) : + SearchRequest { + override val packageFqName: FqName + get() = classId.packageFqName + } + + private data class TraverseRequest( + override val packageFqName: FqName, + override val acceptedRootTypes: Set<JavaRoot.RootType> + ) : SearchRequest + + private interface SearchRequest { + val packageFqName: FqName + val acceptedRootTypes: Set<JavaRoot.RootType> + } + + private sealed class SearchResult { + class Found(val packageDirectory: VirtualFile, val root: JavaRoot) : SearchResult() + + object NotFound : SearchResult() + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinAnalysis.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinAnalysis.kt new file mode 100644 index 00000000..b4a1b8f7 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinAnalysis.kt @@ -0,0 +1,94 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.Closeable + +@Suppress("FunctionName") +internal fun ProjectKotlinAnalysis( + sourceSets: List<DokkaSourceSet>, + context: DokkaContext, + analysisConfiguration: DokkaAnalysisConfiguration = DokkaAnalysisConfiguration() +): KotlinAnalysis { + val environments = sourceSets.associateWith { sourceSet -> + createAnalysisContext( + context = context, + sourceSets = sourceSets, + sourceSet = sourceSet, + analysisConfiguration = analysisConfiguration + ) + } + return EnvironmentKotlinAnalysis(environments) +} + +/** + * [projectKotlinAnalysis] needs to be closed separately + * Usually the analysis created for samples is short-lived and can be closed right after + * it's been used, there's no need to wait for [projectKotlinAnalysis] to be closed as it must be handled separately. + */ +@Suppress("FunctionName") +internal fun SamplesKotlinAnalysis( + sourceSets: List<DokkaSourceSet>, + context: DokkaContext, + projectKotlinAnalysis: KotlinAnalysis, + analysisConfiguration: DokkaAnalysisConfiguration = DokkaAnalysisConfiguration() +): KotlinAnalysis { + val environments = sourceSets + .filter { it.samples.isNotEmpty() } + .associateWith { sourceSet -> + createAnalysisContext( + context = context, + classpath = sourceSet.classpath, + sourceRoots = sourceSet.samples, + sourceSet = sourceSet, + analysisConfiguration = analysisConfiguration + ) + } + + return EnvironmentKotlinAnalysis(environments, projectKotlinAnalysis) +} + +internal class DokkaAnalysisConfiguration( + /** + * Only for common platform ignore BuiltIns for StdLib since it can cause a conflict + * between BuiltIns from a compiler and ones from source code. + */ + val ignoreCommonBuiltIns: Boolean = false +) + +/** + * First child delegation. It does not close [parent]. + */ +@InternalDokkaApi +abstract class KotlinAnalysis( + private val parent: KotlinAnalysis? = null +) : Closeable { + + operator fun get(key: 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) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinCliJavaFileManagerImpl.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinCliJavaFileManagerImpl.kt new file mode 100644 index 00000000..ac120b62 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/KotlinCliJavaFileManagerImpl.kt @@ -0,0 +1,304 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.core.CoreJavaFileManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.impl.file.PsiPackageImpl +import com.intellij.psi.search.GlobalSearchScope +import gnu.trove.THashMap +import gnu.trove.THashSet +import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex +import org.jetbrains.kotlin.cli.jvm.index.SingleJavaFileRootsIndex +import org.jetbrains.kotlin.load.java.JavaClassFinder +import org.jetbrains.kotlin.load.java.structure.JavaClass +import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryClassSignatureParser +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.ClassifierResolutionContext +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.isNotTopLevelClass +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.jvm.KotlinCliJavaFileManager +import org.jetbrains.kotlin.util.PerformanceCounter + +// TODO: do not inherit from CoreJavaFileManager to avoid accidental usage of its methods which do not use caches/indices +// Currently, the only relevant usage of this class as CoreJavaFileManager is at CoreJavaDirectoryService.getPackage, +// which is indirectly invoked from PsiPackage.getSubPackages +internal class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJavaFileManager(myPsiManager), KotlinCliJavaFileManager { + private val perfCounter = PerformanceCounter.create("Find Java class") + private lateinit var index: JvmDependenciesIndex + private lateinit var singleJavaFileRootsIndex: SingleJavaFileRootsIndex + private lateinit var packagePartProviders: List<JvmPackagePartProvider> + private val topLevelClassesCache: MutableMap<FqName, VirtualFile?> = THashMap() + private val allScope = GlobalSearchScope.allScope(myPsiManager.project) + private var usePsiClassFilesReading = false + + fun initialize( + index: JvmDependenciesIndex, + packagePartProviders: List<JvmPackagePartProvider>, + singleJavaFileRootsIndex: SingleJavaFileRootsIndex, + usePsiClassFilesReading: Boolean + ) { + this.index = index + this.packagePartProviders = packagePartProviders + this.singleJavaFileRootsIndex = singleJavaFileRootsIndex + this.usePsiClassFilesReading = usePsiClassFilesReading + } + + private fun findPsiClass(classId: ClassId, searchScope: GlobalSearchScope): PsiClass? = perfCounter.time { + findVirtualFileForTopLevelClass(classId, searchScope)?.findPsiClassInVirtualFile(classId.relativeClassName.asString()) + } + + private fun findVirtualFileForTopLevelClass(classId: ClassId, searchScope: GlobalSearchScope): VirtualFile? { + val relativeClassName = classId.relativeClassName.asString() + synchronized(topLevelClassesCache) { + return topLevelClassesCache.getOrPut(classId.packageFqName.child(classId.relativeClassName.pathSegments().first())) { + index.findClass(classId) { dir, type -> + findVirtualFileGivenPackage(dir, relativeClassName, type) + } ?: singleJavaFileRootsIndex.findJavaSourceClass(classId) + }?.takeIf { it in searchScope } + } + } + + private val binaryCache: MutableMap<ClassId, JavaClass?> = THashMap() + private val signatureParsingComponent = BinaryClassSignatureParser() + + fun findClass(classId: ClassId, searchScope: GlobalSearchScope): JavaClass? = findClass(JavaClassFinder.Request(classId), searchScope) + + override fun findClass(request: JavaClassFinder.Request, searchScope: GlobalSearchScope): JavaClass? { + val (classId, classFileContentFromRequest, outerClassFromRequest) = request + val virtualFile = findVirtualFileForTopLevelClass(classId, searchScope) ?: return null + + if (!usePsiClassFilesReading && (virtualFile.extension == "class" || virtualFile.extension == "sig")) { + synchronized(binaryCache){ + // We return all class files' names in the directory in knownClassNamesInPackage method, so one may request an inner class + return binaryCache.getOrPut(classId) { + // Note that currently we implicitly suppose that searchScope for binary classes is constant and we do not use it + // as a key in cache + // This is a true assumption by now since there are two search scopes in compiler: one for sources and another one for binary + // When it become wrong because we introduce the modules into CLI, it's worth to consider + // having different KotlinCliJavaFileManagerImpl's for different modules + + classId.outerClassId?.let { outerClassId -> + val outerClass = outerClassFromRequest ?: findClass(outerClassId, searchScope) + + return if (outerClass is BinaryJavaClass) + outerClass.findInnerClass(classId.shortClassName, classFileContentFromRequest) + else + outerClass?.findInnerClass(classId.shortClassName) + } + + // Here, we assume the class is top-level + val classContent = classFileContentFromRequest ?: virtualFile.contentsToByteArray() + if (virtualFile.nameWithoutExtension.contains("$") && isNotTopLevelClass(classContent)) return@getOrPut null + + val resolver = ClassifierResolutionContext { findClass(it, allScope) } + + BinaryJavaClass( + virtualFile, classId.asSingleFqName(), resolver, signatureParsingComponent, + outerClass = null, classContent = classContent + ) + } + } + } + + return virtualFile.findPsiClassInVirtualFile(classId.relativeClassName.asString())?.let(::JavaClassImpl) + } + + // this method is called from IDEA to resolve dependencies in Java code + // which supposedly shouldn't have errors so the dependencies exist in general + override fun findClass(qName: String, scope: GlobalSearchScope): PsiClass? { + // String cannot be reliably converted to ClassId because we don't know where the package name ends and class names begin. + // For example, if qName is "a.b.c.d.e", we should either look for a top level class "e" in the package "a.b.c.d", + // or, for example, for a nested class with the relative qualified name "c.d.e" in the package "a.b". + // Below, we start by looking for the top level class "e" in the package "a.b.c.d" first, then for the class "d.e" in the package + // "a.b.c", and so on, until we find something. Most classes are top level, so most of the times the search ends quickly + + forEachClassId(qName) { classId -> + findPsiClass(classId, scope)?.let { return it } + } + + return null + } + + private inline fun forEachClassId(fqName: String, block: (ClassId) -> Unit) { + var classId = fqName.toSafeTopLevelClassId() ?: return + + while (true) { + block(classId) + + val packageFqName = classId.packageFqName + if (packageFqName.isRoot) break + + classId = ClassId( + packageFqName.parent(), + FqName(packageFqName.shortName().asString() + "." + classId.relativeClassName.asString()), + false + ) + } + } + + override fun findClasses(qName: String, scope: GlobalSearchScope): Array<PsiClass> = perfCounter.time { + val result = ArrayList<PsiClass>(1) + forEachClassId(qName) { classId -> + val relativeClassName = classId.relativeClassName.asString() + index.traverseDirectoriesInPackage(classId.packageFqName) { dir, rootType -> + val psiClass = + findVirtualFileGivenPackage(dir, relativeClassName, rootType) + ?.takeIf { it in scope } + ?.findPsiClassInVirtualFile(relativeClassName) + if (psiClass != null) { + result.add(psiClass) + } + // traverse all + true + } + + singleJavaFileRootsIndex.findJavaSourceClass(classId) + ?.takeIf { it in scope } + ?.findPsiClassInVirtualFile(relativeClassName) + ?.let { result.add(it) } + + if (result.isNotEmpty()) { + return@time result.toTypedArray() + } + } + + PsiClass.EMPTY_ARRAY + } + + override fun findPackage(packageName: String): PsiPackage? { + var found = false + val packageFqName = packageName.toSafeFqName() ?: return null + index.traverseDirectoriesInPackage(packageFqName) { _, _ -> + found = true + //abort on first found + false + } + if (!found) { + found = packagePartProviders.any { it.findPackageParts(packageName).isNotEmpty() } + } + if (!found) { + found = singleJavaFileRootsIndex.findJavaSourceClasses(packageFqName).isNotEmpty() + } + return if (found) PsiPackageImpl(myPsiManager, packageName) else null + } + + private fun findVirtualFileGivenPackage( + packageDir: VirtualFile, + classNameWithInnerClasses: String, + rootType: JavaRoot.RootType + ): VirtualFile? { + val topLevelClassName = classNameWithInnerClasses.substringBefore('.') + + val vFile = when (rootType) { + JavaRoot.RootType.BINARY -> packageDir.findChild("$topLevelClassName.class") + JavaRoot.RootType.BINARY_SIG -> packageDir.findChild("$topLevelClassName.sig") + JavaRoot.RootType.SOURCE -> packageDir.findChild("$topLevelClassName.java") + } ?: return null + + if (!vFile.isValid) { + LOG.error("Invalid child of valid parent: ${vFile.path}; ${packageDir.isValid} path=${packageDir.path}") + return null + } + + return vFile + } + + private fun VirtualFile.findPsiClassInVirtualFile(classNameWithInnerClasses: String): PsiClass? { + val file = myPsiManager.findFile(this) as? PsiClassOwner ?: return null + return findClassInPsiFile(classNameWithInnerClasses, file) + } + + override fun knownClassNamesInPackage(packageFqName: FqName): Set<String> { + val result = THashSet<String>() + index.traverseDirectoriesInPackage(packageFqName, continueSearch = { dir, _ -> + for (child in dir.children) { + if (child.extension == "class" || child.extension == "java" || child.extension == "sig") { + result.add(child.nameWithoutExtension) + } + } + + true + }) + + for (classId in singleJavaFileRootsIndex.findJavaSourceClasses(packageFqName)) { + assert(!classId.isNestedClass) { "ClassId of a single .java source class should not be nested: $classId" } + result.add(classId.shortClassName.asString()) + } + + return result + } + + override fun findModules(moduleName: String, scope: GlobalSearchScope): Collection<PsiJavaModule> { + // TODO + return emptySet() + } + + override fun getNonTrivialPackagePrefixes(): Collection<String> = emptyList() + + companion object { + private val LOG = Logger.getInstance(KotlinCliJavaFileManagerImpl::class.java) + + private fun findClassInPsiFile(classNameWithInnerClassesDotSeparated: String, file: PsiClassOwner): PsiClass? { + for (topLevelClass in file.classes) { + val candidate = findClassByTopLevelClass(classNameWithInnerClassesDotSeparated, topLevelClass) + if (candidate != null) { + return candidate + } + } + return null + } + + private fun findClassByTopLevelClass(className: String, topLevelClass: PsiClass): PsiClass? { + if (className.indexOf('.') < 0) { + return if (className == topLevelClass.name) topLevelClass else null + } + + val segments = StringUtil.split(className, ".").iterator() + if (!segments.hasNext() || segments.next() != topLevelClass.name) { + return null + } + var curClass = topLevelClass + while (segments.hasNext()) { + val innerClassName = segments.next() + val innerClass = curClass.findInnerClassByName(innerClassName, false) ?: return null + curClass = innerClass + } + return curClass + } + } +} + +// a sad workaround to avoid throwing exception when called from inside IDEA code +private fun <T : Any> safely(compute: () -> T): T? = + try { + compute() + } catch (e: IllegalArgumentException) { + null + } catch (e: AssertionError) { + null + } + +private fun String.toSafeFqName(): FqName? = safely { FqName(this) } +private fun String.toSafeTopLevelClassId(): ClassId? = safely { ClassId.topLevel(FqName(this)) } diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/TypeReferenceFactory.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/TypeReferenceFactory.kt new file mode 100644 index 00000000..f271e1f1 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/TypeReferenceFactory.kt @@ -0,0 +1,68 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration + +import com.intellij.psi.PsiClass +import org.jetbrains.dokka.links.* +import org.jetbrains.kotlin.descriptors.ReceiverParameterDescriptor +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.scopes.receivers.ExtensionReceiver +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjection +import org.jetbrains.kotlin.types.error.ErrorType +import org.jetbrains.kotlin.types.error.ErrorTypeConstructor +import org.jetbrains.kotlin.types.error.ErrorTypeKind + +internal fun TypeReference.Companion.from(d: ReceiverParameterDescriptor): TypeReference? = + when (d.value) { + is ExtensionReceiver -> fromPossiblyNullable(d.type, emptyList()) + else -> run { + println("Unknown value type for $d") + null + } + } + +internal fun TypeReference.Companion.from(d: ValueParameterDescriptor): TypeReference = + fromPossiblyNullable(d.type, emptyList()) + +internal fun TypeReference.Companion.from(@Suppress("UNUSED_PARAMETER") p: PsiClass) = TypeReference + +private fun TypeReference.Companion.fromPossiblyNullable(t: KotlinType, paramTrace: List<KotlinType>): TypeReference = + fromPossiblyRecursive(t, paramTrace).let { if (t.isMarkedNullable) Nullable(it) else it } + +private fun TypeReference.Companion.fromPossiblyRecursive(t: KotlinType, paramTrace: List<KotlinType>): TypeReference = + paramTrace.indexOfFirst { it.constructor == t.constructor && it.arguments == t.arguments } + .takeIf { it >= 0 } + ?.let(::RecursiveType) + ?: from(t, paramTrace) + +private fun TypeReference.Companion.from(t: KotlinType, paramTrace: List<KotlinType>): TypeReference { + if (t is ErrorType) { + val errorConstructor = t.constructor as? ErrorTypeConstructor + val presentableName = + if (errorConstructor?.kind == ErrorTypeKind.UNRESOLVED_TYPE && errorConstructor.parameters.isNotEmpty()) + errorConstructor.getParam(0) + else + t.constructor.toString() + return TypeConstructor(presentableName, t.arguments.map { fromProjection(it, paramTrace) }) + } + return when (val d = t.constructor.declarationDescriptor) { + is TypeParameterDescriptor -> TypeParam( + d.upperBounds.map { fromPossiblyNullable(it, paramTrace + t) } + ) + else -> TypeConstructor( + t.constructorName.orEmpty(), + t.arguments.map { fromProjection(it, paramTrace) } + ) + } +} + +private fun TypeReference.Companion.fromProjection(t: TypeProjection, paramTrace: List<KotlinType>): TypeReference = + if (t.isStarProjection) { + StarProjection + } else { + fromPossiblyNullable(t.type, paramTrace) + } + +private val KotlinType.constructorName + get() = constructor.declarationDescriptor?.fqNameSafe?.asString() diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/CommonKlibModuleInfo.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/CommonKlibModuleInfo.kt new file mode 100644 index 00000000..a7667009 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/CommonKlibModuleInfo.kt @@ -0,0 +1,25 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.analyzer.common.CommonPlatformAnalyzerServices +import org.jetbrains.kotlin.library.KotlinLibrary +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.CommonPlatforms +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices + +internal class CommonKlibModuleInfo( + override val name: Name, + val kotlinLibrary: KotlinLibrary, + private val dependOnModules: List<ModuleInfo> +) : ModuleInfo { + override fun dependencies(): List<ModuleInfo> = dependOnModules + + override fun dependencyOnBuiltIns(): ModuleInfo.DependencyOnBuiltIns = ModuleInfo.DependencyOnBuiltIns.LAST + + override val platform: TargetPlatform + get() = CommonPlatforms.defaultCommonPlatform + + override val analyzerServices: PlatformDependentAnalyzerServices + get() = CommonPlatformAnalyzerServices +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaJsKlibLibraryInfo.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaJsKlibLibraryInfo.kt new file mode 100644 index 00000000..675bf28e --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaJsKlibLibraryInfo.kt @@ -0,0 +1,30 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.library.KotlinLibrary +import org.jetbrains.kotlin.library.shortName +import org.jetbrains.kotlin.library.uniqueName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.js.JsPlatforms +import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices + +/** TODO: replace by [org.jetbrains.kotlin.caches.resolve.JsKlibLibraryInfo] after fix of KT-40734 */ +internal class DokkaJsKlibLibraryInfo( + override val kotlinLibrary: KotlinLibrary, + override val analyzerServices: PlatformDependentAnalyzerServices, + private val dependencyResolver: DokkaKlibLibraryDependencyResolver +) : DokkaKlibLibraryInfo() { + init { + dependencyResolver.registerLibrary(this) + } + + override val name: Name by lazy { + val libraryName = kotlinLibrary.shortName ?: kotlinLibrary.uniqueName + Name.special("<$libraryName>") + } + + override val platform: TargetPlatform = JsPlatforms.defaultJsPlatform + override fun dependencies(): List<ModuleInfo> = listOf(this) + dependencyResolver.resolveDependencies(this) + override fun getLibraryRoots(): Collection<String> = listOf(libraryRoot) +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaJsResolverForModuleFactory.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaJsResolverForModuleFactory.kt new file mode 100644 index 00000000..b409441b --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaJsResolverForModuleFactory.kt @@ -0,0 +1,125 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KLibService +import org.jetbrains.kotlin.analyzer.* +import org.jetbrains.kotlin.builtins.DefaultBuiltIns +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.container.StorageComponentContainer +import org.jetbrains.kotlin.container.get +import org.jetbrains.kotlin.context.ModuleContext +import org.jetbrains.kotlin.descriptors.PackageFragmentProvider +import org.jetbrains.kotlin.descriptors.impl.CompositePackageFragmentProvider +import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl +import org.jetbrains.kotlin.frontend.di.createContainerForLazyResolve +import org.jetbrains.kotlin.incremental.components.LookupTracker +import org.jetbrains.kotlin.js.resolve.JsPlatformAnalyzerServices +import org.jetbrains.kotlin.konan.util.KlibMetadataFactories +import org.jetbrains.kotlin.resolve.CodeAnalyzerInitializer +import org.jetbrains.kotlin.resolve.SealedClassInheritorsProvider +import org.jetbrains.kotlin.resolve.TargetEnvironment +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import org.jetbrains.kotlin.resolve.lazy.declarations.DeclarationProviderFactoryService +import org.jetbrains.kotlin.serialization.js.DynamicTypeDeserializer +import org.jetbrains.kotlin.serialization.js.KotlinJavascriptSerializationUtil +import org.jetbrains.kotlin.serialization.js.createKotlinJavascriptPackageFragmentProvider +import org.jetbrains.kotlin.serialization.konan.impl.KlibMetadataModuleDescriptorFactoryImpl +import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils +import java.io.File + +/** TODO: replace by [org.jetbrains.kotlin.caches.resolve.JsResolverForModuleFactory] after fix of KT-40734 */ +internal class DokkaJsResolverForModuleFactory( + private val targetEnvironment: TargetEnvironment, + private val kLibService: KLibService +) : ResolverForModuleFactory() { + companion object { + private val metadataFactories = KlibMetadataFactories({ DefaultBuiltIns.Instance }, DynamicTypeDeserializer) + + private val metadataModuleDescriptorFactory = KlibMetadataModuleDescriptorFactoryImpl( + metadataFactories.DefaultDescriptorFactory, + metadataFactories.DefaultPackageFragmentsFactory, + metadataFactories.flexibleTypeDeserializer, + metadataFactories.platformDependentTypeTransformer + ) + } + + override fun <M : ModuleInfo> createResolverForModule( + moduleDescriptor: ModuleDescriptorImpl, + moduleContext: ModuleContext, + moduleContent: ModuleContent<M>, + resolverForProject: ResolverForProject<M>, + languageVersionSettings: LanguageVersionSettings, + sealedInheritorsProvider: SealedClassInheritorsProvider + ): ResolverForModule { + val declarationProviderFactory = DeclarationProviderFactoryService.createDeclarationProviderFactory( + moduleContext.project, + moduleContext.storageManager, + moduleContent.syntheticFiles, + moduleContent.moduleContentScope, + moduleContent.moduleInfo + ) + + val container = createContainerForLazyResolve( + moduleContext, + declarationProviderFactory, + CodeAnalyzerInitializer.getInstance(moduleContext.project).createTrace(), // BindingTraceContext(/* allowSliceRewrite = */ true), + moduleDescriptor.platform!!, + JsPlatformAnalyzerServices, + targetEnvironment, + languageVersionSettings + ) + + var packageFragmentProvider = container.get<ResolveSession>().packageFragmentProvider + + val libraryProviders = createPackageFragmentProvider(moduleContent.moduleInfo, container, moduleContext, moduleDescriptor, languageVersionSettings) + + if (libraryProviders.isNotEmpty()) { + packageFragmentProvider = + CompositePackageFragmentProvider(listOf(packageFragmentProvider) + libraryProviders, "DokkaCompositePackageFragmentProvider") + } + return ResolverForModule(packageFragmentProvider, container) + } + + internal fun <M : ModuleInfo> createPackageFragmentProvider( + moduleInfo: M, + container: StorageComponentContainer, + moduleContext: ModuleContext, + moduleDescriptor: ModuleDescriptorImpl, + languageVersionSettings: LanguageVersionSettings + ): List<PackageFragmentProvider> = when (moduleInfo) { + is DokkaJsKlibLibraryInfo -> { + with(kLibService) { + listOfNotNull( + moduleInfo.kotlinLibrary + .createPackageFragmentProvider( + storageManager = moduleContext.storageManager, + metadataModuleDescriptorFactory = metadataModuleDescriptorFactory, + languageVersionSettings = languageVersionSettings, + moduleDescriptor = moduleDescriptor, + lookupTracker = LookupTracker.DO_NOTHING + ) + ) + } + } + is LibraryModuleInfo -> { + moduleInfo.getLibraryRoots() + .flatMap { + if (File(it).exists()) { + KotlinJavascriptMetadataUtils.loadMetadata(it) + } else { + // TODO can/should we warn a user about a problem in a library root? If so how? + emptyList() + } + } + .filter { it.version.isCompatible() } + .map { metadata -> + val (header, packageFragmentProtos) = + KotlinJavascriptSerializationUtil.readModuleAsProto(metadata.body, metadata.version) + createKotlinJavascriptPackageFragmentProvider( + moduleContext.storageManager, moduleDescriptor, header, packageFragmentProtos, metadata.version, + container.get(), LookupTracker.DO_NOTHING + ) + } + } + else -> emptyList() + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibLibraryDependencyResolver.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibLibraryDependencyResolver.kt new file mode 100644 index 00000000..a07bdc35 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibLibraryDependencyResolver.kt @@ -0,0 +1,17 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.kotlin.library.uniqueName +import org.jetbrains.kotlin.library.unresolvedDependencies + +/** TODO: replace by [NativeKlibLibraryInfo] after fix of KT-40734 */ +internal class DokkaKlibLibraryDependencyResolver { + private val cachedDependencies = mutableMapOf</* libraryName */String, DokkaKlibLibraryInfo>() + + fun registerLibrary(libraryInfo: DokkaKlibLibraryInfo) { + cachedDependencies[libraryInfo.kotlinLibrary.uniqueName] = libraryInfo + } + + fun resolveDependencies(libraryInfo: DokkaKlibLibraryInfo): List<DokkaKlibLibraryInfo> { + return libraryInfo.kotlinLibrary.unresolvedDependencies.mapNotNull { cachedDependencies[it.path] } + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibLibraryInfo.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibLibraryInfo.kt new file mode 100644 index 00000000..3362633d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibLibraryInfo.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.kotlin.analyzer.LibraryModuleInfo +import org.jetbrains.kotlin.library.KotlinLibrary + +internal abstract class DokkaKlibLibraryInfo : LibraryModuleInfo { + abstract val kotlinLibrary: KotlinLibrary + internal val libraryRoot: String + get() = kotlinLibrary.libraryFile.path +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibMetadataCommonDependencyContainer.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibMetadataCommonDependencyContainer.kt new file mode 100644 index 00000000..95f5486d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaKlibMetadataCommonDependencyContainer.kt @@ -0,0 +1,139 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.analyzer.common.CommonDependenciesContainer +import org.jetbrains.kotlin.builtins.DefaultBuiltIns +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.languageVersionSettings +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.PackageFragmentProvider +import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl +import org.jetbrains.kotlin.descriptors.konan.DeserializedKlibModuleOrigin +import org.jetbrains.kotlin.incremental.components.LookupTracker +import org.jetbrains.kotlin.konan.util.KlibMetadataFactories +import org.jetbrains.kotlin.library.KotlinLibrary +import org.jetbrains.kotlin.library.metadata.NativeTypeTransformer +import org.jetbrains.kotlin.library.metadata.NullFlexibleTypeDeserializer +import org.jetbrains.kotlin.library.metadata.parseModuleHeader +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.CompilerDeserializationConfiguration +import org.jetbrains.kotlin.serialization.konan.impl.KlibMetadataModuleDescriptorFactoryImpl +import org.jetbrains.kotlin.storage.LockBasedStorageManager +import org.jetbrains.kotlin.storage.StorageManager + +/** + * Adapted from org.jetbrains.kotlin.cli.metadata.KlibMetadataDependencyContainer + */ +internal class DokkaKlibMetadataCommonDependencyContainer( + kotlinLibraries: List<KotlinLibrary>, + private val configuration: CompilerConfiguration, + private val storageManager: StorageManager +) : CommonDependenciesContainer { + + private val builtIns + get() = DefaultBuiltIns.Instance + + private val mutableDependenciesForAllModuleDescriptors = mutableListOf<ModuleDescriptorImpl>().apply { + add(builtIns.builtInsModule) + } + + private val mutableDependenciesForAllModules = mutableListOf<ModuleInfo>() + + private val moduleDescriptorsForKotlinLibraries: Map<KotlinLibrary, ModuleDescriptorImpl> = + kotlinLibraries.associateBy({ it }) { library -> + val moduleHeader = parseModuleHeader(library.moduleHeaderData) + val moduleName = Name.special(moduleHeader.moduleName) + val moduleOrigin = DeserializedKlibModuleOrigin(library) + MetadataFactories.DefaultDescriptorFactory.createDescriptor( + moduleName, storageManager, builtIns, moduleOrigin + ) + }.also { result -> + val resultValues = result.values + resultValues.forEach { module -> + module.setDependencies(mutableDependenciesForAllModuleDescriptors) + } + mutableDependenciesForAllModuleDescriptors.addAll(resultValues) + } + + private val moduleInfosImpl: List<CommonKlibModuleInfo> = mutableListOf<CommonKlibModuleInfo>().apply { + addAll( + moduleDescriptorsForKotlinLibraries.map { (kotlinLibrary, moduleDescriptor) -> + CommonKlibModuleInfo(moduleDescriptor.name, kotlinLibrary, mutableDependenciesForAllModules) + } + ) + mutableDependenciesForAllModules.addAll(this@apply) + } + + override val moduleInfos: List<ModuleInfo> get() = moduleInfosImpl + + /* not used in Dokka */ + override val friendModuleInfos: List<ModuleInfo> = emptyList() + + /* not used in Dokka */ + override val refinesModuleInfos: List<ModuleInfo> = emptyList() + + override fun moduleDescriptorForModuleInfo(moduleInfo: ModuleInfo): ModuleDescriptor { + if (moduleInfo !in moduleInfos) + error("Unknown module info $moduleInfo") + + // Ensure that the package fragment provider has been created and the module descriptor has been + // initialized with the package fragment provider: + packageFragmentProviderForModuleInfo(moduleInfo) + + return moduleDescriptorsForKotlinLibraries.getValue((moduleInfo as CommonKlibModuleInfo).kotlinLibrary) + } + + override fun registerDependencyForAllModules( + moduleInfo: ModuleInfo, + descriptorForModule: ModuleDescriptorImpl + ) { + mutableDependenciesForAllModules.add(moduleInfo) + mutableDependenciesForAllModuleDescriptors.add(descriptorForModule) + } + + override fun packageFragmentProviderForModuleInfo( + moduleInfo: ModuleInfo + ): PackageFragmentProvider? { + if (moduleInfo !in moduleInfos) + return null + return packageFragmentProviderForKotlinLibrary((moduleInfo as CommonKlibModuleInfo).kotlinLibrary) + } + + private val klibMetadataModuleDescriptorFactory by lazy { + KlibMetadataModuleDescriptorFactoryImpl( + MetadataFactories.DefaultDescriptorFactory, + MetadataFactories.DefaultPackageFragmentsFactory, + MetadataFactories.flexibleTypeDeserializer, + MetadataFactories.platformDependentTypeTransformer + ) + } + + private fun packageFragmentProviderForKotlinLibrary( + library: KotlinLibrary + ): PackageFragmentProvider { + val languageVersionSettings = configuration.languageVersionSettings + + val libraryModuleDescriptor = moduleDescriptorsForKotlinLibraries.getValue(library) + val packageFragmentNames = parseModuleHeader(library.moduleHeaderData).packageFragmentNameList + + return klibMetadataModuleDescriptorFactory.createPackageFragmentProvider( + library, + packageAccessHandler = null, + packageFragmentNames = packageFragmentNames, + storageManager = LockBasedStorageManager("KlibMetadataPackageFragmentProvider"), + moduleDescriptor = libraryModuleDescriptor, + configuration = CompilerDeserializationConfiguration(languageVersionSettings), + compositePackageFragmentAddend = null, + lookupTracker = LookupTracker.DO_NOTHING + ).also { + libraryModuleDescriptor.initialize(it) + } + } +} + +private val MetadataFactories = + KlibMetadataFactories( + { DefaultBuiltIns.Instance }, + NullFlexibleTypeDeserializer, + NativeTypeTransformer() + ) diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaNativeKlibLibraryInfo.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaNativeKlibLibraryInfo.kt new file mode 100644 index 00000000..526815d3 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaNativeKlibLibraryInfo.kt @@ -0,0 +1,50 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.descriptors.ModuleCapability +import org.jetbrains.kotlin.descriptors.konan.DeserializedKlibModuleOrigin +import org.jetbrains.kotlin.descriptors.konan.KlibModuleOrigin +import org.jetbrains.kotlin.library.KotlinLibrary +import org.jetbrains.kotlin.library.isInterop +import org.jetbrains.kotlin.library.shortName +import org.jetbrains.kotlin.library.uniqueName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.konan.NativePlatforms +import org.jetbrains.kotlin.resolve.ImplicitIntegerCoercion +import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices +import java.io.IOException + +/** TODO: replace by [NativeKlibLibraryInfo] after fix of KT-40734 */ +internal class DokkaNativeKlibLibraryInfo( + override val kotlinLibrary: KotlinLibrary, + override val analyzerServices: PlatformDependentAnalyzerServices, + private val dependencyResolver: DokkaKlibLibraryDependencyResolver +) : DokkaKlibLibraryInfo() { + init { + dependencyResolver.registerLibrary(this) + } + + override val name: Name by lazy { + val libraryName = kotlinLibrary.shortName ?: kotlinLibrary.uniqueName + Name.special("<$libraryName>") + } + + override val platform: TargetPlatform = NativePlatforms.unspecifiedNativePlatform + override fun dependencies(): List<ModuleInfo> = listOf(this) + dependencyResolver.resolveDependencies(this) + override fun getLibraryRoots(): Collection<String> = listOf(libraryRoot) + + override val capabilities: Map<ModuleCapability<*>, Any?> + get() { + val capabilities = super.capabilities.toMutableMap() + capabilities[KlibModuleOrigin.CAPABILITY] = DeserializedKlibModuleOrigin(kotlinLibrary) + capabilities[ImplicitIntegerCoercion.MODULE_CAPABILITY] = kotlinLibrary.safeRead(false) { isInterop } + return capabilities + } + + private fun <T> KotlinLibrary.safeRead(defaultValue: T, action: KotlinLibrary.() -> T) = try { + action() + } catch (_: IOException) { + defaultValue + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaNativeResolverForModuleFactory.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaNativeResolverForModuleFactory.kt new file mode 100644 index 00000000..db86b82f --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/resolve/DokkaNativeResolverForModuleFactory.kt @@ -0,0 +1,79 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.resolve + +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KLibService +import org.jetbrains.kotlin.analyzer.* +import org.jetbrains.kotlin.builtins.konan.KonanBuiltIns +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.container.get +import org.jetbrains.kotlin.context.ModuleContext +import org.jetbrains.kotlin.descriptors.impl.CompositePackageFragmentProvider +import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl +import org.jetbrains.kotlin.frontend.di.createContainerForLazyResolve +import org.jetbrains.kotlin.incremental.components.LookupTracker +import org.jetbrains.kotlin.konan.util.KlibMetadataFactories +import org.jetbrains.kotlin.library.metadata.NullFlexibleTypeDeserializer +import org.jetbrains.kotlin.resolve.CodeAnalyzerInitializer +import org.jetbrains.kotlin.resolve.SealedClassInheritorsProvider +import org.jetbrains.kotlin.resolve.TargetEnvironment +import org.jetbrains.kotlin.resolve.konan.platform.NativePlatformAnalyzerServices +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import org.jetbrains.kotlin.resolve.lazy.declarations.DeclarationProviderFactoryService + +/** TODO: replace by [NativeResolverForModuleFactory] after fix of KT-40734 */ +internal class DokkaNativeResolverForModuleFactory( + private val targetEnvironment: TargetEnvironment, + private val kLibService: KLibService, +) : ResolverForModuleFactory() { + companion object { + private val metadataFactories = KlibMetadataFactories(::KonanBuiltIns, NullFlexibleTypeDeserializer) + } + + override fun <M : ModuleInfo> createResolverForModule( + moduleDescriptor: ModuleDescriptorImpl, + moduleContext: ModuleContext, + moduleContent: ModuleContent<M>, + resolverForProject: ResolverForProject<M>, + languageVersionSettings: LanguageVersionSettings, + sealedInheritorsProvider: SealedClassInheritorsProvider + ): ResolverForModule { + + val declarationProviderFactory = DeclarationProviderFactoryService.createDeclarationProviderFactory( + moduleContext.project, + moduleContext.storageManager, + moduleContent.syntheticFiles, + moduleContent.moduleContentScope, + moduleContent.moduleInfo + ) + + val container = createContainerForLazyResolve( + moduleContext, + declarationProviderFactory, + CodeAnalyzerInitializer.getInstance(moduleContext.project).createTrace(), + moduleDescriptor.platform!!, + NativePlatformAnalyzerServices, + targetEnvironment, + languageVersionSettings + ) + + var packageFragmentProvider = container.get<ResolveSession>().packageFragmentProvider + + val klibPackageFragmentProvider = with(kLibService) { + (moduleContent.moduleInfo as? DokkaNativeKlibLibraryInfo) + ?.kotlinLibrary + ?.createPackageFragmentProvider( + storageManager = moduleContext.storageManager, + metadataModuleDescriptorFactory = metadataFactories.DefaultDeserializedDescriptorFactory, + languageVersionSettings = languageVersionSettings, + moduleDescriptor = moduleDescriptor, + lookupTracker = LookupTracker.DO_NOTHING + ) + } + + if (klibPackageFragmentProvider != null) { + packageFragmentProvider = + CompositePackageFragmentProvider(listOf(packageFragmentProvider, klibPackageFragmentProvider), "DokkaCompositePackageFragmentProvider") + } + + return ResolverForModule(packageFragmentProvider, container) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DefaultSamplesTransformer.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DefaultSamplesTransformer.kt new file mode 100644 index 00000000..22fecdf8 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DefaultSamplesTransformer.kt @@ -0,0 +1,35 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtFile + +internal class DefaultSamplesTransformer(context: DokkaContext) : SamplesTransformerImpl(context) { + + override 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).map { it.takeWhile(Char::isWhitespace).count() }.minOrNull() ?: 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 + } + + override fun processImports(psiElement: PsiElement): String { + val psiFile = psiElement.containingFile + return when(val text = (psiFile as? KtFile)?.importList?.text) { + is String -> text + else -> "" + } + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorFullClassHierarchyBuilder.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorFullClassHierarchyBuilder.kt new file mode 100644 index 00000000..13645762 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorFullClassHierarchyBuilder.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl + +import com.intellij.psi.PsiClass +import kotlinx.coroutines.coroutineScope +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.DescriptorDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.utilities.parallelForEach +import org.jetbrains.kotlin.analysis.kotlin.internal.ClassHierarchy +import org.jetbrains.kotlin.analysis.kotlin.internal.FullClassHierarchyBuilder +import org.jetbrains.kotlin.analysis.kotlin.internal.Supertypes +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.typeUtil.immediateSupertypes +import org.jetbrains.kotlin.types.typeUtil.isAnyOrNullableAny +import java.util.concurrent.ConcurrentHashMap + +internal class DescriptorFullClassHierarchyBuilder : FullClassHierarchyBuilder { + + override suspend fun build(module: DModule): ClassHierarchy = coroutineScope { + val map = module.sourceSets.associateWith { ConcurrentHashMap<DRI, List<DRI>>() } + module.packages.parallelForEach { visitDocumentable(it, map) } + map + } + + private suspend fun collectSupertypesFromKotlinType( + driWithKType: Pair<DRI, KotlinType>, + supersMap: MutableMap<DRI, Supertypes> + ): Unit = coroutineScope { + val (dri, kotlinType) = driWithKType + val supertypes = kotlinType.immediateSupertypes().filterNot { it.isAnyOrNullableAny() } + val supertypesDriWithKType = supertypes.mapNotNull { supertype -> + supertype.constructor.declarationDescriptor?.let { + DRI.from(it) to supertype + } + } + + if (supersMap[dri] == null) { + // another thread can rewrite the same value, but it isn't a problem + supersMap[dri] = supertypesDriWithKType.map { it.first } + supertypesDriWithKType.parallelForEach { collectSupertypesFromKotlinType(it, supersMap) } + } + } + + private suspend fun collectSupertypesFromPsiClass( + driWithPsiClass: Pair<DRI, PsiClass>, + supersMap: MutableMap<DRI, Supertypes> + ): Unit = coroutineScope { + 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) { + // another thread can rewrite the same value, but it isn't a problem + supersMap[dri] = supertypesDriWithPsiClass.map { it.first } + supertypesDriWithPsiClass.parallelForEach { collectSupertypesFromPsiClass(it, supersMap) } + } + } + + private suspend fun visitDocumentable( + documentable: Documentable, + hierarchy: SourceSetDependent<MutableMap<DRI, List<DRI>>> + ): Unit = coroutineScope { + if (documentable is WithScope) { + documentable.classlikes.parallelForEach { 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 DescriptorDocumentableSource) { + val descriptor = source.descriptor as ClassDescriptor + val type = descriptor.defaultType + hierarchy[sourceSet]?.let { collectSupertypesFromKotlinType(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-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorInheritanceBuilder.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorInheritanceBuilder.kt new file mode 100644 index 00000000..15dd8f1d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorInheritanceBuilder.kt @@ -0,0 +1,91 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl + +import com.intellij.psi.PsiClass +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.DescriptorDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.WithSources +import org.jetbrains.kotlin.analysis.kotlin.internal.InheritanceBuilder +import org.jetbrains.kotlin.analysis.kotlin.internal.InheritanceNode +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.resolve.DescriptorUtils.getClassDescriptorForType + +internal class DescriptorInheritanceBuilder : InheritanceBuilder { + + override fun build(documentables: Map<DRI, Documentable>): List<InheritanceNode> { + val descriptorMap = getDescriptorMap(documentables) + + val psiInheritanceTree = + documentables.flatMap { (_, v) -> (v as? WithSources)?.sources?.values.orEmpty() } + .filterIsInstance<PsiDocumentableSource>().mapNotNull { it.psi as? PsiClass } + .flatMap(::gatherPsiClasses) + .flatMap { entry -> entry.second.map { it to entry.first } } + .let { + it + it.map { it.second to null } + } + .groupBy({ it.first }) { it.second } + .map { it.key to it.value.filterNotNull().distinct() } + .map { (k, v) -> + InheritanceNode( + DRI.from(k), + v.map { InheritanceNode(DRI.from(it)) }, + k.supers.filter { it.isInterface }.map { DRI.from(it) }, + k.isInterface + ) + + } + + val descriptorInheritanceTree = descriptorMap.flatMap { (_, v) -> + v.typeConstructor.supertypes + .map { getClassDescriptorForType(it) to v } + } + .let { + it + it.map { it.second to null } + } + .groupBy({ it.first }) { it.second } + .map { it.key to it.value.filterNotNull().distinct() } + .map { (k, v) -> + InheritanceNode( + DRI.from(k), + v.map { InheritanceNode(DRI.from(it)) }, + k.typeConstructor.supertypes.map { getClassDescriptorForType(it) } + .mapNotNull { cd -> cd.takeIf { it.kind == ClassKind.INTERFACE }?.let { DRI.from(it) } }, + isInterface = k.kind == ClassKind.INTERFACE + ) + } + + return psiInheritanceTree + descriptorInheritanceTree + } + + private fun gatherPsiClasses(psi: PsiClass): List<Pair<PsiClass, List<PsiClass>>> = psi.supers.toList().let { l -> + listOf(psi to l) + l.flatMap { gatherPsiClasses(it) } + } + + private fun getDescriptorMap(documentables: Map<DRI, Documentable>): Map<DRI, ClassDescriptor> { + val map: MutableMap<DRI, ClassDescriptor> = mutableMapOf() + documentables + .mapNotNull { (k, v) -> + v.descriptorForPlatform()?.let { k to it }?.also { (k, v) -> map[k] = v } + }.map { it.second }.forEach { gatherSupertypes(it, map) } + + return map.toMap() + } + + private fun gatherSupertypes(descriptor: ClassDescriptor, map: MutableMap<DRI, ClassDescriptor>) { + map.putIfAbsent(DRI.from(descriptor), descriptor) + descriptor.typeConstructor.supertypes.map { getClassDescriptorForType(it) } + .forEach { gatherSupertypes(it, map) } + } + + + private fun Documentable?.descriptorForPlatform(platform: Platform = Platform.jvm) = + (this as? WithSources).descriptorForPlatform(platform) + + private fun WithSources?.descriptorForPlatform(platform: Platform = Platform.jvm) = this?.let { + it.sources.entries.find { it.key.analysisPlatform == platform }?.value?.let { it as? DescriptorDocumentableSource }?.descriptor as? ClassDescriptor + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorKotlinToJavaMapper.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorKotlinToJavaMapper.kt new file mode 100644 index 00000000..394a9863 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorKotlinToJavaMapper.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.kotlin.analysis.kotlin.internal.KotlinToJavaService +import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +internal class DescriptorKotlinToJavaMapper : KotlinToJavaService { + + override fun findAsJava(kotlinDri: DRI): DRI? { + return kotlinDri.partialFqName().mapToJava()?.toDRI(kotlinDri) + } + + private fun DRI.partialFqName() = packageName?.let { "$it." } + classNames + + private fun String.mapToJava(): ClassId? = + JavaToKotlinClassMap.mapKotlinToJava(FqName(this).toUnsafe()) + + private fun ClassId.toDRI(dri: DRI?): DRI = DRI( + packageName = packageFqName.asString(), + classNames = classNames(), + callable = dri?.callable,//?.asJava(), TODO: check this + extra = null, + target = PointingToDeclaration + ) + + private fun ClassId.classNames(): String = + shortClassName.identifier + (outerClassId?.classNames()?.let { ".$it" } ?: "") +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorSyntheticDocumentableDetector.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorSyntheticDocumentableDetector.kt new file mode 100644 index 00000000..34f9bba1 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorSyntheticDocumentableDetector.kt @@ -0,0 +1,33 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.DescriptorDocumentableSource +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.WithSources +import org.jetbrains.kotlin.analysis.kotlin.internal.SyntheticDocumentableDetector +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor + +internal class DescriptorSyntheticDocumentableDetector : SyntheticDocumentableDetector { + override fun isSynthetic(documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet): Boolean { + return isFakeOverride(documentable, sourceSet) || isSynthesized(documentable, sourceSet) + } + + private fun isFakeOverride(documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet): Boolean { + return callableMemberDescriptorOrNull(documentable, sourceSet)?.kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE + } + + private fun isSynthesized(documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet): Boolean { + return callableMemberDescriptorOrNull(documentable, sourceSet)?.kind == CallableMemberDescriptor.Kind.SYNTHESIZED + } + + private fun callableMemberDescriptorOrNull( + documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet + ): CallableMemberDescriptor? { + if (documentable is WithSources) { + return documentable.sources[sourceSet] + .let { it as? DescriptorDocumentableSource }?.descriptor as? CallableMemberDescriptor + } + + return null + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/SamplesTransformerImpl.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/SamplesTransformerImpl.kt new file mode 100644 index 00000000..f1924708 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/SamplesTransformerImpl.kt @@ -0,0 +1,153 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl + +import com.intellij.psi.PsiElement +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.SamplesKotlinAnalysis +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.model.doc.Sample +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.lazy.ResolveSession + +internal const val KOTLIN_PLAYGROUND_SCRIPT = "<script src=\"`https://unpkg.com/kotlin-playground@1`\"></script>" + +internal abstract class SamplesTransformerImpl(val context: DokkaContext) : PageTransformer { + + private val kDocFinder: KDocFinder = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kdocFinder } + + abstract fun processBody(psiElement: PsiElement): String + abstract fun processImports(psiElement: PsiElement): String + + final override fun invoke(input: RootPageNode): RootPageNode = + /** + * Run from the thread of [Dispatchers.Default]. It can help to avoid a memory leaks in `ThreadLocal`s (that keep `URLCLassLoader`) + * since we shut down Dispatchers. Default at the end of each task (see [org.jetbrains.dokka.DokkaConfiguration.finalizeCoroutines]). + * Currently, all `ThreadLocal`s are in a compiler/IDE codebase. + */ + runBlocking(Dispatchers.Default) { + val analysis = SamplesKotlinAnalysis( + sourceSets = context.configuration.sourceSets, + context = context, + projectKotlinAnalysis = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kotlinAnalysis } + ) + analysis.use { + input.transformContentPagesTree { page -> + val samples = (page as? WithDocumentables)?.documentables?.flatMap { + it.documentation.entries.flatMap { entry -> + entry.value.children.filterIsInstance<Sample>().map { entry.key to it } + } + } + + samples?.fold(page as ContentPage) { acc, (sampleSourceSet, sample) -> + acc.modified( + content = acc.content.addSample(page, sampleSourceSet, sample.name, it), + embeddedResources = acc.embeddedResources + KOTLIN_PLAYGROUND_SCRIPT + ) + } ?: page + } + } + } + + private fun ContentNode.addSample( + contentPage: ContentPage, + sourceSet: DokkaSourceSet, + fqName: String, + analysis: KotlinAnalysis + ): ContentNode { + val resolveSession = analysis[sourceSet].resolveSession + val psiElement = fqNameToPsiElement(resolveSession, fqName, sourceSet) + ?: return this.also { context.logger.warn("Cannot find PsiElement corresponding to $fqName") } + val imports = + processImports(psiElement) + val body = processBody(psiElement) + val node = contentCode(contentPage.content.sourceSets, contentPage.dri, createSampleBody(imports, body), "kotlin") + + return dfs(fqName, node) + } + + protected open fun createSampleBody(imports: String, body: String) = + """ |$imports + |fun main() { + | //sampleStart + | $body + | //sampleEnd + |}""".trimMargin() + + private fun ContentNode.dfs(fqName: String, node: ContentCodeBlock): ContentNode { + return when (this) { + is ContentHeader -> copy(children.map { it.dfs(fqName, node) }) + is ContentDivergentGroup -> @Suppress("UNCHECKED_CAST") copy(children.map { + it.dfs(fqName, node) + } as List<ContentDivergentInstance>) + is ContentDivergentInstance -> copy( + before.let { it?.dfs(fqName, node) }, + divergent.dfs(fqName, node), + after.let { it?.dfs(fqName, node) }) + is ContentCodeBlock -> copy(children.map { it.dfs(fqName, node) }) + is ContentCodeInline -> copy(children.map { it.dfs(fqName, node) }) + is ContentDRILink -> copy(children.map { it.dfs(fqName, node) }) + is ContentResolvedLink -> copy(children.map { it.dfs(fqName, node) }) + is ContentEmbeddedResource -> copy(children.map { it.dfs(fqName, node) }) + is ContentTable -> copy(children = children.map { it.dfs(fqName, node) as ContentGroup }) + is ContentList -> copy(children.map { it.dfs(fqName, node) }) + is ContentGroup -> copy(children.map { it.dfs(fqName, node) }) + is PlatformHintedContent -> copy(inner.dfs(fqName, node)) + is ContentText -> if (text == fqName) node else this + is ContentBreakLine -> this + else -> this.also { context.logger.error("Could not recognize $this ContentNode in SamplesTransformer") } + } + } + + private fun fqNameToPsiElement(resolveSession: ResolveSession, functionName: String, dokkaSourceSet: DokkaSourceSet): PsiElement? { + val packageName = functionName.takeWhile { it != '.' } + val descriptor = resolveSession.getPackageFragment(FqName(packageName)) + ?: return null.also { context.logger.warn("Cannot find descriptor for package $packageName") } + + with (kDocFinder) { + val symbol = resolveKDocLink( + descriptor, + functionName, + dokkaSourceSet, + emptyBindingContext = true + ).firstOrNull() ?: return null.also { context.logger.warn("Unresolved function $functionName in @sample") } + return DescriptorToSourceUtils.descriptorToDeclaration(symbol) + } + } + + private fun contentCode( + sourceSets: Set<DisplaySourceSet>, + dri: Set<DRI>, + content: String, + language: String, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty() + ) = + ContentCodeBlock( + children = listOf( + ContentText( + text = content, + dci = DCI(dri, ContentKind.Sample), + sourceSets = sourceSets, + style = emptySet(), + extra = PropertyContainer.empty() + ) + ), + language = language, + dci = DCI(dri, ContentKind.Sample), + sourceSets = sourceSets, + style = styles + ContentStyle.RunnableSample + TextStyle.Monospace, + extra = extra + ) +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/IllegalModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/IllegalModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..0d5fe5c5 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/IllegalModuleAndPackageDocumentation.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs + +import org.jetbrains.dokka.DokkaException + +internal class IllegalModuleAndPackageDocumentation( + source: ModuleAndPackageDocumentationSource, message: String +) : DokkaException("[$source] $message") diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..0aaea9c8 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentation.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.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-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationFragment.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationFragment.kt new file mode 100644 index 00000000..c0df713b --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationFragment.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs + + +internal data class ModuleAndPackageDocumentationFragment( + val name: String, + val classifier: ModuleAndPackageDocumentation.Classifier, + val documentation: String, + val source: ModuleAndPackageDocumentationSource +) diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationParsingContext.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationParsingContext.kt new file mode 100644 index 00000000..f6ce66d6 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationParsingContext.kt @@ -0,0 +1,71 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.from +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs.ModuleAndPackageDocumentation.Classifier.Module +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs.ModuleAndPackageDocumentation.Classifier.Package +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +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, + moduleDescriptor: ModuleDescriptor? = null, + kDocFinder: KDocFinder? = null, + sourceSet: DokkaConfiguration.DokkaSourceSet? = null +) = ModuleAndPackageDocumentationParsingContext { fragment, sourceLocation -> + val descriptor = when (fragment.classifier) { + Module -> moduleDescriptor?.getPackage(FqName.topLevel(Name.identifier(""))) + Package -> moduleDescriptor?.getPackage(FqName(fragment.name)) + } + + val externalDri = { link: String -> + try { + if (kDocFinder != null && descriptor != null && sourceSet != null) { + with(kDocFinder) { + resolveKDocLink( + descriptor, + link, + sourceSet + ).sorted().firstOrNull()?.let { + DRI.from( + it + ) + } + } + } else null + } catch (e1: IllegalArgumentException) { + logger.warn("Couldn't resolve link for $link") + null + } + } + + MarkdownParser(externalDri = externalDri, sourceLocation) +} + +private fun Collection<DeclarationDescriptor>.sorted() = sortedWith( + compareBy( + { it is ClassDescriptor }, + { (it as? FunctionDescriptor)?.name }, + { (it as? FunctionDescriptor)?.valueParameters?.size }, + { (it as? FunctionDescriptor)?.valueParameters?.joinToString { it.type.toString() } } + ) +) diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationReader.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationReader.kt new file mode 100644 index 00000000..66bbf1a8 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationReader.kt @@ -0,0 +1,113 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs.ModuleAndPackageDocumentation.Classifier +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.kotlin.analysis.kotlin.internal.ModuleAndPackageDocumentationReader + +internal fun ModuleAndPackageDocumentationReader(context: DokkaContext): ModuleAndPackageDocumentationReader = + ContextModuleAndPackageDocumentationReader(context) + +private class ContextModuleAndPackageDocumentationReader( + private val context: DokkaContext +) : ModuleAndPackageDocumentationReader { + + private val kotlinAnalysis: KotlinAnalysis = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kotlinAnalysis } + private val kdocFinder: KDocFinder = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kdocFinder } + + 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) + val moduleDescriptor = kotlinAnalysis[sourceSet].moduleDescriptor + val documentations = fragments.map { fragment -> + parseModuleAndPackageDocumentation( + context = ModuleAndPackageDocumentationParsingContext(context.logger, moduleDescriptor, kdocFinder, 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-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationSource.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationSource.kt new file mode 100644 index 00000000..18105be0 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/ModuleAndPackageDocumentationSource.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.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-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/parseModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/parseModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..59b7d2e9 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/parseModuleAndPackageDocumentation.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.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-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/parseModuleAndPackageDocumentationFragments.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/parseModuleAndPackageDocumentationFragments.kt new file mode 100644 index 00000000..32f636ff --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/moduledocs/parseModuleAndPackageDocumentationFragments.kt @@ -0,0 +1,55 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs + +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs.ModuleAndPackageDocumentation.Classifier.Module +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.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-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorDocumentationContent.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorDocumentationContent.kt new file mode 100644 index 00000000..5adf1194 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorDocumentationContent.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.java + +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.dokka.analysis.java.JavadocTag +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag + +internal data class DescriptorDocumentationContent( + val descriptor: DeclarationDescriptor, + val element: KDocTag, + override val tag: JavadocTag, +) : DocumentationContent { + override fun resolveSiblings(): List<DocumentationContent> { + return listOf(this) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocComment.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocComment.kt new file mode 100644 index 00000000..da7d5140 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocComment.kt @@ -0,0 +1,79 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.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.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag + +internal class DescriptorKotlinDocComment( + val comment: KDocTag, + val descriptor: DeclarationDescriptor +) : 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(descriptor, 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(descriptor, it, tag) } + } + + private fun resolveGeneric(tag: JavadocTag): List<DescriptorDocumentationContent> { + return comment.children.mapNotNull { element -> + if (element is KDocTag && element.name == tag.name) { + DescriptorDocumentationContent(descriptor, element, tag) + } else { + null + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DescriptorKotlinDocComment + + if (comment != other.comment) return false + if (descriptor.name != other.descriptor.name) return false + if (tagsWithContent != other.tagsWithContent) return false + + return true + } + + override fun hashCode(): Int { + var result = comment.hashCode() + result = 31 * result + descriptor.name.hashCode() + result = 31 * result + tagsWithContent.hashCode() + return result + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocCommentCreator.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocCommentCreator.kt new file mode 100644 index 00000000..b191355d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocCommentCreator.kt @@ -0,0 +1,26 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.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.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.DescriptorFinder +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtElement + +internal class DescriptorKotlinDocCommentCreator( + private val kdocFinder: KDocFinder, + private val descriptorFinder: DescriptorFinder +) : DocCommentCreator { + override fun create(element: PsiNamedElement): DocComment? { + val ktElement = element.navigationElement as? KtElement ?: return null + val kdoc = with (kdocFinder) { + ktElement.findKDoc() + } ?: return null + val descriptor = with (descriptorFinder) { + (element.navigationElement as? KtDeclaration)?.findDescriptor() + } ?: return null + + return DescriptorKotlinDocComment(kdoc, descriptor) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocCommentParser.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocCommentParser.kt new file mode 100644 index 00000000..28565f2d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/DescriptorKotlinDocCommentParser.kt @@ -0,0 +1,54 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.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.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.from +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator.parseFromKDocTag +import org.jetbrains.dokka.links.DRI +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.dokka.utilities.DokkaLogger + +internal class DescriptorKotlinDocCommentParser( + private val context: DokkaContext, + private val logger: DokkaLogger +) : DocCommentParser { + + override fun canParse(docComment: DocComment): Boolean { + return docComment is DescriptorKotlinDocComment + } + + override fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode { + val kotlinDocComment = docComment as DescriptorKotlinDocComment + return parseDocumentation(kotlinDocComment) + } + + fun parseDocumentation(element: DescriptorKotlinDocComment, parseWithChildren: Boolean = true): DocumentationNode { + val sourceSet = context.configuration.sourceSets.let { sourceSets -> + sourceSets.firstOrNull { it.sourceSetID.sourceSetName == "jvmMain" } + ?: sourceSets.first { it.analysisPlatform == Platform.jvm } + } + val kdocFinder = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kdocFinder } + return parseFromKDocTag( + kDocTag = element.comment, + externalDri = { link: String -> + try { + kdocFinder.resolveKDocLink(element.descriptor, link, sourceSet) + .firstOrNull() + ?.let { DRI.from(it) } + } catch (e1: IllegalArgumentException) { + logger.warn("Couldn't resolve link for $link") + null + } + }, + kdocLocation = null, + parseWithChildren = parseWithChildren + ) + } +} + diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinAnalysisProjectProvider.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinAnalysisProjectProvider.kt new file mode 100644 index 00000000..72151a72 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinAnalysisProjectProvider.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.java + +import com.intellij.openapi.project.Project +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.ProjectProvider +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +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<CompilerDescriptorAnalysisPlugin>().querySingle { kotlinAnalysis } + return kotlinAnalysis[sourceSet].project + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinAnalysisSourceRootsExtractor.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinAnalysisSourceRootsExtractor.kt new file mode 100644 index 00000000..ed6b8c53 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinAnalysisSourceRootsExtractor.kt @@ -0,0 +1,27 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.java + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.SourceRootsExtractor +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import java.io.File + +internal class KotlinAnalysisSourceRootsExtractor : SourceRootsExtractor { + + override fun extract(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): List<File> { + val kotlinAnalysis = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kotlinAnalysis } + val environment = kotlinAnalysis[sourceSet].environment + return environment.configuration.get(CLIConfigurationKeys.CONTENT_ROOTS) + ?.filterIsInstance<JavaSourceRoot>() + ?.mapNotNull { it.file.takeIf { isFileInSourceRoots(it, sourceSet) } } + ?: listOf() + } + + private fun isFileInSourceRoots(file: File, sourceSet: DokkaConfiguration.DokkaSourceSet): Boolean = + sourceSet.sourceRoots.any { root -> file.startsWith(root) } + +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinInheritDocTagContentProvider.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinInheritDocTagContentProvider.kt new file mode 100644 index 00000000..3c92fc65 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/java/KotlinInheritDocTagContentProvider.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.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: DescriptorKotlinDocCommentParser by lazy { + context.plugin<JavaAnalysisPlugin>().query { docCommentParsers } + .single { it is DescriptorKotlinDocCommentParser } as DescriptorKotlinDocCommentParser + } + + 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( + DescriptorKotlinDocComment(descriptorContent.element, descriptorContent.descriptor), + parseWithChildren = false + ) + val id = docTagParserContext.store(inheritedDocNode) + return """<inheritdoc id="$id"/>""" + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/CollectionExtensions.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/CollectionExtensions.kt new file mode 100644 index 00000000..e1dec28c --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/CollectionExtensions.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +internal inline fun <T, R : Any> Iterable<T>.firstNotNullOfOrNull(transform: (T) -> R?): R? { + for (element in this) { + val result = transform(element) + if (result != null) { + return result + } + } + return null +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultDescriptorToDocumentableTranslator.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultDescriptorToDocumentableTranslator.kt new file mode 100644 index 00000000..7633a93f --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultDescriptorToDocumentableTranslator.kt @@ -0,0 +1,1275 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiLiteralUtil.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.java.parsers.JavadocParser +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.AnalysisContext +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.DescriptorDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.from +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.AnnotationTarget +import org.jetbrains.dokka.model.Nullable +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.dokka.utilities.parallelMap +import org.jetbrains.dokka.utilities.parallelMapNotNull +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.builtins.functions.FunctionClassDescriptor +import org.jetbrains.kotlin.builtins.isBuiltinExtensionFunctionalType +import org.jetbrains.kotlin.builtins.isExtensionFunctionType +import org.jetbrains.kotlin.builtins.isSuspendFunctionTypeOrSubtype +import org.jetbrains.kotlin.codegen.isJvmStaticInObjectOrClassOrInterface +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.descriptors.java.JavaVisibilities +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.calls.components.isVararg +import org.jetbrains.kotlin.resolve.calls.util.getValueArgumentsInParentheses +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.constants.KClassValue.Value.LocalClass +import org.jetbrains.kotlin.resolve.constants.KClassValue.Value.NormalClass +import org.jetbrains.kotlin.resolve.descriptorUtil.annotationClass +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull +import org.jetbrains.kotlin.resolve.descriptorUtil.parents +import org.jetbrains.kotlin.resolve.scopes.MemberScope +import org.jetbrains.kotlin.resolve.scopes.StaticScopeForKotlinEnum +import org.jetbrains.kotlin.resolve.source.KotlinSourceElement +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.resolve.source.PsiSourceFile +import org.jetbrains.kotlin.types.* +import org.jetbrains.kotlin.types.typeUtil.immediateSupertypes +import org.jetbrains.kotlin.types.typeUtil.isAnyOrNullableAny +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import java.nio.file.Paths +import org.jetbrains.kotlin.resolve.constants.AnnotationValue as ConstantsAnnotationValue +import org.jetbrains.kotlin.resolve.constants.ArrayValue as ConstantsArrayValue +import org.jetbrains.kotlin.resolve.constants.BooleanValue as ConstantsBooleanValue +import org.jetbrains.kotlin.resolve.constants.DoubleValue as ConstantsDoubleValue +import org.jetbrains.kotlin.resolve.constants.EnumValue as ConstantsEnumValue +import org.jetbrains.kotlin.resolve.constants.FloatValue as ConstantsFloatValue +import org.jetbrains.kotlin.resolve.constants.IntValue as ConstantsIntValue +import org.jetbrains.kotlin.resolve.constants.KClassValue as ConstantsKtClassValue +import org.jetbrains.kotlin.resolve.constants.LongValue as ConstantsLongValue +import org.jetbrains.kotlin.resolve.constants.NullValue as ConstantsNullValue +import org.jetbrains.kotlin.resolve.constants.UIntValue as ConstantsUIntValue +import org.jetbrains.kotlin.resolve.constants.ULongValue as ConstantsULongValue + +internal class DefaultDescriptorToDocumentableTranslator( + private val context: DokkaContext +) : AsyncSourceToDocumentableTranslator, ExternalClasslikesTranslator { + + private val kotlinAnalysis: KotlinAnalysis = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kotlinAnalysis } + private val kdocFinder: KDocFinder = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kdocFinder } + + override suspend fun invokeSuspending(sourceSet: DokkaSourceSet, context: DokkaContext): DModule { + val analysisContext = kotlinAnalysis[sourceSet] + val environment = analysisContext.environment + val packageFragments = environment.getSourceFiles().asSequence() + .map { it.packageFqName } + .distinct() + .mapNotNull { analysisContext.resolveSession.getPackageFragment(it) } + .toList() + + val javadocParser = JavadocParser( + docCommentParsers = context.plugin<JavaAnalysisPlugin>().query { docCommentParsers }, + docCommentFinder = context.plugin<JavaAnalysisPlugin>().docCommentFinder + ) + + + return DokkaDescriptorVisitor(sourceSet, kdocFinder, kotlinAnalysis[sourceSet], context.logger, javadocParser).run { + packageFragments.parallelMap { + visitPackageFragmentDescriptor( + it + ) + } + }.let { + DModule( + name = context.configuration.moduleName, + packages = it, + documentation = emptyMap(), + expectPresentInSet = null, + sourceSets = setOf(sourceSet) + ) + } + } + + override fun translateClassDescriptor(descriptor: ClassDescriptor, sourceSet: DokkaSourceSet): DClasslike { + val driInfo = DRI.from(descriptor.parents.first()).withEmptyInfo() + + val javadocParser = JavadocParser( + docCommentParsers = context.plugin<JavaAnalysisPlugin>().query { docCommentParsers }, + docCommentFinder = context.plugin<JavaAnalysisPlugin>().docCommentFinder + ) + + return runBlocking(Dispatchers.Default) { + DokkaDescriptorVisitor(sourceSet, kdocFinder, kotlinAnalysis[sourceSet], context.logger, javadocParser) + .visitClassDescriptor(descriptor, driInfo) + } + } +} + +internal data class DRIWithPlatformInfo( + val dri: DRI, + val actual: SourceSetDependent<DocumentableSource> +) + +internal fun DRI.withEmptyInfo() = DRIWithPlatformInfo(this, emptyMap()) + +private class DokkaDescriptorVisitor( + private val sourceSet: DokkaSourceSet, + private val kDocFinder: KDocFinder, + private val analysisContext: AnalysisContext, + private val logger: DokkaLogger, + private val javadocParser: JavadocParser +) { + private val syntheticDocProvider = SyntheticDescriptorDocumentationProvider(kDocFinder, sourceSet) + + private fun Collection<DeclarationDescriptor>.filterDescriptorsInSourceSet() = filter { + it.toSourceElement.containingFile.toString().let { path -> + path.isNotBlank() && sourceSet.sourceRoots.any { root -> + Paths.get(path).startsWith(root.toPath()) + } + } + } + + private fun <T> T.toSourceSetDependent() = if (this != null) mapOf(sourceSet to this) else emptyMap() + + suspend fun visitPackageFragmentDescriptor(descriptor: PackageFragmentDescriptor): DPackage { + val name = descriptor.fqName.asString().takeUnless { it.isBlank() } ?: "" + val driWithPlatform = DRI(packageName = name).withEmptyInfo() + val scope = descriptor.getMemberScope() + return coroutineScope { + val descriptorsWithKind = scope.getDescriptorsWithKind(true) + + val functions = async { descriptorsWithKind.functions.visitFunctions(driWithPlatform) } + val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform) } + val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) } + val typealiases = async { descriptorsWithKind.typealiases.visitTypealiases() } + + DPackage( + dri = driWithPlatform.dri, + functions = functions.await(), + properties = properties.await(), + classlikes = classlikes.await(), + typealiases = typealiases.await(), + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet) + ) + } + } + + suspend fun visitClassDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DClasslike = + when (descriptor.kind) { + ClassKind.ENUM_CLASS -> enumDescriptor(descriptor, parent) + ClassKind.OBJECT -> objectDescriptor(descriptor, parent) + ClassKind.INTERFACE -> interfaceDescriptor(descriptor, parent) + ClassKind.ANNOTATION_CLASS -> annotationDescriptor(descriptor, parent) + else -> classDescriptor(descriptor, parent) + } + + private suspend fun interfaceDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DInterface { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + val info = descriptor.resolveClassDescriptionData() + + return coroutineScope { + val descriptorsWithKind = scope.getDescriptorsWithKind() + + val functions = async { descriptorsWithKind.functions.visitFunctions(driWithPlatform) } + val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform) } + val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) } + val generics = async { descriptor.declaredTypeParameters.parallelMap { it.toVariantTypeParameter() } } + + DInterface( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + functions = functions.await(), + properties = properties.await(), + classlikes = classlikes.await(), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + generics = generics.await(), + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.ancestry.allImplementedInterfaces().toSourceSetDependent()), + info.ancestry.exceptionInSupertypesOrNull() + ) + ) + } + } + + private suspend fun objectDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DObject { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + val info = descriptor.resolveClassDescriptionData() + + + return coroutineScope { + val descriptorsWithKind = scope.getDescriptorsWithKind() + + val functions = async { descriptorsWithKind.functions.visitFunctions(driWithPlatform) } + val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform) } + val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) } + + DObject( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + functions = functions.await(), + properties = properties.await(), + classlikes = classlikes.await(), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.ancestry.allImplementedInterfaces().toSourceSetDependent()), + info.ancestry.exceptionInSupertypesOrNull() + ) + ) + } + + + } + + private suspend fun enumDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DEnum { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + val info = descriptor.resolveClassDescriptionData() + + return coroutineScope { + val descriptorsWithKind = descriptor.getEnumDescriptorsWithKind() + + val functions = async { descriptorsWithKind.functions.visitFunctions(driWithPlatform) } + val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform) } + val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) } + val constructors = + async { descriptor.constructors.parallelMap { visitConstructorDescriptor(it, driWithPlatform) } } + val entries = async { descriptorsWithKind.enumEntries.visitEnumEntries(driWithPlatform) } + + DEnum( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + entries = entries.await(), + constructors = constructors.await(), + functions = functions.await(), + properties = properties.await(), + classlikes = classlikes.await(), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.ancestry.allImplementedInterfaces().toSourceSetDependent()) + ) + ) + } + } + + private fun ClassDescriptor.getEnumDescriptorsWithKind(): DescriptorsWithKind { + val descriptorsWithKind = this.unsubstitutedMemberScope.getDescriptorsWithKind() + val staticScopeForKotlinEnum = (this.staticScope as? StaticScopeForKotlinEnum) ?: return descriptorsWithKind + + // synthetic values() and valueOf() functions are not present among average class functions + val enumSyntheticFunctions = staticScopeForKotlinEnum.getContributedDescriptors { true } + .filterIsInstance<FunctionDescriptor>() + + return descriptorsWithKind.copy(functions = descriptorsWithKind.functions + enumSyntheticFunctions) + } + + private suspend fun visitEnumEntryDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DEnumEntry { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + + return coroutineScope { + val descriptorsWithKind = scope.getDescriptorsWithKind() + + val functions = async { descriptorsWithKind.functions.visitFunctions(driWithPlatform) } + val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform) } + val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) } + + DEnumEntry( + dri = driWithPlatform.dri.withEnumEntryExtra(), + name = descriptor.name.asString(), + documentation = descriptor.resolveDescriptorData(), + functions = functions.await(), + properties = properties.await(), + classlikes = classlikes.await(), + sourceSets = setOf(sourceSet), + expectPresentInSet = sourceSet.takeIf { isExpect }, + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ) + ) + } + } + + private suspend fun annotationDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DAnnotation { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + + return coroutineScope { + val descriptorsWithKind = scope.getDescriptorsWithKind() + + val functions = async { descriptorsWithKind.functions.visitFunctions(driWithPlatform) } + val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform) } + val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) } + val generics = async { descriptor.declaredTypeParameters.parallelMap { it.toVariantTypeParameter() } } + val constructors = + async { descriptor.constructors.parallelMap { visitConstructorDescriptor(it, driWithPlatform) } } + + DAnnotation( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + documentation = descriptor.resolveDescriptorData(), + functions = functions.await(), + properties = properties.await(), + classlikes = classlikes.await(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ), + companion = descriptor.companionObjectDescriptor?.let { objectDescriptor(it, driWithPlatform) }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = generics.await(), + constructors = constructors.await(), + sources = descriptor.createSources() + ) + } + + + } + + private suspend fun classDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DClass { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + val info = descriptor.resolveClassDescriptionData() + val actual = descriptor.createSources() + + return coroutineScope { + val descriptorsWithKind = scope.getDescriptorsWithKind() + + val (regularFunctions, accessors) = splitFunctionsAndInheritedAccessors( + properties = descriptorsWithKind.properties, + functions = descriptorsWithKind.functions + ) + + val functions = async { regularFunctions.visitFunctions(driWithPlatform) } + val properties = async { descriptorsWithKind.properties.visitProperties(driWithPlatform, accessors) } + val classlikes = async { descriptorsWithKind.classlikes.visitClasslikes(driWithPlatform) } + val generics = async { descriptor.declaredTypeParameters.parallelMap { it.toVariantTypeParameter() } } + val constructors = async { + descriptor.constructors.parallelMap { + visitConstructorDescriptor( + it, + if (it.isPrimary) DRIWithPlatformInfo(driWithPlatform.dri, actual) + else DRIWithPlatformInfo(driWithPlatform.dri, emptyMap()) + ) + } + } + + DClass( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + constructors = constructors.await(), + functions = functions.await(), + properties = properties.await(), + classlikes = classlikes.await(), + sources = actual, + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + generics = generics.await(), + documentation = info.docs, + modifier = descriptor.modifier().toSourceSetDependent(), + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.ancestry.allImplementedInterfaces().toSourceSetDependent()), + info.ancestry.exceptionInSupertypesOrNull() + ) + ) + } + } + + /** + * @param implicitAccessors getters/setters that are not part of the property descriptor, for instance + * average methods inherited from java sources that access the property + */ + private suspend fun visitPropertyDescriptor( + originalDescriptor: PropertyDescriptor, + implicitAccessors: DescriptorAccessorHolder?, + parent: DRIWithPlatformInfo + ): DProperty { + val (dri, _) = originalDescriptor.createDRI() + val inheritedFrom = dri.getInheritedFromDRI(parent) + val descriptor = originalDescriptor.getConcreteDescriptor() + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + + val actual = originalDescriptor.createSources() + + // example - generated getter that comes with data classes + suspend fun getDescriptorGetter() = + descriptor.accessors + .firstIsInstanceOrNull<PropertyGetterDescriptor>() + ?.let { + visitPropertyAccessorDescriptor(it, descriptor, dri, inheritedFrom) + } + + suspend fun getImplicitAccessorGetter() = + implicitAccessors?.getter?.let { visitFunctionDescriptor(it, parent) } + + // example - generated setter that comes with data classes + suspend fun getDescriptorSetter() = + descriptor.accessors + .firstIsInstanceOrNull<PropertySetterDescriptor>() + ?.let { + visitPropertyAccessorDescriptor(it, descriptor, dri, inheritedFrom) + } + + suspend fun getImplicitAccessorSetter() = + implicitAccessors?.setter?.let { visitFunctionDescriptor(it, parent) } + + return coroutineScope { + val generics = async { descriptor.typeParameters.parallelMap { it.toVariantTypeParameter() } } + val getter = getDescriptorGetter() ?: getImplicitAccessorGetter() + val setter = getDescriptorSetter() ?: getImplicitAccessorSetter() + + DProperty( + dri = dri, + name = descriptor.name.asString(), + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + sources = actual, + getter = getter, + setter = setter, + visibility = descriptor.getVisibility(implicitAccessors).toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData(), + modifier = descriptor.modifier().toSourceSetDependent(), + type = descriptor.returnType!!.toBound(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + generics = generics.await(), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + listOfNotNull( + (descriptor.additionalExtras() + descriptor.getAnnotationsWithBackingField() + .toAdditionalExtras()).toSet().toSourceSetDependent().toAdditionalModifiers(), + (descriptor.getAnnotationsWithBackingField() + descriptor.fileLevelAnnotations()).toSourceSetDependent() + .toAnnotations(), + descriptor.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) }, + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + takeIf { descriptor.isVar(getter, setter) }?.let { IsVar }, + takeIf { descriptor.findPsi() is KtParameter }?.let { IsAlsoParameter(listOf(sourceSet)) } + ) + ) + ) + } + } + + private fun PropertyDescriptor.isVar(getter: DFunction?, setter: DFunction?): Boolean { + return if (this is JavaPropertyDescriptor) { + // in Java, concepts of extensibility and mutability are mixed into a single `final` modifier + // in Kotlin, it's different - val/var controls mutability and open modifier controls extensibility + // so when inheriting Java properties, you can end up with a final var - non extensible mutable prop + val isMutable = this.isVar + // non-final java property should be var if it has no accessors at all or has a setter + (isMutable && getter == null && setter == null) || (getter != null && setter != null) + } else { + this.isVar + } + } + + private fun PropertyDescriptor.getVisibility(implicitAccessors: DescriptorAccessorHolder?): Visibility { + val isNonPublicJavaProperty = this is JavaPropertyDescriptor && !this.visibility.isPublicAPI + val visibility = + if (isNonPublicJavaProperty) { + // only try to take implicit getter's visibility if it's a java property + // because it's not guaranteed that implicit accessor will be used + // for the kotlin property, as it may have an explicit accessor of its own, + // i.e in data classes or with get() and set() are overridden + (implicitAccessors?.getter?.visibility ?: this.visibility) + } else { + this.visibility + } + + return visibility.toDokkaVisibility() + } + + private fun CallableMemberDescriptor.createDRI(wasOverridenBy: DRI? = null): Pair<DRI, DRI?> = + if (kind == CallableMemberDescriptor.Kind.DECLARATION || overriddenDescriptors.isEmpty()) + Pair(DRI.from(this), wasOverridenBy) + else + overriddenDescriptors.first().createDRI(DRI.from(this)) + + private suspend fun visitFunctionDescriptor( + originalDescriptor: FunctionDescriptor, + parent: DRIWithPlatformInfo + ): DFunction { + val (dri, _) = originalDescriptor.createDRI() + val inheritedFrom = dri.getInheritedFromDRI(parent) + val descriptor = originalDescriptor.getConcreteDescriptor() + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + + val actual = originalDescriptor.createSources() + return coroutineScope { + val generics = async { descriptor.typeParameters.parallelMap { it.toVariantTypeParameter() } } + + DFunction( + dri = dri, + name = descriptor.name.asString(), + isConstructor = false, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + parameters = descriptor.valueParameters.mapIndexed { index, desc -> + parameter(index, desc, DRIWithPlatformInfo(dri, actual)) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = actual, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = generics.await(), + documentation = descriptor.getDocumentation(), + modifier = descriptor.modifier().toSourceSetDependent(), + type = descriptor.returnType!!.toBound(), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + (descriptor.getAnnotations() + descriptor.fileLevelAnnotations()).toSourceSetDependent() + .toAnnotations(), + ObviousMember.takeIf { descriptor.isObvious() }, + ) + ) + } + } + + private fun FunctionDescriptor.getDocumentation(): SourceSetDependent<DocumentationNode> { + val isSynthesized = this.kind == CallableMemberDescriptor.Kind.SYNTHESIZED + return if (isSynthesized) { + syntheticDocProvider.getDocumentation(this)?.toSourceSetDependent() ?: emptyMap() + } else { + this.resolveDescriptorData() + } + } + + /** + * `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(parent: DRIWithPlatformInfo): DRI? { + return this.copy(callable = null) + .takeIf { parent.dri.classNames != this.classNames || parent.dri.packageName != this.packageName } + } + + private fun FunctionDescriptor.isObvious(): Boolean { + return kind == CallableMemberDescriptor.Kind.FAKE_OVERRIDE + || (kind == CallableMemberDescriptor.Kind.SYNTHESIZED && !syntheticDocProvider.isDocumented(this)) + || containingDeclaration.fqNameOrNull()?.isObvious() == true + } + + private fun FqName.isObvious(): Boolean = with(this.asString()) { + return this == "kotlin.Any" || this == "kotlin.Enum" + || this == "java.lang.Object" || this == "java.lang.Enum" + } + + suspend fun visitConstructorDescriptor(descriptor: ConstructorDescriptor, parent: DRIWithPlatformInfo): DFunction { + val name = descriptor.constructedClass.name.toString() + val dri = parent.dri.copy(callable = Callable.from(descriptor, name)) + val actual = descriptor.createSources() + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + + return coroutineScope { + val generics = async { descriptor.typeParameters.parallelMap { it.toVariantTypeParameter() } } + + DFunction( + dri = dri, + name = name, + isConstructor = true, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + parameters = descriptor.valueParameters.mapIndexed { index, desc -> + parameter(index, desc, DRIWithPlatformInfo(dri, actual)) + }, + sources = actual, + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData().let { sourceSetDependent -> + if (descriptor.isPrimary) { + sourceSetDependent.map { entry -> + Pair( + entry.key, + entry.value.copy(children = (entry.value.children.find { it is Constructor }?.root?.let { constructor -> + listOf(Description(constructor)) + } ?: emptyList<TagWrapper>()) + entry.value.children.filterIsInstance<Param>())) + }.toMap() + } else { + sourceSetDependent + } + }, + type = descriptor.returnType.toBound(), + modifier = descriptor.modifier().toSourceSetDependent(), + generics = generics.await(), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll<DFunction>( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ).let { + if (descriptor.isPrimary) { + it + PrimaryConstructorExtra + } else it + } + ) + } + } + + private suspend fun visitReceiverParameterDescriptor( + descriptor: ReceiverParameterDescriptor, + parent: DRIWithPlatformInfo + ) = DParameter( + dri = parent.dri.copy(target = PointingToDeclaration), + name = null, + type = descriptor.type.toBound(), + expectPresentInSet = null, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll(descriptor.getAnnotations().toSourceSetDependent().toAnnotations()) + ) + + private suspend fun visitPropertyAccessorDescriptor( + descriptor: PropertyAccessorDescriptor, + propertyDescriptor: PropertyDescriptor, + parent: DRI, + inheritedFrom: DRI? = null + ): DFunction { + val dri = parent.copy(callable = Callable.from(descriptor)) + val isGetter = descriptor is PropertyGetterDescriptor + val isExpect = descriptor.isExpect + val isActual = descriptor.isActual + + suspend fun PropertyDescriptor.asParameter(parent: DRI) = + DParameter( + parent.copy(target = PointingToCallableParameters(parameterIndex = 1)), + this.name.asString(), + type = this.type.toBound(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + getAnnotationsWithBackingField().toSourceSetDependent().toAnnotations() + ) + ) + + val name = run { + val rawName = propertyDescriptor.name.asString() + /* + * Kotlin has special rules for conversion around properties that + * start with "is" For more info see: + * https://kotlinlang.org/docs/java-interop.html#getters-and-setters + * https://kotlinlang.org/docs/java-to-kotlin-interop.html#properties + * + * Based on our testing, this rule only applies when the letter after + * the "is" is *not* lowercase. This means that words like "issue" won't + * have the rule applied but "is_foobar" and "is1of" will have the rule applied. + */ + val specialCaseIs = rawName.startsWith("is") + && rawName.getOrNull(2)?.isLowerCase() == false + + if (specialCaseIs) { + if (isGetter) rawName else rawName.replaceFirst("is", "set") + } else { + if (isGetter) "get${rawName.capitalize()}" else "set${rawName.capitalize()}" + } + } + + val parameters = + if (isGetter) { + emptyList() + } else { + listOf(propertyDescriptor.asParameter(dri)) + } + + return coroutineScope { + val generics = async { descriptor.typeParameters.parallelMap { it.toVariantTypeParameter() } } + DFunction( + dri, + name, + isConstructor = false, + parameters = parameters, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData().mapInheritedTagWrappers(), + type = descriptor.returnType!!.toBound(), + generics = generics.await(), + modifier = descriptor.modifier().toSourceSetDependent(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor( + it, + DRIWithPlatformInfo(dri, descriptor.createSources()) + ) + }, + sources = descriptor.createSources(), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) } + ) + ) + } + } + + /** + * Workaround for a problem with inheriting parent TagWrappers of the wrong type. + * + * For instance, if you annotate a class with `@property`, kotlin compiler will propagate + * this tag to the property and its getters and setters. In case of getters and setters, + * it's more correct to display propagated docs as description instead of property + */ + private fun SourceSetDependent<DocumentationNode>.mapInheritedTagWrappers(): SourceSetDependent<DocumentationNode> { + return this.mapValues { (_, value) -> + val mappedChildren = value.children.map { + when (it) { + is Property -> Description(it.root) + else -> it + } + } + value.copy(children = mappedChildren) + } + } + + private suspend fun visitTypeAliasDescriptor(descriptor: TypeAliasDescriptor) = + with(descriptor) { + coroutineScope { + val generics = async { descriptor.declaredTypeParameters.parallelMap { it.toVariantTypeParameter() } } + val info = buildAncestryInformation(defaultType).copy( + superclass = buildAncestryInformation(underlyingType), + interfaces = emptyList() + ) + DTypeAlias( + dri = DRI.from(this@with), + name = name.asString(), + type = defaultType.toBound(), + expectPresentInSet = null, + underlyingType = underlyingType.toBound().toSourceSetDependent(), + visibility = visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = resolveDescriptorData(), + sourceSets = setOf(sourceSet), + generics = generics.await(), + sources = descriptor.createSources(), + extra = PropertyContainer.withAll( + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + info.exceptionInSupertypesOrNull(), + ) + ) + } + } + + private suspend fun parameter(index: Int, descriptor: ValueParameterDescriptor, parent: DRIWithPlatformInfo) = + DParameter( + dri = parent.dri.copy(target = PointingToCallableParameters(index)), + name = descriptor.name.asString(), + type = descriptor.varargElementType?.toBound() ?: descriptor.type.toBound(), + expectPresentInSet = null, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll(listOfNotNull( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + descriptor.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) } + )) + ) + + private data class DescriptorsWithKind( + val functions: List<FunctionDescriptor>, + val properties: List<PropertyDescriptor>, + val classlikes: List<ClassDescriptor>, + val typealiases: List<TypeAliasDescriptor>, + val enumEntries: List<ClassDescriptor> + ) + + private fun MemberScope.getDescriptorsWithKind(shouldFilter: Boolean = false): DescriptorsWithKind { + val descriptors = getContributedDescriptors { true }.let { + if (shouldFilter) it.filterDescriptorsInSourceSet() else it + } + + class EnumEntryDescriptor + + val groupedDescriptors = descriptors.groupBy { + when { + it is FunctionDescriptor -> FunctionDescriptor::class + it is PropertyDescriptor -> PropertyDescriptor::class + it is ClassDescriptor && it.kind != ClassKind.ENUM_ENTRY -> ClassDescriptor::class + it is TypeAliasDescriptor -> TypeAliasDescriptor::class + it is ClassDescriptor && it.kind == ClassKind.ENUM_ENTRY -> EnumEntryDescriptor::class + else -> IllegalStateException::class + } + } + + @Suppress("UNCHECKED_CAST") + return DescriptorsWithKind( + (groupedDescriptors[FunctionDescriptor::class] ?: emptyList()) as List<FunctionDescriptor>, + (groupedDescriptors[PropertyDescriptor::class] ?: emptyList()) as List<PropertyDescriptor>, + (groupedDescriptors[ClassDescriptor::class] ?: emptyList()) as List<ClassDescriptor>, + (groupedDescriptors[TypeAliasDescriptor::class] ?: emptyList()) as List<TypeAliasDescriptor>, + (groupedDescriptors[EnumEntryDescriptor::class] ?: emptyList()) as List<ClassDescriptor> + ) + } + + private suspend fun List<FunctionDescriptor>.visitFunctions(parent: DRIWithPlatformInfo): List<DFunction> = + coroutineScope { parallelMap { visitFunctionDescriptor(it, parent) } } + + private suspend fun List<PropertyDescriptor>.visitProperties( + parent: DRIWithPlatformInfo, + implicitAccessors: Map<PropertyDescriptor, DescriptorAccessorHolder> = emptyMap(), + ): List<DProperty> { + return coroutineScope { + parallelMap { + visitPropertyDescriptor(it, implicitAccessors[it], parent) + } + } + } + + private suspend fun List<ClassDescriptor>.visitClasslikes(parent: DRIWithPlatformInfo): List<DClasslike> = + coroutineScope { parallelMap { visitClassDescriptor(it, parent) } } + + private suspend fun List<TypeAliasDescriptor>.visitTypealiases(): List<DTypeAlias> = + coroutineScope { parallelMap { visitTypeAliasDescriptor(it) } } + + private suspend fun List<ClassDescriptor>.visitEnumEntries(parent: DRIWithPlatformInfo): List<DEnumEntry> = + coroutineScope { parallelMap { visitEnumEntryDescriptor(it, parent) } } + + private fun DeclarationDescriptor.resolveDescriptorData(): SourceSetDependent<DocumentationNode> = + getDocumentation()?.toSourceSetDependent() ?: emptyMap() + + + private suspend fun toTypeConstructor(kt: KotlinType) = + GenericTypeConstructor( + DRI.from(kt.constructor.declarationDescriptor as DeclarationDescriptor), + kt.arguments.map { it.toProjection() }, + extra = PropertyContainer.withAll(kt.getAnnotations().toSourceSetDependent().toAnnotations()) + ) + + private suspend fun buildAncestryInformation( + kotlinType: KotlinType + ): AncestryNode { + val (interfaces, superclass) = kotlinType.immediateSupertypes().filterNot { it.isAnyOrNullableAny() } + .partition { + val declaration = it.constructor.declarationDescriptor + val descriptor = declaration as? ClassDescriptor + ?: (declaration as? TypeAliasDescriptor)?.underlyingType?.constructor?.declarationDescriptor as? ClassDescriptor + descriptor?.kind == ClassKind.INTERFACE + } + + return coroutineScope { + AncestryNode( + typeConstructor = toTypeConstructor(kotlinType), + superclass = superclass.parallelMap(::buildAncestryInformation).singleOrNull(), + interfaces = interfaces.parallelMap(::buildAncestryInformation) + ) + } + } + + + private suspend fun ClassDescriptor.resolveClassDescriptionData(): ClassInfo { + return coroutineScope { + ClassInfo( + buildAncestryInformation(this@resolveClassDescriptionData.defaultType), + resolveDescriptorData() + ) + } + } + + private suspend fun TypeParameterDescriptor.toVariantTypeParameter() = + DTypeParameter( + variantTypeParameter( + TypeParameter(DRI.from(this), name.identifier, annotations.getPresentableName()) + ), + resolveDescriptorData(), + null, + upperBounds.map { it.toBound() }, + setOf(sourceSet), + extra = PropertyContainer.withAll( + additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + getAnnotations().toSourceSetDependent().toAnnotations() + ) + ) + + private fun org.jetbrains.kotlin.descriptors.annotations.Annotations.getPresentableName(): String? = + mapNotNull { it.toAnnotation() }.singleOrNull { it.dri.classNames == "ParameterName" }?.params?.get("name") + .let { it as? StringValue }?.value?.let { unquotedValue(it) } + + private suspend fun KotlinType.toBound(): Bound { + suspend fun <T : AnnotationTarget> annotations(): PropertyContainer<T> = + getAnnotations().takeIf { it.isNotEmpty() }?.let { annotations -> + PropertyContainer.withAll(annotations.toSourceSetDependent().toAnnotations()) + } ?: PropertyContainer.empty() + + return when (this) { + is DynamicType -> Dynamic + is AbbreviatedType -> TypeAliased( + abbreviation.toBound(), + expandedType.toBound(), + annotations() + ) + is DefinitelyNotNullType -> DefinitelyNonNullable( + original.toBound() + ) + else -> when (val ctor = constructor.declarationDescriptor) { + is TypeParameterDescriptor -> TypeParameter( + dri = DRI.from(ctor), + name = ctor.name.asString(), + presentableName = annotations.getPresentableName(), + extra = annotations() + ) + is FunctionClassDescriptor -> FunctionalTypeConstructor( + DRI.from(ctor), + arguments.map { it.toProjection() }, + isExtensionFunction = isExtensionFunctionType || isBuiltinExtensionFunctionalType, + isSuspendable = isSuspendFunctionTypeOrSubtype, + presentableName = annotations.getPresentableName(), + extra = annotations() + ) + else -> GenericTypeConstructor( + DRI.from(ctor!!), // TODO: remove '!!' + arguments.map { it.toProjection() }, + annotations.getPresentableName(), + extra = annotations() + ) + }.let { + if (isMarkedNullable) Nullable(it) else it + } + } + } + + private suspend fun TypeProjection.toProjection(): Projection = + if (isStarProjection) Star else formPossiblyVariant() + + private suspend fun TypeProjection.formPossiblyVariant(): Projection = + type.toBound().wrapWithVariance(projectionKind) + + private fun TypeParameterDescriptor.variantTypeParameter(type: TypeParameter) = + type.wrapWithVariance(variance) + + private 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) + } + + private fun descriptorToAnyDeclaration(descriptor: DeclarationDescriptor): PsiElement? { + val effectiveReferencedDescriptors = DescriptorToSourceUtils.getEffectiveReferencedDescriptors(descriptor) + //take any + return effectiveReferencedDescriptors.firstOrNull()?.let { DescriptorToSourceUtils.getSourceFromDescriptor(it) } + } + + private fun DeclarationDescriptor.getDocumentation(): DocumentationNode? { + val find = with(kDocFinder) { + find(::descriptorToAnyDeclaration) + } + + return (find?.let { + parseFromKDocTag( + kDocTag = it, + externalDri = { link: String -> + try { + val kdocLink = with(kDocFinder) { + resolveKDocLink( + fromDescriptor = this@getDocumentation, + qualifiedName = link, + sourceSet = sourceSet + ) + } + kdocLink.firstOrNull()?.let { + DRI.from( + it + ) + } + } catch (e1: IllegalArgumentException) { + logger.warn("Couldn't resolve link for $link") + null + } + }, + kdocLocation = toSourceElement.containingFile.name?.let { + val fqName = fqNameOrNull()?.asString() + if (fqName != null) "$it/$fqName" + else it + } + ) + } ?: getJavaDocs())?.takeIf { it.children.isNotEmpty() } + } + + private fun DeclarationDescriptor.getJavaDocs(): DocumentationNode? { + val overriddenDescriptors = (this as? CallableDescriptor)?.overriddenDescriptors ?: emptyList() + val allDescriptors = overriddenDescriptors + listOf(this) + return allDescriptors + .mapNotNull { it.findPsi() as? PsiNamedElement } + .firstOrNull() + ?.let { javadocParser.parseDocumentation(it) } + } + + private suspend fun ClassDescriptor.companion(dri: DRIWithPlatformInfo): DObject? = companionObjectDescriptor?.let { + objectDescriptor(it, dri) + } + + private fun MemberDescriptor.modifier() = when (modality) { + Modality.FINAL -> KotlinModifier.Final + Modality.SEALED -> KotlinModifier.Sealed + Modality.OPEN -> KotlinModifier.Open + Modality.ABSTRACT -> KotlinModifier.Abstract + else -> KotlinModifier.Empty + } + + private fun MemberDescriptor.createSources(): SourceSetDependent<DocumentableSource> = + DescriptorDocumentableSource(this).toSourceSetDependent() + + private fun FunctionDescriptor.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 { isTailrec }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { DescriptorUtils.isOverride(this) } + ).toSet() + + private fun ClassDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, + ExtraModifiers.KotlinOnlyModifiers.Value.takeIf { isValue }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Inner.takeIf { isInner }, + ExtraModifiers.KotlinOnlyModifiers.Data.takeIf { isData }, + ExtraModifiers.KotlinOnlyModifiers.Fun.takeIf { isFun }, + ).toSet() + + private fun ValueParameterDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.NoInline.takeIf { isNoinline }, + ExtraModifiers.KotlinOnlyModifiers.CrossInline.takeIf { isCrossinline }, + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { isConst }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { isLateInit }, + ExtraModifiers.KotlinOnlyModifiers.VarArg.takeIf { isVararg } + ).toSet() + + private fun TypeParameterDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Reified.takeIf { isReified } + ).toSet() + + private fun PropertyDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { isConst }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { isLateInit }, + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { DescriptorUtils.isOverride(this) } + ) + + private suspend fun Annotated.getAnnotations() = annotations.parallelMapNotNull { it.toAnnotation() } + + private fun ConstantValue<*>.toValue(): AnnotationParameterValue = when (this) { + is ConstantsAnnotationValue -> AnnotationValue(value.toAnnotation()) + is ConstantsArrayValue -> ArrayValue(value.map { it.toValue() }) + is ConstantsEnumValue -> EnumValue( + fullEnumEntryName(), + DRI(enumClassId.packageFqName.asString(), fullEnumEntryName()) + ) + is ConstantsKtClassValue -> when (value) { + is NormalClass -> (value as NormalClass).value.classId.let { + ClassValue( + it.relativeClassName.asString(), + DRI(it.packageFqName.asString(), it.relativeClassName.asString()) + ) + } + is LocalClass -> (value as LocalClass).type.let { + ClassValue( + it.toString(), + DRI.from(it.constructor.declarationDescriptor as DeclarationDescriptor) + ) + } + } + is ConstantsFloatValue -> FloatValue(value) + is ConstantsDoubleValue -> DoubleValue(value) + is ConstantsUIntValue -> IntValue(value) + is ConstantsULongValue -> LongValue(value) + is ConstantsIntValue -> IntValue(value) + is ConstantsLongValue -> LongValue(value) + is ConstantsBooleanValue -> BooleanValue(value) + is ConstantsNullValue -> NullValue + else -> StringValue(unquotedValue(toString())) + } + + private fun AnnotationDescriptor.toAnnotation(scope: Annotations.AnnotationScope = Annotations.AnnotationScope.DIRECT): Annotations.Annotation = + Annotations.Annotation( + DRI.from(annotationClass as DeclarationDescriptor), + allValueArguments.map { it.key.asString() to it.value.toValue() }.toMap(), + mustBeDocumented(), + scope + ) + + private fun AnnotationDescriptor.mustBeDocumented(): Boolean = + if (source.toString() == "NO_SOURCE") false + else annotationClass?.annotations?.hasAnnotation(FqName("kotlin.annotation.MustBeDocumented")) ?: false + + private suspend fun PropertyDescriptor.getAnnotationsWithBackingField(): List<Annotations.Annotation> = + getAnnotations() + (backingField?.getAnnotations() ?: emptyList()) + + private fun List<Annotations.Annotation>.toAdditionalExtras() = mapNotNull { + try { + ExtraModifiers.valueOf(it.dri.classNames?.toLowerCase() ?: "") + } catch (e: IllegalArgumentException) { + null + } + } + + private fun <T : CallableMemberDescriptor> T.getConcreteDescriptor(): T { + return if (kind != CallableMemberDescriptor.Kind.FAKE_OVERRIDE) { + this + } else { + @Suppress("UNCHECKED_CAST") + overriddenDescriptors.first().getConcreteDescriptor() as T + } + } + + private fun ValueParameterDescriptor.getDefaultValue(): Expression? = + ((source as? KotlinSourceElement)?.psi as? KtParameter)?.defaultValue?.toDefaultValueExpression() + + private fun PropertyDescriptor.getDefaultValue(): Expression? = + (source as? KotlinSourceElement)?.psi?.children?.firstIsInstanceOrNull<KtConstantExpression>() + ?.toDefaultValueExpression() + + private fun ClassDescriptor.getAppliedConstructorParameters() = + (source as PsiSourceElement).psi?.children?.flatMap { + (it as? KtInitializerList)?.initializersAsExpression().orEmpty() + }.orEmpty() + + private fun KtInitializerList.initializersAsExpression() = + initializers.firstIsInstanceOrNull<KtCallElement>() + ?.getValueArgumentsInParentheses() + ?.map { it.getArgumentExpression()?.toDefaultValueExpression() ?: ComplexExpression("") } + .orEmpty() + + private fun KtExpression.toDefaultValueExpression(): Expression? = when (node?.elementType) { + KtNodeTypes.INTEGER_CONSTANT -> parseLong(node?.text)?.let { IntegerConstant(it) } + KtNodeTypes.FLOAT_CONSTANT -> if (node?.text?.toLowerCase()?.endsWith('f') == true) + parseFloat(node?.text)?.let { FloatConstant(it) } + else 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 data class ClassInfo(val ancestry: AncestryNode, val docs: SourceSetDependent<DocumentationNode>) { + val supertypes: List<TypeConstructorWithKind> + get() = listOfNotNull(ancestry.superclass?.let { + it.typeConstructor.let { + TypeConstructorWithKind( + it, + KotlinClassKindTypes.CLASS + ) + } + }) + ancestry.interfaces.map { TypeConstructorWithKind(it.typeConstructor, KotlinClassKindTypes.INTERFACE) } + } + + private fun DescriptorVisibility.toDokkaVisibility(): Visibility = when (this.delegate) { + 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 + } + + private fun ConstantsEnumValue.fullEnumEntryName() = + "${this.enumClassId.relativeClassName.asString()}.${this.enumEntryName.identifier}" + + private fun DeclarationDescriptorWithSource.ktFile(): KtFile? = + (source.containingFile as? PsiSourceFile)?.psiFile as? KtFile + + private suspend fun DeclarationDescriptorWithSource.fileLevelAnnotations() = ktFile() + ?.let { file -> + analysisContext.resolveSession.getFileAnnotations(file) + } + ?.toList() + ?.parallelMap { it.toAnnotation(scope = Annotations.AnnotationScope.FILE) } + .orEmpty() + + private fun AncestryNode.exceptionInSupertypesOrNull(): ExceptionInSupertypes? = + typeConstructorsBeingExceptions().takeIf { it.isNotEmpty() }?.let { ExceptionInSupertypes(it.toSourceSetDependent()) } +} + diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultExternalDocumentablesProvider.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultExternalDocumentablesProvider.kt new file mode 100644 index 00000000..3c29b61d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DefaultExternalDocumentablesProvider.kt @@ -0,0 +1,43 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +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.kotlin.internal.ExternalDocumentablesProvider +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.PackageViewDescriptor +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.scopes.MemberScope +import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered + +internal class DefaultExternalDocumentablesProvider(context: DokkaContext) : ExternalDocumentablesProvider { + private val analysis = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kotlinAnalysis } + + private val translator: ExternalClasslikesTranslator = DefaultDescriptorToDocumentableTranslator(context) + + override fun findClasslike(dri: DRI, sourceSet: DokkaSourceSet): DClasslike? { + val pkg = dri.packageName?.let { FqName(it) } ?: FqName.ROOT + val names = dri.classNames?.split('.') ?: return null + + val packageDsc = analysis[sourceSet].moduleDescriptor.getPackage(pkg) + val classDsc = names.fold<String, DeclarationDescriptor?>(packageDsc) { dsc, name -> + dsc?.scope?.getDescriptorsFiltered { it.identifier == name } + ?.filterIsInstance<ClassDescriptor>() + ?.firstOrNull() + } + + return (classDsc as? ClassDescriptor)?.let { translator.translateClassDescriptor(it, sourceSet) } + } + + private val DeclarationDescriptor.scope: MemberScope + get() = when (this) { + is PackageViewDescriptor -> memberScope + is ClassDescriptor -> unsubstitutedMemberScope + else -> throw IllegalArgumentException("Unexpected type of descriptor: ${this::class}") + } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DescriptorAccessorConventionUtil.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DescriptorAccessorConventionUtil.kt new file mode 100644 index 00000000..fcb0b83d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/DescriptorAccessorConventionUtil.kt @@ -0,0 +1,144 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.load.java.JvmAbi +import org.jetbrains.kotlin.load.java.descriptors.JavaMethodDescriptor +import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName +import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName + +internal data class DescriptorFunctionsHolder( + val regularFunctions: List<FunctionDescriptor>, + val accessors: Map<PropertyDescriptor, DescriptorAccessorHolder> +) + +internal data class DescriptorAccessorHolder( + val getter: FunctionDescriptor? = null, + val setter: FunctionDescriptor? = null +) + +/** + * Separate regular Kotlin/Java functions and inherited Java accessors + * to properly display properties inherited from Java. + * + * Take this example: + * ``` + * // java + * public class JavaClass { + * private int a = 1; + * public int getA() { return a; } + * public void setA(int a) { this.a = a; } + * } + * + * // kotlin + * class Bar : JavaClass() { + * fun foo() {} + * } + * ``` + * + * It should result in: + * - 1 regular function `foo` + * - Map a=[`getA`, `setA`] + */ +internal fun splitFunctionsAndInheritedAccessors( + properties: List<PropertyDescriptor>, + functions: List<FunctionDescriptor> +): DescriptorFunctionsHolder { + val (javaMethods, kotlinFunctions) = functions.partition { it is JavaMethodDescriptor } + if (javaMethods.isEmpty()) { + return DescriptorFunctionsHolder(regularFunctions = kotlinFunctions, emptyMap()) + } + + val propertiesByName = properties.associateBy { it.name.asString() } + val regularFunctions = ArrayList<FunctionDescriptor>(kotlinFunctions) + + val accessors = mutableMapOf<PropertyDescriptor, DescriptorAccessorHolder>() + javaMethods.forEach { function -> + val possiblePropertyNamesForFunction = function.toPossiblePropertyNames() + val property = possiblePropertyNamesForFunction.firstNotNullOfOrNull { propertiesByName[it] } + if (property != null && function.isAccessorFor(property)) { + accessors.compute(property) { prop, accessorHolder -> + if (function.isGetterFor(prop)) + accessorHolder?.copy(getter = function) ?: DescriptorAccessorHolder(getter = function) + else + accessorHolder?.copy(setter = function) ?: DescriptorAccessorHolder(setter = function) + } + } else { + regularFunctions.add(function) + } + } + + val accessorLookalikes = removeNonAccessorsReturning(accessors) + regularFunctions.addAll(accessorLookalikes) + + return DescriptorFunctionsHolder(regularFunctions, accessors) +} + +/** + * If a field has no getter, it's not accessible as a property from Kotlin's perspective, + * but it still might have a setter lookalike. In this case, this "setter" should be just a regular function + * + * @return removed elements + */ +private fun removeNonAccessorsReturning( + propertyAccessors: MutableMap<PropertyDescriptor, DescriptorAccessorHolder> +): List<FunctionDescriptor> { + val nonAccessors = mutableListOf<FunctionDescriptor>() + propertyAccessors.entries.removeIf { (_, accessors) -> + if (accessors.getter == null && accessors.setter != null) { + nonAccessors.add(accessors.setter) + true + } else { + false + } + } + return nonAccessors +} + +private fun FunctionDescriptor.toPossiblePropertyNames(): List<String> { + val stringName = this.name.asString() + return when { + JvmAbi.isSetterName(stringName) -> propertyNamesBySetMethodName(this.name).map { it.asString() } + JvmAbi.isGetterName(stringName) -> propertyNamesByGetMethod(this) + else -> listOf() + } +} + +private fun propertyNamesByGetMethod(functionDescriptor: FunctionDescriptor): List<String> { + val stringName = functionDescriptor.name.asString() + // In java, the convention for boolean property accessors is as follows: + // - `private boolean active;` + // - `private boolean isActive();` + // + // Whereas in Kotlin, because there are no explicit accessors, the convention is + // - `val isActive: Boolean` + // + // This makes it difficult to guess the name of the accessor property in case of Java + val javaPropName = if (functionDescriptor is JavaMethodDescriptor && JvmAbi.startsWithIsPrefix(stringName)) { + val javaPropName = stringName.removePrefix("is").let { newName -> + newName.replaceFirst(newName[0], newName[0].toLowerCase()) + } + javaPropName + } else { + null + } + val kotlinPropName = propertyNameByGetMethodName(functionDescriptor.name)?.asString() + return listOfNotNull(javaPropName, kotlinPropName) +} + +private fun FunctionDescriptor.isAccessorFor(property: PropertyDescriptor): Boolean { + return (this.isGetterFor(property) || this.isSetterFor(property)) + && !property.visibility.isPublicAPI + && this.visibility.isPublicAPI +} + +private fun FunctionDescriptor.isGetterFor(property: PropertyDescriptor): Boolean { + return this.returnType == property.returnType + && this.valueParameters.isEmpty() +} + +private fun FunctionDescriptor.isSetterFor(property: PropertyDescriptor): Boolean { + return this.valueParameters.size == 1 + && this.valueParameters[0].type == property.returnType +} + diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/ExternalClasslikesTranslator.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/ExternalClasslikesTranslator.kt new file mode 100644 index 00000000..0b4b4442 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/ExternalClasslikesTranslator.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.model.DClasslike +import org.jetbrains.kotlin.descriptors.ClassDescriptor + +/** + * Service translating [ClassDescriptor]s of symbols defined outside of documented project to [DClasslike]s. + */ +internal fun interface ExternalClasslikesTranslator { + fun translateClassDescriptor(descriptor: ClassDescriptor, sourceSet: DokkaSourceSet): DClasslike +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/KdocMarkdownParser.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/KdocMarkdownParser.kt new file mode 100644 index 00000000..e47b9ba2 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/KdocMarkdownParser.kt @@ -0,0 +1,101 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +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.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag + +internal fun parseFromKDocTag( + kDocTag: KDocTag?, + externalDri: (String) -> DRI?, + kdocLocation: String?, + parseWithChildren: Boolean = true +): DocumentationNode { + return if (kDocTag == null) { + DocumentationNode(emptyList()) + } else { + fun parseStringToDocNode(text: String) = + MarkdownParser(externalDri, kdocLocation).parseStringToDocNode(text) + + fun pointedLink(tag: KDocTag): DRI? = (parseStringToDocNode("[${tag.getSubjectName()}]")).let { + val link = it.children[0].children[0] + if (link is DocumentationLink) link.dri else null + } + + val allTags = + listOf(kDocTag) + if (kDocTag.canHaveParent() && parseWithChildren) getAllKDocTags(findParent(kDocTag)) else emptyList() + DocumentationNode( + allTags.map { + when (it.knownTag) { + null -> if (it.name == null) Description(parseStringToDocNode(it.getContent())) else CustomTagWrapper( + parseStringToDocNode(it.getContent()), + it.name!! + ) + KDocKnownTag.AUTHOR -> Author(parseStringToDocNode(it.getContent())) + KDocKnownTag.THROWS -> { + val dri = pointedLink(it) + Throws( + parseStringToDocNode(it.getContent()), + dri?.fqDeclarationName() ?: it.getSubjectName().orEmpty(), + dri, + ) + } + KDocKnownTag.EXCEPTION -> { + val dri = pointedLink(it) + Throws( + parseStringToDocNode(it.getContent()), + dri?.fqDeclarationName() ?: it.getSubjectName().orEmpty(), + dri + ) + } + KDocKnownTag.PARAM -> Param( + parseStringToDocNode(it.getContent()), + it.getSubjectName().orEmpty() + ) + KDocKnownTag.RECEIVER -> Receiver(parseStringToDocNode(it.getContent())) + KDocKnownTag.RETURN -> Return(parseStringToDocNode(it.getContent())) + KDocKnownTag.SEE -> { + val dri = pointedLink(it) + See( + parseStringToDocNode(it.getContent()), + dri?.fqDeclarationName() ?: it.getSubjectName().orEmpty(), + dri, + ) + } + KDocKnownTag.SINCE -> Since(parseStringToDocNode(it.getContent())) + KDocKnownTag.CONSTRUCTOR -> Constructor(parseStringToDocNode(it.getContent())) + KDocKnownTag.PROPERTY -> Property( + parseStringToDocNode(it.getContent()), + it.getSubjectName().orEmpty() + ) + KDocKnownTag.SAMPLE -> Sample( + parseStringToDocNode(it.getContent()), + it.getSubjectName().orEmpty() + ) + KDocKnownTag.SUPPRESS -> Suppress(parseStringToDocNode(it.getContent())) + } + } + ) + } +} + +//Horrible hack but since link resolution is passed as a function i am not able to resolve them otherwise +@kotlin.Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated("This function makes wrong assumptions and is missing a lot of corner cases related to generics, " + + "parameters and static members. This is not supposed to be public API and will not be supported in the future") +internal fun DRI.fqName(): String? = "$packageName.$classNames".takeIf { packageName != null && classNames != null } + +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-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/SyntheticDescriptorDocumentationProvider.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/SyntheticDescriptorDocumentationProvider.kt new file mode 100644 index 00000000..3b21f771 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/SyntheticDescriptorDocumentationProvider.kt @@ -0,0 +1,46 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.from +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.resolve.DescriptorFactory + +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 class SyntheticDescriptorDocumentationProvider( + private val kDocFinder: KDocFinder, + private val sourceSet: DokkaConfiguration.DokkaSourceSet +) { + fun isDocumented(descriptor: DeclarationDescriptor): Boolean = descriptor is FunctionDescriptor + && (DescriptorFactory.isEnumValuesMethod(descriptor) || DescriptorFactory.isEnumValueOfMethod(descriptor)) + + fun getDocumentation(descriptor: DeclarationDescriptor): DocumentationNode? { + val function = descriptor as? FunctionDescriptor ?: return null + return when { + DescriptorFactory.isEnumValuesMethod(function) -> loadTemplate(descriptor, ENUM_VALUES_TEMPLATE_PATH) + DescriptorFactory.isEnumValueOfMethod(function) -> loadTemplate(descriptor, ENUM_VALUEOF_TEMPLATE_PATH) + else -> null + } + } + + private fun loadTemplate(descriptor: DeclarationDescriptor, filePath: String): DocumentationNode? { + val kdoc = loadContent(filePath) ?: return null + val parser = MarkdownParser({ link -> resolveLink(descriptor, link)}, filePath) + return parser.parse(kdoc) + } + + private fun loadContent(filePath: String): String? = javaClass.getResource(filePath)?.readText() + + private fun resolveLink(descriptor: DeclarationDescriptor, link: String): DRI? = + kDocFinder.resolveKDocLink( + fromDescriptor = descriptor, + qualifiedName = link, + sourceSet = sourceSet + ).firstOrNull()?.let { DRI.from(it) } +} diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/annotationsValue.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/annotationsValue.kt new file mode 100644 index 00000000..70254566 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/annotationsValue.kt @@ -0,0 +1,3 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +internal fun unquotedValue(value: String): String = value.removeSurrounding("\"") diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/isException.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/isException.kt new file mode 100644 index 00000000..710846b3 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/translator/isException.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator + +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 superclass?.let(::traverseSupertypes)?.filter { type -> type.dri.isDirectlyAnException() } ?: emptyList() +} + +internal fun DRI.isDirectlyAnException(): Boolean = + toString().let { stringed -> + stringed == "kotlin/Exception///PointingToDeclaration/" || + stringed == "java.lang/Exception///PointingToDeclaration/" + } diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-kotlin-descriptors/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..c7a8a233 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin diff --git a/subprojects/analysis-kotlin-descriptors/compiler/src/test/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ParseModuleAndPackageDocumentationFragmentsTest.kt b/subprojects/analysis-kotlin-descriptors/compiler/src/test/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ParseModuleAndPackageDocumentationFragmentsTest.kt new file mode 100644 index 00000000..321aba45 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/compiler/src/test/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ParseModuleAndPackageDocumentationFragmentsTest.kt @@ -0,0 +1,282 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors + + +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs.* +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.moduledocs.ModuleAndPackageDocumentation.Classifier.* +import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.utilities.DokkaLogger +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.test.assertEquals + +class ParseModuleAndPackageDocumentationFragmentsTest { + + private fun testBasicExample(lineSeperator: String = "\n") { + val source = source( + """ + # Module kotlin-demo + Module description + + # Package org.jetbrains.kotlin.demo + Package demo description + ## Level 2 heading + Heading 2\r\n + + # Package org.jetbrains.kotlin.demo2 + Package demo2 description + """.trimIndent().replace("\n", lineSeperator) + ) + val fragments = parseModuleAndPackageDocumentationFragments(source) + + assertEquals( + listOf( + ModuleAndPackageDocumentationFragment( + classifier = Module, + name = "kotlin-demo", + documentation = "Module description", + source = source + ), + ModuleAndPackageDocumentationFragment( + classifier = Package, + name = "org.jetbrains.kotlin.demo", + documentation = "Package demo description${lineSeperator}## Level 2 heading${lineSeperator}Heading 2\\r\\n", + source = source + ), + ModuleAndPackageDocumentationFragment( + classifier = Package, + name = "org.jetbrains.kotlin.demo2", + documentation = "Package demo2 description", + source = source + ) + ), + fragments + ) + } + + @Test + fun `basic example`() { + testBasicExample() + } + + @Test + fun `CRLF line seperators`() { + testBasicExample("\r\n") + } + + @Test + fun `no module name specified fails`() { + val exception = assertThrows<IllegalModuleAndPackageDocumentation> { + parseModuleAndPackageDocumentationFragments( + source( + """ + # Module + No module name given + """.trimIndent() + ) + ) + } + + assertTrue( + "Missing Module name" in exception.message.orEmpty(), + "Expected 'Missing Module name' in error message" + ) + } + + @Test + fun `no package name specified does not fail`() { + val source = source( + """ + # Package + This is a root package + """.trimIndent() + ) + val fragments = parseModuleAndPackageDocumentationFragments(source) + assertEquals(1, fragments.size, "Expected a single package fragment") + + assertEquals( + ModuleAndPackageDocumentationFragment( + name = "", + classifier = Package, + documentation = "This is a root package", + source = source + ), + fragments.single() + ) + } + + @Test + fun `white space in module name is supported`() { + val fragment = parseModuleAndPackageDocumentationFragments( + source( + """ + # Module My Module + Documentation for my module + """.trimIndent() + ) + ) + + assertEquals( + Module, fragment.single().classifier, + "Expected module being parsec" + ) + + assertEquals( + "My Module", fragment.single().name, + "Expected module name with white spaces being parsed" + ) + + assertEquals( + "Documentation for my module", fragment.single().documentation, + "Expected documentation being available" + ) + } + + @Test + fun `white space in package name fails`() { + val exception = assertThrows<IllegalModuleAndPackageDocumentation> { + parseModuleAndPackageDocumentationFragments( + source( + """ + # Package my package + """.trimIndent() + ) + ) + } + + assertTrue( + "Package my package" in exception.message.orEmpty(), + "Expected problematic statement in error message" + ) + } + + @Test + fun `multiple whitespaces are supported in first line`() { + val source = source( + """ + # Module my-module + My Module + # Package com.my.package + My Package + """.trimIndent() + ) + val fragments = parseModuleAndPackageDocumentationFragments(source) + + assertEquals( + listOf( + ModuleAndPackageDocumentationFragment( + classifier = Module, + name = "my-module", + documentation = "My Module", + source = source + ), + ModuleAndPackageDocumentationFragment( + classifier = Package, + name = "com.my.package", + documentation = "My Package", + source = source + ) + ), + fragments + ) + } + + @Test + fun `parse from file`(@TempDir temporaryFolder: Path) { + val file = temporaryFolder.resolve("other.md").toFile() + file.writeText( + """ + # Module MyModule + D1 + # Package com.sample + D2 + """.trimIndent() + ) + + assertEquals( + listOf( + ModuleAndPackageDocumentationFragment( + classifier = Module, + name = "MyModule", + documentation = "D1", + source = ModuleAndPackageDocumentationFile(file) + ), + ModuleAndPackageDocumentationFragment( + classifier = Package, + name = "com.sample", + documentation = "D2", + source = ModuleAndPackageDocumentationFile(file) + ) + ), + parseModuleAndPackageDocumentationFragments(file) + ) + } + + @Test + fun `at in code block is supported`() { + val fragment = parseModuleAndPackageDocumentationFragments( + source( + """ + # Module My Module + ``` + @Smth + ``` + @author Smb + """.trimIndent() + ) + ) + + assertEquals( + "```\n" + + "@Smth\n" + + "```\n" + + "@author Smb", fragment.single().documentation, + "Expected documentation being available" + ) + + val parsingContext = ModuleAndPackageDocumentationParsingContext(object : DokkaLogger { + override var warningsCount: Int = 0 + override var errorsCount: Int = 0 + override fun debug(message: String) {} + override fun info(message: String) {} + override fun progress(message: String) {} + override fun warn(message: String) {} + override fun error(message: String) {} + }) + val parsedFragment = parseModuleAndPackageDocumentation(parsingContext, fragment.single()) + val expectedDocumentationNode = DocumentationNode( + listOf( + Description( + CustomDocTag( + listOf( + CodeBlock( + listOf( + Text("@Smth") + ) + ) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ), + Author( + CustomDocTag( + listOf( + P(listOf(Text("Smb"))) + ), name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + ) + ) + assertEquals( + expectedDocumentationNode, parsedFragment.documentation + ) + } + + private fun source(documentation: String): ModuleAndPackageDocumentationSource = + object : ModuleAndPackageDocumentationSource() { + override val sourceDescription: String = "inline test" + override val documentation: String = documentation + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/README.md b/subprojects/analysis-kotlin-descriptors/ide/README.md new file mode 100644 index 00000000..14ed5baa --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/README.md @@ -0,0 +1,11 @@ +# Descriptors: IDE + +An internal module that encapsulates external IDE (`org.jetbrains.kotlin:idea`) dependencies. + +IDE artifacts are reused for things that are not possible to do with the Kotlin compiler API, such +as KDoc or KLib parsing/processing, because Dokka is very similar to an IDE when it comes to analyzing +source code and docs. + +Exists primarily to make sure that unreliable and coupled external dependencies are somewhat abstracted away, +otherwise everything gets tangled together and breaking changes in such dependencies become very +difficult to resolve. diff --git a/subprojects/analysis-kotlin-descriptors/ide/api/ide.api b/subprojects/analysis-kotlin-descriptors/ide/api/ide.api new file mode 100644 index 00000000..a59658a3 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/api/ide.api @@ -0,0 +1,4 @@ +public final class org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeDescriptorAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V +} + diff --git a/subprojects/analysis-kotlin-descriptors/ide/build.gradle.kts b/subprojects/analysis-kotlin-descriptors/ide/build.gradle.kts new file mode 100644 index 00000000..79777256 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("org.jetbrains.conventions.kotlin-jvm") +} + +dependencies { + compileOnly(projects.core) + compileOnly(projects.subprojects.analysisKotlinApi) + + implementation(projects.subprojects.analysisKotlinDescriptors.compiler) + + api(libs.kotlin.idePlugin.common) + api(libs.kotlin.idePlugin.idea) + api(libs.kotlin.idePlugin.core) + api(libs.kotlin.idePlugin.native) + + // TODO [beresnev] needed for CommonIdePlatformKind, describe + implementation(libs.kotlin.jps.common) + + // TODO [beresnev] get rid of it + compileOnly(libs.kotlinx.coroutines.core) +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/CoreKotlinCacheService.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/CoreKotlinCacheService.kt new file mode 100644 index 00000000..d7069656 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/CoreKotlinCacheService.kt @@ -0,0 +1,54 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.caches.resolve.PlatformAnalysisSettings +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.diagnostics.KotlinSuppressCache + + +internal class CoreKotlinCacheService(private val resolutionFacade: DokkaResolutionFacade) : KotlinCacheService { + override fun getResolutionFacade(elements: List<KtElement>): ResolutionFacade { + return resolutionFacade + } + + override fun getResolutionFacade(element: KtElement): ResolutionFacade { + return resolutionFacade + } + + override fun getResolutionFacadeByFile( + file: PsiFile, + platform: org.jetbrains.kotlin.platform.TargetPlatform + ): ResolutionFacade { + return resolutionFacade + } + + override fun getResolutionFacadeByModuleInfo( + moduleInfo: ModuleInfo, + settings: PlatformAnalysisSettings + ): ResolutionFacade { + return resolutionFacade + } + + override fun getResolutionFacadeByModuleInfo( + moduleInfo: ModuleInfo, + platform: org.jetbrains.kotlin.platform.TargetPlatform + ): ResolutionFacade { + return resolutionFacade + } + + override fun getResolutionFacadeWithForcedPlatform( + elements: List<KtElement>, + platform: TargetPlatform + ): ResolutionFacade { + return resolutionFacade + } + + override fun getSuppressionCache(): KotlinSuppressCache { + throw UnsupportedOperationException() + } + +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/DokkaResolutionFacade.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/DokkaResolutionFacade.kt new file mode 100644 index 00000000..e2253b99 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/DokkaResolutionFacade.kt @@ -0,0 +1,123 @@ +@file:OptIn(FrontendInternals::class) + +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import com.google.common.collect.ImmutableMap +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.analyzer.AnalysisResult +import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.analyzer.ResolverForModule +import org.jetbrains.kotlin.analyzer.ResolverForProject +import org.jetbrains.kotlin.container.getService +import org.jetbrains.kotlin.container.tryGetService +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.diagnostics.DiagnosticSink +import org.jetbrains.kotlin.idea.FrontendInternals +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.util.slicedMap.ReadOnlySlice +import org.jetbrains.kotlin.util.slicedMap.WritableSlice + +internal class DokkaResolutionFacade( + override val project: Project, + override val moduleDescriptor: ModuleDescriptor, + val resolverForModule: ResolverForModule +) : ResolutionFacade { + override fun analyzeWithAllCompilerChecks( + elements: Collection<KtElement>, + callback: DiagnosticSink.DiagnosticsCallback? + ): AnalysisResult { + throw UnsupportedOperationException() + } + + @OptIn(FrontendInternals::class) + override fun <T : Any> tryGetFrontendService(element: PsiElement, serviceClass: Class<T>): T? { + return resolverForModule.componentProvider.tryGetService(serviceClass) + } + + override fun resolveToDescriptor( + declaration: KtDeclaration, + bodyResolveMode: BodyResolveMode + ): DeclarationDescriptor { + return resolveSession.resolveToDescriptor(declaration) + } + + override fun analyze(elements: Collection<KtElement>, bodyResolveMode: BodyResolveMode): BindingContext { + throw UnsupportedOperationException() + } + + val resolveSession: ResolveSession get() = getFrontendService(ResolveSession::class.java) + + override fun analyze(element: KtElement, bodyResolveMode: BodyResolveMode): BindingContext { + if (element is KtDeclaration) { + val descriptor = resolveToDescriptor(element) + return object : BindingContext { + override fun <K : Any?, V : Any?> getKeys(p0: WritableSlice<K, V>?): Collection<K> { + throw UnsupportedOperationException() + } + + override fun getType(p0: KtExpression): KotlinType? { + throw UnsupportedOperationException() + } + + override fun <K : Any?, V : Any?> get(slice: ReadOnlySlice<K, V>?, key: K): V? { + if (key != element) { + throw UnsupportedOperationException() + } + @Suppress("UNCHECKED_CAST") + return when { + slice == BindingContext.DECLARATION_TO_DESCRIPTOR -> descriptor as V + slice == BindingContext.PRIMARY_CONSTRUCTOR_PARAMETER && (element as KtParameter).hasValOrVar() -> descriptor as V + else -> null + } + } + + override fun getDiagnostics(): Diagnostics { + throw UnsupportedOperationException() + } + + override fun addOwnDataTo(p0: BindingTrace, p1: Boolean) { + throw UnsupportedOperationException() + } + + override fun <K : Any?, V : Any?> getSliceContents(p0: ReadOnlySlice<K, V>): ImmutableMap<K, V> { + throw UnsupportedOperationException() + } + + } + } + throw UnsupportedOperationException() + } + + override fun <T : Any> getFrontendService(element: PsiElement, serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun <T : Any> getFrontendService(serviceClass: Class<T>): T { + return resolverForModule.componentProvider.getService(serviceClass) + } + + override fun <T : Any> getFrontendService(moduleDescriptor: ModuleDescriptor, serviceClass: Class<T>): T { + return resolverForModule.componentProvider.getService(serviceClass) + } + + override fun <T : Any> getIdeService(serviceClass: Class<T>): T { + throw UnsupportedOperationException() + } + + override fun getResolverForProject(): ResolverForProject<out ModuleInfo> { + throw UnsupportedOperationException() + } + +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeAnalysisContextCreator.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeAnalysisContextCreator.kt new file mode 100644 index 00000000..166e25fa --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeAnalysisContextCreator.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import com.intellij.mock.MockComponentManager +import com.intellij.mock.MockProject +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.AnalysisContextCreator +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.AnalysisContext +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.AnalysisEnvironment +import org.jetbrains.kotlin.analyzer.ResolverForModule +import org.jetbrains.kotlin.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.descriptors.ModuleDescriptor + +internal class IdeAnalysisContextCreator : AnalysisContextCreator { + override fun create( + project: MockProject, + moduleDescriptor: ModuleDescriptor, + moduleResolver: ResolverForModule, + kotlinEnvironment: KotlinCoreEnvironment, + analysisEnvironment: AnalysisEnvironment, + ): AnalysisContext { + val facade = DokkaResolutionFacade(project, moduleDescriptor, moduleResolver) + val projectComponentManager = project as MockComponentManager + projectComponentManager.registerService( + KotlinCacheService::class.java, + CoreKotlinCacheService(facade) + ) + return ResolutionFacadeAnalysisContext(facade, kotlinEnvironment, analysisEnvironment) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeCompilerExtensionPointProvider.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeCompilerExtensionPointProvider.kt new file mode 100644 index 00000000..73d908e9 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeCompilerExtensionPointProvider.kt @@ -0,0 +1,46 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerExtensionPointProvider +import org.jetbrains.kotlin.caches.resolve.CommonPlatformKindResolution +import org.jetbrains.kotlin.caches.resolve.IdePlatformKindResolution +import org.jetbrains.kotlin.caches.resolve.JsPlatformKindResolution +import org.jetbrains.kotlin.caches.resolve.JvmPlatformKindResolution +import org.jetbrains.kotlin.extensions.ApplicationExtensionDescriptor +import org.jetbrains.kotlin.ide.konan.NativePlatformKindResolution +import org.jetbrains.kotlin.platform.IdePlatformKind +import org.jetbrains.kotlin.platform.impl.CommonIdePlatformKind +import org.jetbrains.kotlin.platform.impl.JsIdePlatformKind +import org.jetbrains.kotlin.platform.impl.JvmIdePlatformKind +import org.jetbrains.kotlin.platform.impl.NativeIdePlatformKind + +internal class IdeCompilerExtensionPointProvider : CompilerExtensionPointProvider { + override fun get(): List<CompilerExtensionPointProvider.CompilerExtensionPoint> { + + @Suppress("UNCHECKED_CAST") + val idePlatformKind = CompilerExtensionPointProvider.CompilerExtensionPoint( + ApplicationExtensionDescriptor( + "org.jetbrains.kotlin.idePlatformKind", + IdePlatformKind::class.java + ) as ApplicationExtensionDescriptor<Any>, + listOf( + CommonIdePlatformKind, + JvmIdePlatformKind, + JsIdePlatformKind, + NativeIdePlatformKind + ) + ) + + @Suppress("UNCHECKED_CAST") + val resolution = CompilerExtensionPointProvider.CompilerExtensionPoint( + IdePlatformKindResolution as ApplicationExtensionDescriptor<Any>, + listOf( + CommonPlatformKindResolution(), + JvmPlatformKindResolution(), + JsPlatformKindResolution(), + NativePlatformKindResolution() + ) + ) + + return listOf(idePlatformKind, resolution) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeDescriptorAnalysisPlugin.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeDescriptorAnalysisPlugin.kt new file mode 100644 index 00000000..930e4a3f --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeDescriptorAnalysisPlugin.kt @@ -0,0 +1,38 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement + +@InternalDokkaApi +class IdeDescriptorAnalysisPlugin : DokkaPlugin() { + + internal val ideKdocFinder by extending { + plugin<CompilerDescriptorAnalysisPlugin>().kdocFinder providing ::IdePluginKDocFinder + } + + internal val ideDescriptorFinder by extending { + plugin<CompilerDescriptorAnalysisPlugin>().descriptorFinder providing { IdeDescriptorFinder() } + } + + internal val ideKlibService by extending { + plugin<CompilerDescriptorAnalysisPlugin>().klibService providing { IdeKLibService() } + } + + internal val ideCompilerExtensionPointProvider by extending { + plugin<CompilerDescriptorAnalysisPlugin>().compilerExtensionPointProvider providing { IdeCompilerExtensionPointProvider() } + } + + internal val ideApplicationHack by extending { + plugin<CompilerDescriptorAnalysisPlugin>().mockApplicationHack providing { IdeMockApplicationHack() } + } + + internal val ideAnalysisContextCreator by extending { + plugin<CompilerDescriptorAnalysisPlugin>().analysisContextCreator providing { IdeAnalysisContextCreator() } + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeDescriptorFinder.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeDescriptorFinder.kt new file mode 100644 index 00000000..bc591151 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeDescriptorFinder.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.DescriptorFinder +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor +import org.jetbrains.kotlin.psi.KtDeclaration + +internal class IdeDescriptorFinder : DescriptorFinder { + override fun KtDeclaration.findDescriptor(): DeclarationDescriptor? { + return this.descriptor + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeKLibService.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeKLibService.kt new file mode 100644 index 00000000..c3422f56 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeKLibService.kt @@ -0,0 +1,30 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KLibService +import org.jetbrains.kotlin.backend.common.serialization.metadata.KlibMetadataModuleDescriptorFactory +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.PackageFragmentProvider +import org.jetbrains.kotlin.idea.klib.createKlibPackageFragmentProvider +import org.jetbrains.kotlin.idea.klib.getCompatibilityInfo +import org.jetbrains.kotlin.incremental.components.LookupTracker +import org.jetbrains.kotlin.library.KotlinLibrary +import org.jetbrains.kotlin.storage.StorageManager + +internal class IdeKLibService : KLibService { + override fun KotlinLibrary.createPackageFragmentProvider( + storageManager: StorageManager, + metadataModuleDescriptorFactory: KlibMetadataModuleDescriptorFactory, + languageVersionSettings: LanguageVersionSettings, + moduleDescriptor: ModuleDescriptor, + lookupTracker: LookupTracker, + ): PackageFragmentProvider? { + return this.createKlibPackageFragmentProvider( + storageManager, metadataModuleDescriptorFactory, languageVersionSettings, moduleDescriptor, lookupTracker + ) + } + + override fun isAnalysisCompatible(kotlinLibrary: KotlinLibrary): Boolean { + return kotlinLibrary.getCompatibilityInfo().isCompatible + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeMockApplicationHack.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeMockApplicationHack.kt new file mode 100644 index 00000000..a582572d --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdeMockApplicationHack.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import com.intellij.mock.MockApplication +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.MockApplicationHack +import org.jetbrains.kotlin.idea.klib.KlibLoadingMetadataCache + +internal class IdeMockApplicationHack : MockApplicationHack { + override fun hack(mockApplication: MockApplication) { + if (mockApplication.getService(KlibLoadingMetadataCache::class.java) == null) + mockApplication.registerService(KlibLoadingMetadataCache::class.java, KlibLoadingMetadataCache()) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdePluginKDocFinder.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdePluginKDocFinder.kt new file mode 100644 index 00000000..d0d217f6 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/IdePluginKDocFinder.kt @@ -0,0 +1,48 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource +import org.jetbrains.kotlin.idea.kdoc.findKDoc +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils + +internal class IdePluginKDocFinder( + private val context: DokkaContext +) : KDocFinder { + + override fun KtElement.findKDoc(): KDocTag? { + return this.findKDoc { DescriptorToSourceUtils.descriptorToDeclaration(it) } + } + + override fun DeclarationDescriptor.find(descriptorToPsi: (DeclarationDescriptorWithSource) -> PsiElement?): KDocTag? { + return this.findKDoc(descriptorToPsi) + } + + override fun resolveKDocLink( + fromDescriptor: DeclarationDescriptor, + qualifiedName: String, + sourceSet: DokkaConfiguration.DokkaSourceSet, + emptyBindingContext: Boolean + ): Collection<DeclarationDescriptor> { + val facadeAnalysisContext = context + .plugin<CompilerDescriptorAnalysisPlugin>() + .querySingle { kotlinAnalysis }[sourceSet] as ResolutionFacadeAnalysisContext + + return org.jetbrains.kotlin.idea.kdoc.resolveKDocLink( + context = if (emptyBindingContext) BindingContext.EMPTY else facadeAnalysisContext.resolveSession.bindingContext, + resolutionFacade = facadeAnalysisContext.facade, + fromDescriptor = fromDescriptor, + fromSubjectOfTag = null, + qualifiedName = qualifiedName.split('.') + ) + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/ResolutionFacadeAnalysisContext.kt b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/ResolutionFacadeAnalysisContext.kt new file mode 100644 index 00000000..d16ee7d2 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/ide/ResolutionFacadeAnalysisContext.kt @@ -0,0 +1,30 @@ +package org.jetbrains.dokka.analysis.kotlin.descriptors.ide + +import com.intellij.openapi.project.Project +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.AnalysisContext +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.AnalysisEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.resolve.lazy.ResolveSession + +internal class ResolutionFacadeAnalysisContext( + val facade: DokkaResolutionFacade, + + private val kotlinEnvironment: KotlinCoreEnvironment, + private val analysisEnvironment: AnalysisEnvironment +) : AnalysisContext { + private var isClosed: Boolean = false + + override val environment: KotlinCoreEnvironment + get() = kotlinEnvironment.takeUnless { isClosed } + ?: throw IllegalStateException("AnalysisEnvironment is already closed") + + override val resolveSession: ResolveSession = facade.resolveSession + override val moduleDescriptor: ModuleDescriptor = facade.moduleDescriptor + override val project: Project = facade.project + + override fun close() { + isClosed = true + analysisEnvironment.dispose() + } +} diff --git a/subprojects/analysis-kotlin-descriptors/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-kotlin-descriptors/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..f993dcb1 --- /dev/null +++ b/subprojects/analysis-kotlin-descriptors/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.analysis.kotlin.descriptors.ide.IdeDescriptorAnalysisPlugin diff --git a/subprojects/analysis-kotlin-symbols/README.md b/subprojects/analysis-kotlin-symbols/README.md new file mode 100644 index 00000000..12e3041c --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/README.md @@ -0,0 +1,8 @@ +# Analysis: Kotlin symbols + +An internal symbols-based implementation for [analysis-kotlin-api](../analysis-kotlin-api), 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. diff --git a/subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api b/subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api diff --git a/subprojects/analysis-kotlin-symbols/build.gradle.kts b/subprojects/analysis-kotlin-symbols/build.gradle.kts new file mode 100644 index 00000000..c000df58 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.DokkaPublicationBuilder +import org.jetbrains.registerDokkaArtifactPublication + +plugins { + id("org.jetbrains.conventions.kotlin-jvm") + id("org.jetbrains.conventions.maven-publish") + id("com.github.johnrengelman.shadow") +} + +dependencies { + implementation(projects.subprojects.analysisKotlinApi) + implementation(projects.subprojects.analysisKotlinSymbols.compiler) + implementation(projects.subprojects.analysisKotlinSymbols.ide) +} + +tasks { + shadowJar { + val dokka_version: String by project + + // cannot be named exactly like the artifact (i.e analysis-kotlin-symbols-VER.jar), + // otherwise leads to obscure test failures when run via CLI, but not via IJ + archiveFileName.set("analysis-kotlin-symbols-all-$dokka_version.jar") + archiveClassifier.set("") + + // service files are merged to make sure all Dokka plugins + // from the dependencies are loaded, and not just a single one. + mergeServiceFiles() + } +} + +registerDokkaArtifactPublication("analysisKotlinSymbols") { + artifactId = "analysis-kotlin-symbols" + component = DokkaPublicationBuilder.Component.Shadow +} diff --git a/subprojects/analysis-kotlin-symbols/compiler/api/compiler.api b/subprojects/analysis-kotlin-symbols/compiler/api/compiler.api new file mode 100644 index 00000000..39870f57 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/compiler/api/compiler.api @@ -0,0 +1,4 @@ +public final class org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V +} + diff --git a/subprojects/analysis-kotlin-symbols/compiler/build.gradle.kts b/subprojects/analysis-kotlin-symbols/compiler/build.gradle.kts new file mode 100644 index 00000000..876d87ca --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/compiler/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("org.jetbrains.conventions.kotlin-jvm") +} + +dependencies { + compileOnly(projects.core) + compileOnly(projects.subprojects.analysisKotlinApi) + + // TODO + + // TODO [beresnev] get rid of it + compileOnly(libs.kotlinx.coroutines.core) +} diff --git a/subprojects/analysis-kotlin-symbols/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin.kt b/subprojects/analysis-kotlin-symbols/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin.kt new file mode 100644 index 00000000..4a39e0d8 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.compiler + +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement + +class CompilerSymbolsAnalysisPlugin : DokkaPlugin() { + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-symbols/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-kotlin-symbols/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..47163f6e --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.analysis.kotlin.symbols.compiler.CompilerSymbolsAnalysisPlugin diff --git a/subprojects/analysis-kotlin-symbols/ide/api/ide.api b/subprojects/analysis-kotlin-symbols/ide/api/ide.api new file mode 100644 index 00000000..9be46c0b --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/ide/api/ide.api @@ -0,0 +1,4 @@ +public final class org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V +} + diff --git a/subprojects/analysis-kotlin-symbols/ide/build.gradle.kts b/subprojects/analysis-kotlin-symbols/ide/build.gradle.kts new file mode 100644 index 00000000..876d87ca --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/ide/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("org.jetbrains.conventions.kotlin-jvm") +} + +dependencies { + compileOnly(projects.core) + compileOnly(projects.subprojects.analysisKotlinApi) + + // TODO + + // TODO [beresnev] get rid of it + compileOnly(libs.kotlinx.coroutines.core) +} diff --git a/subprojects/analysis-kotlin-symbols/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin.kt b/subprojects/analysis-kotlin-symbols/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin.kt new file mode 100644 index 00000000..33f555cb --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.ide + +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement + +class IdeSymbolsAnalysisPlugin : DokkaPlugin() { + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-symbols/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-kotlin-symbols/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..59245578 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.analysis.kotlin.symbols.ide.IdeSymbolsAnalysisPlugin diff --git a/subprojects/analysis-markdown-jb/README.md b/subprojects/analysis-markdown-jb/README.md new file mode 100644 index 00000000..2922abc8 --- /dev/null +++ b/subprojects/analysis-markdown-jb/README.md @@ -0,0 +1,7 @@ +# Ananlysis: Markdown (JetBrains) + +An internal module that encapsulates Markdown file and format parsing by using `org.jetbrains:markdown` +as the primary implementation dependency. + +Used by other Dokka modules, but it must not be used by external users directly until stable public API +is provided. diff --git a/subprojects/analysis-markdown-jb/api/analysis-markdown-jb.api b/subprojects/analysis-markdown-jb/api/analysis-markdown-jb.api new file mode 100644 index 00000000..3a8c37c5 --- /dev/null +++ b/subprojects/analysis-markdown-jb/api/analysis-markdown-jb.api @@ -0,0 +1,28 @@ +public final class org/jetbrains/dokka/analysis/markdown/jb/MarkdownApiKt { + public static final fun getMARKDOWN_ELEMENT_FILE_NAME ()Ljava/lang/String; +} + +public class org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser : org/jetbrains/dokka/analysis/markdown/jb/Parser { + public static final field Companion Lorg/jetbrains/dokka/analysis/markdown/jb/MarkdownParser$Companion; + public fun <init> (Lkotlin/jvm/functions/Function1;Ljava/lang/String;)V + public fun parseStringToDocNode (Ljava/lang/String;)Lorg/jetbrains/dokka/model/doc/DocTag; + protected fun parseTagWithBody (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/model/doc/TagWrapper; + protected fun preparse (Ljava/lang/String;)Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser$Companion { + public final fun fqDeclarationName (Lorg/jetbrains/dokka/links/DRI;)Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/markdown/jb/ParseUtilsKt { + public static final fun parseHtmlEncodedWithNormalisedSpaces (Ljava/lang/String;Z)Ljava/util/List; +} + +public abstract class org/jetbrains/dokka/analysis/markdown/jb/Parser { + public fun <init> ()V + public fun parse (Ljava/lang/String;)Lorg/jetbrains/dokka/model/doc/DocumentationNode; + public abstract fun parseStringToDocNode (Ljava/lang/String;)Lorg/jetbrains/dokka/model/doc/DocTag; + protected fun parseTagWithBody (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/model/doc/TagWrapper; + protected abstract fun preparse (Ljava/lang/String;)Ljava/lang/String; +} + diff --git a/subprojects/analysis-markdown-jb/build.gradle.kts b/subprojects/analysis-markdown-jb/build.gradle.kts new file mode 100644 index 00000000..c67ee53c --- /dev/null +++ b/subprojects/analysis-markdown-jb/build.gradle.kts @@ -0,0 +1,17 @@ +import org.jetbrains.registerDokkaArtifactPublication + +plugins { + id("org.jetbrains.conventions.kotlin-jvm") + id("org.jetbrains.conventions.maven-publish") +} + +dependencies { + compileOnly(projects.core) + + implementation(libs.jsoup) + implementation(libs.jetbrains.markdown) +} + +registerDokkaArtifactPublication("analysisMarkdown") { + artifactId = "analysis-markdown" +} diff --git a/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownApi.kt b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownApi.kt new file mode 100644 index 00000000..e8738190 --- /dev/null +++ b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownApi.kt @@ -0,0 +1,8 @@ +package org.jetbrains.dokka.analysis.markdown.jb + +import org.intellij.markdown.MarkdownElementTypes +import org.jetbrains.dokka.InternalDokkaApi + +// TODO [beresnev] move/rename if it's only used for CustomDocTag. for now left as is for compatibility +@InternalDokkaApi +val MARKDOWN_ELEMENT_FILE_NAME = MarkdownElementTypes.MARKDOWN_FILE.name diff --git a/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser.kt b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser.kt new file mode 100644 index 00000000..165ca4c6 --- /dev/null +++ b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/MarkdownParser.kt @@ -0,0 +1,511 @@ +package org.jetbrains.dokka.analysis.markdown.jb + +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.CompositeASTNode +import org.intellij.markdown.ast.LeafASTNode +import org.intellij.markdown.ast.impl.ListItemCompositeNode +import org.intellij.markdown.flavours.gfm.GFMElementTypes +import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor +import org.intellij.markdown.flavours.gfm.GFMTokenTypes +import org.intellij.markdown.html.HtmlGenerator +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.markdown.jb.factories.DocTagsFromIElementFactory +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.PointingToDeclaration +import org.jetbrains.dokka.model.doc.* +import java.net.MalformedURLException +import java.net.URL +import org.intellij.markdown.parser.MarkdownParser as IntellijMarkdownParser + +@InternalDokkaApi +open class MarkdownParser( + private val externalDri: (String) -> DRI?, + private val kdocLocation: String?, +) : Parser() { + + private lateinit var destinationLinksMap: Map<String, String> + private lateinit var text: String + + override fun parseStringToDocNode(extractedString: String): DocTag { + val gfmFlavourDescriptor = GFMFlavourDescriptor() + val markdownAstRoot = IntellijMarkdownParser(gfmFlavourDescriptor).buildMarkdownTreeFromString(extractedString) + destinationLinksMap = getAllDestinationLinks(extractedString, markdownAstRoot).toMap() + text = extractedString + + val parsed = visitNode(markdownAstRoot) + if (parsed.size == 1) { + return parsed.first() + } + return CustomDocTag(children = parsed, params = emptyMap(), name = "") + } + + override fun preparse(text: String) = text.replace("\r\n", "\n").replace("\r", "\n") + + override fun parseTagWithBody(tagName: String, content: String): TagWrapper = + when (tagName) { + "see" -> { + val referencedName = content.substringBefore(' ') + val dri = externalDri(referencedName) + See( + parseStringToDocNode(content.substringAfter(' ')), + dri?.fqDeclarationName() ?: referencedName, + dri + ) + } + "throws", "exception" -> { + val dri = externalDri(content.substringBefore(' ')) + Throws( + parseStringToDocNode(content.substringAfter(' ')), + dri?.fqDeclarationName() ?: content.substringBefore(' '), + dri + ) + } + else -> super.parseTagWithBody(tagName, content) + } + + private fun headersHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + node.type, + visitNode(node.children.find { it.type == MarkdownTokenTypes.ATX_CONTENT } + ?: throw detailedException("Wrong AST Tree. Header does not contain expected content", node) + ).flatMap { it.children } + ) + + private fun horizontalRulesHandler() = + DocTagsFromIElementFactory.getInstance(MarkdownTokenTypes.HORIZONTAL_RULE) + + private fun emphasisHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + node.type, + children = node.children.evaluateChildrenWithDroppedEnclosingTokens(1) + ) + + private fun strongHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + node.type, + children = node.children.evaluateChildrenWithDroppedEnclosingTokens(2) + ) + + private fun List<ASTNode>.evaluateChildrenWithDroppedEnclosingTokens(count: Int) = + drop(count).dropLast(count).evaluateChildren() + + private fun blockquotesHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + node.type, children = node.children + .filterIsInstance<CompositeASTNode>() + .evaluateChildren() + ) + + private fun listsHandler(node: ASTNode): List<DocTag> { + + val children = node.children.filterIsInstance<ListItemCompositeNode>().flatMap { + if (it.children.last().type in listOf( + MarkdownElementTypes.ORDERED_LIST, + MarkdownElementTypes.UNORDERED_LIST + ) + ) { + val nestedList = it.children.last() + (it.children as MutableList).removeAt(it.children.lastIndex) + listOf(it, nestedList) + } else + listOf(it) + } + + return DocTagsFromIElementFactory.getInstance( + node.type, + children = + children + .flatMap { + if (it.type == MarkdownElementTypes.LIST_ITEM) + DocTagsFromIElementFactory.getInstance( + it.type, + children = it + .children + .filterIsInstance<CompositeASTNode>() + .evaluateChildren() + ) + else + visitNode(it) + }, + params = + if (node.type == MarkdownElementTypes.ORDERED_LIST) { + val listNumberNode = node.children.first().children.first() + mapOf( + "start" to text.substring( + listNumberNode.startOffset, + listNumberNode.endOffset + ).trim().dropLast(1) + ) + } else + emptyMap() + ) + } + + private fun resolveDRI(mdLink: String): DRI? = + mdLink + .removePrefix("[") + .removeSuffix("]") + .let { link -> + try { + URL(link) + null + } catch (e: MalformedURLException) { + externalDri(link) + } + } + + private fun getAllDestinationLinks(text: String, node: ASTNode): List<Pair<String, String>> = + node.children + .filter { it.type == MarkdownElementTypes.LINK_DEFINITION } + .map { + text.substring(it.children[0].startOffset, it.children[0].endOffset).toLowerCase() to + text.substring(it.children[2].startOffset, it.children[2].endOffset) + } + + node.children.filterIsInstance<CompositeASTNode>().flatMap { getAllDestinationLinks(text, it) } + + + private fun referenceLinksHandler(node: ASTNode): List<DocTag> { + val linkLabel = node.children.find { it.type == MarkdownElementTypes.LINK_LABEL } + ?: throw detailedException("Wrong AST Tree. Reference link does not contain link label", node) + val linkText = node.children.findLast { it.type == MarkdownElementTypes.LINK_TEXT } ?: linkLabel + + val linkKey = text.substring(linkLabel.startOffset, linkLabel.endOffset) + + val link = destinationLinksMap[linkKey.toLowerCase()] ?: linkKey + + return linksHandler(linkText, link) + } + + private fun inlineLinksHandler(node: ASTNode): List<DocTag> { + val linkText = node.children.find { it.type == MarkdownElementTypes.LINK_TEXT } + ?: throw detailedException("Wrong AST Tree. Inline link does not contain link text", node) + val linkDestination = node.children.find { it.type == MarkdownElementTypes.LINK_DESTINATION } + val linkTitle = node.children.find { it.type == MarkdownElementTypes.LINK_TITLE } + + // Link destination may be ommited: https://github.github.com/gfm/#example-495 + val link = linkDestination?.let { text.substring(it.startOffset, it.endOffset) } + + return linksHandler(linkText, link, linkTitle) + } + + private fun markdownFileHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + node.type, + children = node.children + .filterSpacesAndEOL() + .evaluateChildren() + ) + + private fun autoLinksHandler(node: ASTNode): List<DocTag> { + val link = text.substring(node.startOffset + 1, node.endOffset - 1) + + return linksHandler(node, link) + } + + private fun linksHandler(linkText: ASTNode, link: String?, linkTitle: ASTNode? = null): List<DocTag> { + val dri: DRI? = link?.let { resolveDRI(it) } + val linkOrEmpty = link ?: "" + val linkTextString = + if (linkTitle == null) linkOrEmpty else text.substring(linkTitle.startOffset + 1, linkTitle.endOffset - 1) + + val params = if (linkTitle == null) + mapOf("href" to linkOrEmpty) + else + mapOf("href" to linkOrEmpty, "title" to linkTextString) + + return if (link != null && dri == null && !linkOrEmpty.isRemoteLink()) { + DocTagsFromIElementFactory.getInstance( + MarkdownTokenTypes.TEXT, + params = params, + children = linkText.children.drop(1).dropLast(1).evaluateChildren(), + body = linkTextString.removeSurrounding("[", "]") + ) + } else { + DocTagsFromIElementFactory.getInstance( + MarkdownElementTypes.INLINE_LINK, + params = params, + children = linkText.children.drop(1).dropLast(1).evaluateChildren(), + dri = dri + ) + } + } + + private fun codeLineHandler(node: ASTNode) = DocTagsFromIElementFactory.getInstance( + MarkdownElementTypes.CODE_BLOCK, + body = text.substring(node.startOffset, node.endOffset) + ) + + private fun textHandler(node: ASTNode, keepAllFormatting: Boolean) = DocTagsFromIElementFactory.getInstance( + MarkdownTokenTypes.TEXT, + body = text.substring(node.startOffset, node.endOffset).transform(), + keepFormatting = keepAllFormatting + ) + + private fun strikeThroughHandler(node: ASTNode) = DocTagsFromIElementFactory.getInstance( + node.type, + children = node.children.evaluateChildrenWithDroppedEnclosingTokens(2) + ) + + private fun tableHandler(node: ASTNode) = DocTagsFromIElementFactory.getInstance( + GFMElementTypes.TABLE, + children = node.children + .filter { it.type == GFMElementTypes.ROW || it.type == GFMElementTypes.HEADER } + .evaluateChildren() + ) + + private fun headerHandler(node: ASTNode) = DocTagsFromIElementFactory.getInstance( + GFMElementTypes.HEADER, + children = node.children + .filter { it.type == GFMTokenTypes.CELL } + .evaluateChildren() + ) + + private fun rowHandler(node: ASTNode) = DocTagsFromIElementFactory.getInstance( + GFMElementTypes.ROW, + children = node.children + .filter { it.type == GFMTokenTypes.CELL } + .evaluateChildren() + ) + + private fun cellHandler(node: ASTNode) = DocTagsFromIElementFactory.getInstance( + GFMTokenTypes.CELL, + children = node.children.filterTabSeparators().evaluateChildren().trimSurroundingTokensIfText() + ) + + private fun String.isRemoteLink() = try { + URL(this) + true + } catch (e: MalformedURLException) { + false + } + + private fun imagesHandler(node: ASTNode): List<DocTag> = + with(node.children.last().children) { + val destination = find { it.type == MarkdownElementTypes.LINK_DESTINATION } + val description = find { it.type == MarkdownElementTypes.LINK_TEXT } + + val src = destination?.let { + mapOf("href" to text.substring(it.startOffset, it.endOffset)) + } ?: emptyMap() + + val alt = description?.let { + mapOf("alt" to text.substring(it.startOffset + 1, it.endOffset - 1)) + } ?: emptyMap() + + return DocTagsFromIElementFactory.getInstance( + node.type, + params = src + alt + ) + } + + + private fun rawHtmlHandler(node: ASTNode): List<DocTag> = + DocTagsFromIElementFactory.getInstance( + node.type, + body = text.substring(node.startOffset, node.endOffset) + ) + + private fun codeSpansHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + node.type, + children = DocTagsFromIElementFactory.getInstance( + MarkdownTokenTypes.TEXT, + body = text.substring(node.startOffset + 1, node.endOffset - 1).replace('\n', ' ').trimIndent(), + keepFormatting = true + ) + ) + + private fun codeFencesHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + node.type, + children = node + .children + .dropWhile { it.type != MarkdownTokenTypes.CODE_FENCE_CONTENT } + .dropLastWhile { it.type != MarkdownTokenTypes.CODE_FENCE_CONTENT } + .filter { it.type != MarkdownTokenTypes.WHITE_SPACE } + .map { + if (it.type == MarkdownTokenTypes.EOL) + LeafASTNode(MarkdownTokenTypes.HARD_LINE_BREAK, 0, 0) + else + it + }.evaluateChildren(keepAllFormatting = true), + params = node + .children + .find { it.type == MarkdownTokenTypes.FENCE_LANG } + ?.let { mapOf("lang" to text.substring(it.startOffset, it.endOffset)) } + ?: emptyMap() + ) + + private fun codeBlocksHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance(node.type, children = node.children.mergeLeafASTNodes().flatMap { + DocTagsFromIElementFactory.getInstance( + MarkdownTokenTypes.TEXT, + body = HtmlGenerator.trimIndents(text.substring(it.startOffset, it.endOffset), 4).toString() + ) + }) + + private fun defaultHandler(node: ASTNode) = + DocTagsFromIElementFactory.getInstance( + MarkdownElementTypes.PARAGRAPH, + children = node.children.evaluateChildren() + ) + + private fun visitNode(node: ASTNode, keepAllFormatting: Boolean = false): List<DocTag> = + when (node.type) { + MarkdownElementTypes.ATX_1, + MarkdownElementTypes.ATX_2, + MarkdownElementTypes.ATX_3, + MarkdownElementTypes.ATX_4, + MarkdownElementTypes.ATX_5, + MarkdownElementTypes.ATX_6, + -> headersHandler(node) + MarkdownTokenTypes.HORIZONTAL_RULE -> horizontalRulesHandler() + MarkdownElementTypes.STRONG -> strongHandler(node) + MarkdownElementTypes.EMPH -> emphasisHandler(node) + MarkdownElementTypes.FULL_REFERENCE_LINK, + MarkdownElementTypes.SHORT_REFERENCE_LINK, + -> referenceLinksHandler(node) + MarkdownElementTypes.INLINE_LINK -> inlineLinksHandler(node) + MarkdownElementTypes.AUTOLINK -> autoLinksHandler(node) + MarkdownElementTypes.BLOCK_QUOTE -> blockquotesHandler(node) + MarkdownElementTypes.UNORDERED_LIST, + MarkdownElementTypes.ORDERED_LIST, + -> listsHandler(node) + MarkdownElementTypes.CODE_BLOCK -> codeBlocksHandler(node) + MarkdownElementTypes.CODE_FENCE -> codeFencesHandler(node) + MarkdownElementTypes.CODE_SPAN -> codeSpansHandler(node) + MarkdownElementTypes.IMAGE -> imagesHandler(node) + MarkdownElementTypes.HTML_BLOCK, + MarkdownTokenTypes.HTML_TAG, + MarkdownTokenTypes.HTML_BLOCK_CONTENT, + -> rawHtmlHandler(node) + MarkdownTokenTypes.HARD_LINE_BREAK -> DocTagsFromIElementFactory.getInstance(node.type) + MarkdownTokenTypes.CODE_FENCE_CONTENT, + MarkdownTokenTypes.CODE_LINE, + -> codeLineHandler(node) + MarkdownTokenTypes.TEXT -> textHandler(node, keepAllFormatting) + MarkdownElementTypes.MARKDOWN_FILE -> markdownFileHandler(node) + GFMElementTypes.STRIKETHROUGH -> strikeThroughHandler(node) + GFMElementTypes.TABLE -> tableHandler(node) + GFMElementTypes.HEADER -> headerHandler(node) + GFMElementTypes.ROW -> rowHandler(node) + GFMTokenTypes.CELL -> cellHandler(node) + else -> defaultHandler(node) + } + + private fun List<ASTNode>.filterTabSeparators() = + this.filterNot { it.type == GFMTokenTypes.TABLE_SEPARATOR } + + private fun List<ASTNode>.filterSpacesAndEOL() = + this.filterNot { it.type == MarkdownTokenTypes.WHITE_SPACE || it.type == MarkdownTokenTypes.EOL } + + private fun List<ASTNode>.evaluateChildren(keepAllFormatting: Boolean = false): List<DocTag> = + this.removeUselessTokens().swapImagesThatShouldBeLinks(keepAllFormatting).mergeLeafASTNodes().flatMap { visitNode(it, keepAllFormatting) } + + private fun List<ASTNode>.swapImagesThatShouldBeLinks(keepAllFormatting: Boolean): List<ASTNode> = + if (keepAllFormatting) { + this + } else { + flatMap { node -> + if (node.type == MarkdownElementTypes.IMAGE + && node.children.firstOrNull()?.let { it is LeafASTNode && it.type.name == "!" } == true + && node.children.lastOrNull()?.type == MarkdownElementTypes.SHORT_REFERENCE_LINK + ) { + node.children + } else { + listOf(node) + } + } + } + + private fun List<ASTNode>.removeUselessTokens(): List<ASTNode> = + this.filterIndexed { index, node -> + !(node.type == MarkdownElementTypes.LINK_DEFINITION || ( + node.type == MarkdownTokenTypes.EOL && + this.getOrNull(index - 1)?.type == MarkdownTokenTypes.HARD_LINE_BREAK + )) + } + + private fun List<DocTag>.trimSurroundingTokensIfText() = mapIndexed { index, elem -> + val elemTransformed = if (index == 0 && elem is Text) elem.copy(elem.body.trimStart()) else elem + if (index == lastIndex && elemTransformed is Text) elemTransformed.copy(elemTransformed.body.trimEnd()) else elemTransformed + } + + private val notLeafNodes = listOf( + MarkdownTokenTypes.HORIZONTAL_RULE, + MarkdownTokenTypes.HARD_LINE_BREAK, + MarkdownTokenTypes.HTML_TAG, + MarkdownTokenTypes.HTML_BLOCK_CONTENT + ) + + private fun ASTNode.isNotLeaf() = this is CompositeASTNode || this.type in notLeafNodes + + private fun List<ASTNode>.isNotLeaf(index: Int): Boolean = + if (index in 0..this.lastIndex) + this[index].isNotLeaf() + else + false + + private fun List<ASTNode>.mergeLeafASTNodes(): List<ASTNode> { + val children: MutableList<ASTNode> = mutableListOf() + var index = 0 + while (index <= this.lastIndex) { + if (this.isNotLeaf(index)) { + children += this[index] + } else { + val startOffset = this[index].startOffset + val sIndex = index + while (index < this.lastIndex) { + if (this.isNotLeaf(index + 1) || this[index + 1].startOffset != this[index].endOffset) { + children += mergedLeafNode(this, index, startOffset, sIndex) + break + } + index++ + } + if (index == this.lastIndex) { + children += mergedLeafNode(this, index, startOffset, sIndex) + } + } + index++ + } + return children + } + + private fun mergedLeafNode(nodes: List<ASTNode>, index: Int, startOffset: Int, sIndex: Int): LeafASTNode { + val endOffset = nodes[index].endOffset + val type = if (nodes.subList(sIndex, index) + .any { it.type == MarkdownTokenTypes.CODE_LINE } + ) MarkdownTokenTypes.CODE_LINE else MarkdownTokenTypes.TEXT + return LeafASTNode(type, startOffset, endOffset) + } + + private fun String.transform() = this + .replace(Regex("\n\n+"), "") // Squashing new lines between paragraphs + .replace(Regex("\n"), " ") + .replace(Regex(" >+ +"), " ") // Replacement used in blockquotes, get rid of garbage + + private fun detailedException(baseMessage: String, node: ASTNode) = + IllegalStateException( + baseMessage + " in ${kdocLocation ?: "unspecified location"}, element starts from offset ${node.startOffset} and ends ${node.endOffset}: ${ + text.substring( + node.startOffset, + node.endOffset + ) + }" + ) + + + companion object { + fun DRI.fqDeclarationName(): String? { + if (this.target !is PointingToDeclaration) { + return null + } + return listOfNotNull(this.packageName, this.classNames, this.callable?.name) + .joinToString(separator = ".") + .takeIf { it.isNotBlank() } + } + } +} + diff --git a/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/ParseUtils.kt b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/ParseUtils.kt new file mode 100644 index 00000000..7f520591 --- /dev/null +++ b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/ParseUtils.kt @@ -0,0 +1,39 @@ +package org.jetbrains.dokka.analysis.markdown.jb + +import org.intellij.markdown.lexer.Compat +import org.intellij.markdown.lexer.Compat.forEachCodePoint +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.model.doc.DocTag +import org.jetbrains.dokka.model.doc.Text +import org.jsoup.internal.StringUtil +import org.jsoup.nodes.Entities + +@InternalDokkaApi +fun String.parseHtmlEncodedWithNormalisedSpaces( + renderWhiteCharactersAsSpaces: Boolean +): List<DocTag> { + val accum = StringBuilder() + val tags = mutableListOf<DocTag>() + var lastWasWhite = false + + forEachCodePoint { c -> + if (renderWhiteCharactersAsSpaces && StringUtil.isWhitespace(c)) { + if (!lastWasWhite) { + accum.append(' ') + lastWasWhite = true + } + } else if (Compat.codePointToString(c).let { it != Entities.escape(it) }) { + accum.toString().takeIf { it.isNotBlank() }?.let { tags.add(Text(it)) } + accum.delete(0, accum.length) + + accum.appendCodePoint(c) + tags.add(Text(accum.toString(), params = DocTag.contentTypeParam("html"))) + accum.delete(0, accum.length) + } else if (!StringUtil.isInvisibleChar(c)) { + accum.appendCodePoint(c) + lastWasWhite = false + } + } + accum.toString().takeIf { it.isNotBlank() }?.let { tags.add(Text(it)) } + return tags +} diff --git a/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/Parser.kt b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/Parser.kt new file mode 100644 index 00000000..09650b32 --- /dev/null +++ b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/Parser.kt @@ -0,0 +1,131 @@ +package org.jetbrains.dokka.analysis.markdown.jb + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.model.doc.* + +@InternalDokkaApi +abstract class Parser { + + abstract fun parseStringToDocNode(extractedString: String): DocTag + + protected abstract fun preparse(text: String): String + + open fun parse(text: String): DocumentationNode = + DocumentationNode(extractTagsToListOfPairs(preparse(text)).map { (tag, content) -> parseTagWithBody(tag, content) }) + + protected open fun parseTagWithBody(tagName: String, content: String): TagWrapper = + when (tagName) { + "description" -> Description(parseStringToDocNode(content)) + "author" -> Author(parseStringToDocNode(content)) + "version" -> Version(parseStringToDocNode(content)) + "since" -> Since(parseStringToDocNode(content)) + "see" -> See( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' '), + null + ) + "param" -> Param( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' ') + ) + "property" -> Property( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' ') + ) + "return" -> Return(parseStringToDocNode(content)) + "constructor" -> Constructor(parseStringToDocNode(content)) + "receiver" -> Receiver(parseStringToDocNode(content)) + "throws", "exception" -> Throws( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' '), + null + ) + "deprecated" -> Deprecated(parseStringToDocNode(content)) + "sample" -> Sample( + parseStringToDocNode(content.substringAfter(' ')), + content.substringBefore(' ') + ) + "suppress" -> Suppress(parseStringToDocNode(content)) + else -> CustomTagWrapper(parseStringToDocNode(content), tagName) + } + + /** + * KDoc parser from Kotlin compiler relies on a comment asterisk + * So there is a mini parser here + * TODO: at least to adapt [org.jetbrains.kotlin.kdoc.lexer.KDocLexer] to analyze KDoc without the asterisks and use it here + */ + private fun extractTagsToListOfPairs(text: String): List<Pair<String, String>> = + "description $text" + .extractKDocSections() + .map { content -> + val contentWithEscapedAts = content.replace("\\@", "@") + val (tag, body) = contentWithEscapedAts.split(" ", limit = 2) + tag to body + } + + /** + * Ignore a doc tag inside code spans and blocks + * @see org.jetbrains.kotlin.kdoc.psi.impl.KDocSection + */ + private fun CharSequence.extractKDocSections(delimiter: String = "\n@"): List<String> { + var countOfBackticks = 0 + var countOfTildes = 0 + var countOfBackticksInOpeningFence = 0 + var countOfTildesInOpeningFence = 0 + + var isInCode = false + val result = mutableListOf<String>() + var rangeStart = 0 + var rangeEnd = 0 + var currentOffset = 0 + while (currentOffset < length) { + + when (get(currentOffset)) { + '`' -> { + countOfBackticks++ + countOfTildes = 0 + } + '~' -> { + countOfTildes++ + countOfBackticks = 0 + } + else -> { + if (isInCode) { + // The closing code fence must be at least as long as the opening fence + if(countOfBackticks >= countOfBackticksInOpeningFence + || countOfTildes >= countOfTildesInOpeningFence) + isInCode = false + } else { + // as per CommonMark spec, there can be any number of backticks for a code span, not only one or three + if (countOfBackticks > 0) { + isInCode = true + countOfBackticksInOpeningFence = countOfBackticks + countOfTildesInOpeningFence = Int.MAX_VALUE + } + // tildes are only for a code block, not code span + if (countOfTildes >= 3) { + isInCode = true + countOfTildesInOpeningFence = countOfTildes + countOfBackticksInOpeningFence = Int.MAX_VALUE + } + } + countOfTildes = 0 + countOfBackticks = 0 + } + } + if (!isInCode && startsWith(delimiter, currentOffset)) { + result.add(substring(rangeStart, rangeEnd)) + currentOffset += delimiter.length + rangeStart = currentOffset + rangeEnd = currentOffset + continue + } + + ++rangeEnd + ++currentOffset + } + result.add(substring(rangeStart, rangeEnd)) + return result + } + +} diff --git a/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/factories/DocTagsFromIElementFactory.kt b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/factories/DocTagsFromIElementFactory.kt new file mode 100644 index 00000000..86de8000 --- /dev/null +++ b/subprojects/analysis-markdown-jb/src/main/kotlin/org/jetbrains/dokka/analysis/markdown/jb/factories/DocTagsFromIElementFactory.kt @@ -0,0 +1,86 @@ +package org.jetbrains.dokka.analysis.markdown.jb.factories + +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.flavours.gfm.GFMElementTypes +import org.intellij.markdown.flavours.gfm.GFMTokenTypes +import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.analysis.markdown.jb.parseHtmlEncodedWithNormalisedSpaces +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.DocTag.Companion.contentTypeParam +import org.jsoup.Jsoup + +internal object DocTagsFromIElementFactory { + + @Suppress("IMPLICIT_CAST_TO_ANY") + fun getInstance(type: IElementType, children: List<DocTag> = emptyList(), params: Map<String, String> = emptyMap(), body: String? = null, dri: DRI? = null, keepFormatting: Boolean = false) = + when(type) { + MarkdownElementTypes.SHORT_REFERENCE_LINK, + MarkdownElementTypes.FULL_REFERENCE_LINK, + MarkdownElementTypes.INLINE_LINK -> if(dri == null) A(children, params) else DocumentationLink(dri, children, params) + MarkdownElementTypes.STRONG -> B(children, params) + MarkdownElementTypes.BLOCK_QUOTE -> BlockQuote(children, params) + MarkdownElementTypes.CODE_SPAN -> CodeInline(children, params) + MarkdownElementTypes.CODE_BLOCK, + MarkdownElementTypes.CODE_FENCE -> CodeBlock(children, params) + MarkdownElementTypes.ATX_1 -> H1(children, params) + MarkdownElementTypes.ATX_2 -> H2(children, params) + MarkdownElementTypes.ATX_3 -> H3(children, params) + MarkdownElementTypes.ATX_4 -> H4(children, params) + MarkdownElementTypes.ATX_5 -> H5(children, params) + MarkdownElementTypes.ATX_6 -> H6(children, params) + MarkdownElementTypes.EMPH -> I(children, params) + MarkdownElementTypes.IMAGE -> Img(children, params) + MarkdownElementTypes.LIST_ITEM -> Li(children, params) + MarkdownElementTypes.ORDERED_LIST -> Ol(children, params) + MarkdownElementTypes.UNORDERED_LIST -> Ul(children, params) + MarkdownElementTypes.PARAGRAPH -> P(children, params) + MarkdownTokenTypes.TEXT -> if (keepFormatting) Text( + body.orEmpty(), + children, + params + ) else { + // corner case: there are only spaces between two Markdown nodes + val containsOnlySpaces = body?.isNotEmpty() == true && body.all { it.isWhitespace() } + if (containsOnlySpaces) Text(" ", children, params) + else body?.parseWithNormalisedSpaces(renderWhiteCharactersAsSpaces = false).orEmpty() + } + MarkdownTokenTypes.HORIZONTAL_RULE -> HorizontalRule + MarkdownTokenTypes.HARD_LINE_BREAK -> Br + GFMElementTypes.STRIKETHROUGH -> Strikethrough(children, params) + GFMElementTypes.TABLE -> Table(children, params) + GFMElementTypes.HEADER -> Th(children, params) + GFMElementTypes.ROW -> Tr(children, params) + GFMTokenTypes.CELL -> Td(children, params) + MarkdownElementTypes.MARKDOWN_FILE -> CustomDocTag(children, params, MARKDOWN_ELEMENT_FILE_NAME) + MarkdownElementTypes.HTML_BLOCK, + MarkdownTokenTypes.HTML_TAG, + MarkdownTokenTypes.HTML_BLOCK_CONTENT -> Text(body.orEmpty(), params = params + contentTypeParam("html")) + else -> CustomDocTag(children, params, type.name) + }.let { + @Suppress("UNCHECKED_CAST") + when (it) { + is List<*> -> it as List<DocTag> + else -> listOf(it as DocTag) + } + } + + /** + * Parses string into [Text] doc tags that can have either value of the string or html-encoded value with content-type=html parameter. + * Content type is added when dealing with html entries like ` ` + */ + private fun String.parseWithNormalisedSpaces( + renderWhiteCharactersAsSpaces: Boolean + ): List<DocTag> { + if (!requiresHtmlEncoding()) { + return parseHtmlEncodedWithNormalisedSpaces(renderWhiteCharactersAsSpaces) + } + // parsing it using jsoup is required to get codePoints, otherwise they are interpreted separately, as chars + // But we dont need to do it for java as it is already parsed with jsoup + return Jsoup.parseBodyFragment(this).body().wholeText().parseHtmlEncodedWithNormalisedSpaces(renderWhiteCharactersAsSpaces) + } + + private fun String.requiresHtmlEncoding(): Boolean = indexOf('&') != -1 +} |