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/analysis-kotlin-descriptors/compiler | |
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/analysis-kotlin-descriptors/compiler')
62 files changed, 5315 insertions, 0 deletions
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 + } +} |