diff options
author | Vadim Mishenev <vad-mishenev@yandex.ru> | 2023-08-28 19:42:21 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-28 19:42:21 +0300 |
commit | 0e00edc6fcd406fcf38673ef6a2f8f59e8374de2 (patch) | |
tree | 697b0de0d44b421c922f1f5e6a7c1352f17c68a6 | |
parent | bec2cac91726e52884329e7997207e9777abaab7 (diff) | |
download | dokka-0e00edc6fcd406fcf38673ef6a2f8f59e8374de2.tar.gz dokka-0e00edc6fcd406fcf38673ef6a2f8f59e8374de2.tar.bz2 dokka-0e00edc6fcd406fcf38673ef6a2f8f59e8374de2.zip |
Support Dokka K2 analysis (#3094)
Dokka has its own documentable model to represent analyzed code. The analysis is performed by a compiler frontend.
In K1 the compiler frontend has descriptors that use the underlying Binding Context (global shared stateful structure). Dokka just maps descriptors to Documentable by DefaultDescriptorToDocumentableTranslator.
K2 compiler has FIR tree, which means “Frontend Intermediate Representation”, instead of Binding Context. But we do not use FIR in Dokka directly, since it is too low-level for analysis. The Kotlin compiler provides high-level Analysis API for this case. The API is used by KSP too. Analysis API represent elements of FIR (declarations, parameters and so on) as Symbols. For more details see KtSymbolByFirBuilder, KtSymbol.
For Dokka symbol is the replacement of descriptor in K2.
Also, to set up the environment of project analysis in K1 we use idea dependencies (or copy-past from there). In K2 for these aims, there is a Standalone mode for Analysis API.
98 files changed, 3555 insertions, 98 deletions
diff --git a/build-logic/src/main/kotlin/org/jetbrains/conventions/base-unit-test.gradle.kts b/build-logic/src/main/kotlin/org/jetbrains/conventions/base-unit-test.gradle.kts new file mode 100644 index 00000000..3ece2e8d --- /dev/null +++ b/build-logic/src/main/kotlin/org/jetbrains/conventions/base-unit-test.gradle.kts @@ -0,0 +1,39 @@ +package org.jetbrains.conventions + +/** + * Utility to run ynit tests for K1 and K2 (analysis API). + */ + +plugins { + id("org.jetbrains.conventions.base") + id("org.jetbrains.conventions.base-java") +} + +val descriptorsTestConfiguration: Configuration by configurations.creating { + extendsFrom(configurations.testImplementation.get()) +} +val symbolsTestConfiguration: Configuration by configurations.creating { + extendsFrom(configurations.testImplementation.get()) +} + +val symbolsTest = tasks.register<Test>("symbolsTest") { + useJUnitPlatform { + excludeTags("onlyDescriptors", "onlyDescriptorsMPP", "javaCode", "usingJDK") + } + classpath += symbolsTestConfiguration +} +// run symbols and descriptors tests +tasks.test { + //enabled = false + classpath += descriptorsTestConfiguration + dependsOn(symbolsTest) +} + +val descriptorsTest = tasks.register<Test>("descriptorsTest") { + classpath += descriptorsTestConfiguration +} + +tasks.check { + dependsOn(symbolsTest) +} + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a09f1413..9170c40b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,10 +6,12 @@ gradlePlugin-android = "4.2.2" gradlePlugin-dokka = "1.8.20" kotlinx-coroutines = "1.6.3" +kotlinx-collections-immutable = "0.3.4" kotlinx-bcv = "0.12.1" ## Analysis kotlin-compiler = "1.9.0" +kotlin-compiler-k2 = "1.9.0-release-358" # MUST match the version of the intellij platform used in the kotlin compiler, # otherwise this will lead to different versions of psi API and implementations @@ -56,6 +58,7 @@ eclipse-jgit = "5.12.0.202106070339-r" [libraries] kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm", version.ref = "kotlinx-collections-immutable" } #### Gradle plugins #### # The Maven coordinates of Gradle plugins that are either used in convention plugins, or in Dokka subprojects @@ -68,6 +71,19 @@ gradlePlugin-gradlePublish= { module = "com.gradle.publish:plugin-publish-plugin #### Kotlin analysis #### kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin-compiler" } +###### K2 analysis ###### +kotlin-compiler-k2 = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin-compiler-k2" } +kotlin-high-level-api-api = { module = "org.jetbrains.kotlin:high-level-api-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-high-level-api-impl = { module = "org.jetbrains.kotlin:high-level-api-impl-base-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-high-level-api-fir = { module = "org.jetbrains.kotlin:high-level-api-fir-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-high-level-api-fe10 = { module = "org.jetbrains.kotlin:high-level-api-fe10-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-low-level-api-fir = { module = "org.jetbrains.kotlin:low-level-api-fir-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-analysis-project-structure = { module = "org.jetbrains.kotlin:analysis-project-structure-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-analysis-api-standalone = { module = "org.jetbrains.kotlin:analysis-api-standalone-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-analysis-api-providers = { module = "org.jetbrains.kotlin:analysis-api-providers-for-ide", version.ref = "kotlin-compiler-k2" } +kotlin-symbol-light-classes = { module = "org.jetbrains.kotlin:symbol-light-classes-for-ide", version.ref = "kotlin-compiler-k2" } + + #### Java analysis #### intellij-java-psi-api = { module = "com.jetbrains.intellij.java:java-psi", version.ref = "intellij-platform" } intellij-java-psi-impl = { module = "com.jetbrains.intellij.java:java-psi-impl", version.ref = "intellij-platform" } diff --git a/plugins/android-documentation/build.gradle.kts b/plugins/android-documentation/build.gradle.kts index 4dfc972d..545f8435 100644 --- a/plugins/android-documentation/build.gradle.kts +++ b/plugins/android-documentation/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.registerDokkaArtifactPublication plugins { id("org.jetbrains.conventions.kotlin-jvm") id("org.jetbrains.conventions.maven-publish") + id("org.jetbrains.conventions.base-unit-test") } dependencies { @@ -13,10 +14,15 @@ dependencies { implementation(kotlin("reflect")) testImplementation(projects.plugins.base) - testImplementation(projects.plugins.base.baseTestUtils) testImplementation(projects.core.testApi) testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter) + + symbolsTestConfiguration(project(path = ":subprojects:analysis-kotlin-symbols", configuration = "shadow")) + descriptorsTestConfiguration(project(path = ":subprojects:analysis-kotlin-descriptors", configuration = "shadow")) + testImplementation(projects.plugins.base.baseTestUtils) { + exclude(module = "analysis-kotlin-descriptors") + } } registerDokkaArtifactPublication("androidDocumentationPlugin") { diff --git a/plugins/base/base-test-utils/build.gradle.kts b/plugins/base/base-test-utils/build.gradle.kts index ef4f9f7b..20c3b727 100644 --- a/plugins/base/base-test-utils/build.gradle.kts +++ b/plugins/base/base-test-utils/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { api(projects.subprojects.analysisKotlinApi) // TODO [beresnev] analysis switcher + //runtimeOnly(project(path = ":subprojects:analysis-kotlin-symbols", configuration = "shadow")) runtimeOnly(project(path = ":subprojects:analysis-kotlin-descriptors", configuration = "shadow")) implementation(kotlin("reflect")) diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts index 8bea63e8..2f9f5863 100644 --- a/plugins/base/build.gradle.kts +++ b/plugins/base/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("org.jetbrains.conventions.kotlin-jvm") id("org.jetbrains.conventions.maven-publish") id("org.jetbrains.conventions.dokka-html-frontend-files") + id("org.jetbrains.conventions.base-unit-test") } dependencies { @@ -26,7 +27,11 @@ dependencies { } // Test only - testImplementation(projects.plugins.base.baseTestUtils) + symbolsTestConfiguration(project(path = ":subprojects:analysis-kotlin-symbols", configuration = "shadow")) + descriptorsTestConfiguration(project(path = ":subprojects:analysis-kotlin-descriptors", configuration = "shadow")) + testImplementation(projects.plugins.base.baseTestUtils) { + exclude(module = "analysis-kotlin-descriptors") + } testImplementation(projects.core.contentMatcherTestUtils) testImplementation(projects.core.testApi) testImplementation(platform(libs.junit.bom)) @@ -37,6 +42,12 @@ dependencies { } } + + + + + + // access the frontend files via the dependency on :plugins:base:frontend val dokkaHtmlFrontendFiles: Provider<FileCollection> = configurations.dokkaHtmlFrontendFiles.map { frontendFiles -> diff --git a/plugins/base/src/test/kotlin/basic/DRITest.kt b/plugins/base/src/test/kotlin/basic/DRITest.kt index 9c443567..c70c1b0b 100644 --- a/plugins/base/src/test/kotlin/basic/DRITest.kt +++ b/plugins/base/src/test/kotlin/basic/DRITest.kt @@ -10,6 +10,7 @@ import org.jetbrains.dokka.pages.ClasslikePageNode import org.jetbrains.dokka.pages.ContentPage import org.jetbrains.dokka.pages.MemberPageNode import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class DRITest : BaseAbstractTest() { diff --git a/plugins/base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt b/plugins/base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt index 46239baa..82159e0d 100644 --- a/plugins/base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt +++ b/plugins/base/src/test/kotlin/content/annotations/ContentForAnnotationsTest.kt @@ -10,10 +10,7 @@ import org.jetbrains.dokka.pages.ContentText import org.jetbrains.dokka.pages.MemberPageNode import org.jetbrains.dokka.pages.PackagePageNode import org.junit.jupiter.api.Test -import utils.ParamAttributes -import utils.assertNotNull -import utils.bareSignature -import utils.propertySignature +import utils.* import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -318,6 +315,7 @@ class ContentForAnnotationsTest : BaseAbstractTest() { } } + @JavaCode @Test fun `annotated bounds in Java`() { testInline( diff --git a/plugins/base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt b/plugins/base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt index 961ce5f5..c25c1a1b 100644 --- a/plugins/base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt +++ b/plugins/base/src/test/kotlin/content/annotations/JavaDeprecatedTest.kt @@ -9,10 +9,12 @@ import org.jetbrains.dokka.model.properties.WithExtraProperties import org.jetbrains.dokka.pages.ContentPage import org.jetbrains.dokka.pages.ContentStyle import org.junit.jupiter.api.Test +import utils.JavaCode import utils.pWrapped import kotlin.test.assertEquals import kotlin.test.assertTrue +@JavaCode class JavaDeprecatedTest : BaseAbstractTest() { private val testConfiguration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/content/exceptions/ContentForExceptions.kt b/plugins/base/src/test/kotlin/content/exceptions/ContentForExceptions.kt index 14a36611..17bd209e 100644 --- a/plugins/base/src/test/kotlin/content/exceptions/ContentForExceptions.kt +++ b/plugins/base/src/test/kotlin/content/exceptions/ContentForExceptions.kt @@ -7,9 +7,7 @@ import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.model.DisplaySourceSet import org.junit.jupiter.api.Test -import utils.ParamAttributes -import utils.bareSignature -import utils.findTestType +import utils.* import kotlin.test.assertEquals class ContentForExceptions : BaseAbstractTest() { @@ -55,6 +53,7 @@ class ContentForExceptions : BaseAbstractTest() { ) } + @OnlyDescriptors("Fixed in 1.9.20 (IMPORT STAR)") @Test fun `function with navigatable thrown exception`() { testInline( diff --git a/plugins/base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt b/plugins/base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt index e5059073..2994fb42 100644 --- a/plugins/base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt +++ b/plugins/base/src/test/kotlin/content/inheritors/ContentForInheritorsTest.kt @@ -6,6 +6,7 @@ import org.jetbrains.dokka.PluginConfigurationImpl import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.junit.jupiter.api.Test +import utils.OnlyDescriptors import utils.classSignature import utils.findTestType import kotlin.test.assertEquals @@ -128,6 +129,7 @@ class ContentForInheritorsTest : BaseAbstractTest() { } } + @OnlyDescriptors("Order of inheritors is different in K2") @Test fun `interface with few inheritors has table in description`() { testInline( diff --git a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt index e74cb49d..df5efd03 100644 --- a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt +++ b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt @@ -242,6 +242,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @JavaCode @Test fun `deprecated with multiple links inside`() { testInline( @@ -346,6 +347,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @JavaCode @Test fun `deprecated with an multiple inline links`() { testInline( @@ -410,6 +412,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @JavaCode @Test fun `multiline throws with comment`() { testInline( @@ -473,6 +476,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @OnlyDescriptors("Fixed in 1.9.20 (IMPORT STAR)") @Test fun `multiline kotlin throws with comment`() { testInline( @@ -590,6 +594,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @JavaCode @Test fun `multiline throws where exception is not in the same line as description`() { testInline( @@ -673,6 +678,7 @@ class ContentForParamsTest : BaseAbstractTest() { } + @JavaCode @Test fun `documentation splitted in 2 using enters`() { testInline( @@ -718,6 +724,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @JavaCode @Test fun `multiline return tag with param`() { testInline( @@ -783,6 +790,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @UsingJDK @Test fun `return tag in kotlin`() { testInline( @@ -830,6 +838,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @JavaCode @Test fun `list with links and description`() { testInline( @@ -1476,6 +1485,7 @@ class ContentForParamsTest : BaseAbstractTest() { } } + @JavaCode @Test fun javaDocCommentWithDocumentedParameters() { testInline( diff --git a/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt b/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt index 79c1e1ad..82a04b89 100644 --- a/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt +++ b/plugins/base/src/test/kotlin/content/seealso/ContentForSeeAlsoTest.kt @@ -171,6 +171,7 @@ class ContentForSeeAlsoTest : BaseAbstractTest() { } } + @OnlyDescriptors("No link for `abc` in K1") @Test fun `undocumented seealso with reference to parameter for class`() { testInline( @@ -201,7 +202,7 @@ class ContentForSeeAlsoTest : BaseAbstractTest() { header(4) { +"See also" } table { group { - +"abc" + +"abc" // link { +"abc" } } } } @@ -751,6 +752,7 @@ class ContentForSeeAlsoTest : BaseAbstractTest() { } } + @OnlyDescriptorsMPP @Test fun `multiplatform class with seealso in few platforms`() { testInline( diff --git a/plugins/base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt b/plugins/base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt index 0cf94c18..e57e6a8c 100644 --- a/plugins/base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt +++ b/plugins/base/src/test/kotlin/content/signatures/ConstructorsSignaturesTest.kt @@ -5,6 +5,7 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.pages.BasicTabbedContentType import org.jetbrains.dokka.pages.ContentPage import org.junit.jupiter.api.Test +import utils.OnlyDescriptors class ConstructorsSignaturesTest : BaseAbstractTest() { private val testConfiguration = dokkaConfiguration { @@ -157,6 +158,7 @@ class ConstructorsSignaturesTest : BaseAbstractTest() { } } + @OnlyDescriptors("Order of constructors is different in K2") @Test fun `class with a parameterless secondary constructor`() { testInline( @@ -227,6 +229,7 @@ class ConstructorsSignaturesTest : BaseAbstractTest() { } + @OnlyDescriptors("Order of constructors is different in K2") @Test fun `class with a few documented constructors`() { testInline( diff --git a/plugins/base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt b/plugins/base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt index efab9aba..6423655a 100644 --- a/plugins/base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt +++ b/plugins/base/src/test/kotlin/filter/JavaVisibilityFilterTest.kt @@ -11,8 +11,10 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import testApi.testRunner.dokkaConfiguration +import utils.JavaCode import kotlin.test.assertEquals +@JavaCode class JavaVisibilityFilterTest : BaseAbstractTest() { @Test diff --git a/plugins/base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt b/plugins/base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt index f4ef85b9..af77d61c 100644 --- a/plugins/base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt +++ b/plugins/base/src/test/kotlin/filter/KotlinArrayDocumentableReplacerTest.kt @@ -8,6 +8,7 @@ import org.jetbrains.dokka.model.GenericTypeConstructor import org.jetbrains.dokka.model.Invariance import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import utils.OnlyDescriptors class KotlinArrayDocumentableReplacerTest : BaseAbstractTest() { private val configuration = dokkaConfiguration { @@ -157,6 +158,8 @@ class KotlinArrayDocumentableReplacerTest : BaseAbstractTest() { } } } + + @OnlyDescriptors("Fix module.contentScope in new Standalone API") // TODO fix module.contentScope [getKtModuleForKtElement] @Test fun `no jvm source set`() { val configurationWithNoJVM = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/linkableContent/LinkableContentTest.kt b/plugins/base/src/test/kotlin/linkableContent/LinkableContentTest.kt index be75e01f..ccf62641 100644 --- a/plugins/base/src/test/kotlin/linkableContent/LinkableContentTest.kt +++ b/plugins/base/src/test/kotlin/linkableContent/LinkableContentTest.kt @@ -10,6 +10,8 @@ import org.jetbrains.dokka.pages.* import org.jsoup.Jsoup import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import utils.OnlyDescriptors +import utils.OnlyDescriptorsMPP import utils.TestOutputWriterPlugin import utils.assertNotNull import java.net.URL @@ -18,6 +20,7 @@ import kotlin.test.assertEquals class LinkableContentTest : BaseAbstractTest() { + @OnlyDescriptorsMPP @Test fun `Include module and package documentation`() { @@ -143,6 +146,7 @@ class LinkableContentTest : BaseAbstractTest() { } } + @OnlyDescriptorsMPP @Test fun `Samples multiplatform documentation`() { diff --git a/plugins/base/src/test/kotlin/linking/EnumValuesLinkingTest.kt b/plugins/base/src/test/kotlin/linking/EnumValuesLinkingTest.kt index 14875832..9ba428d6 100644 --- a/plugins/base/src/test/kotlin/linking/EnumValuesLinkingTest.kt +++ b/plugins/base/src/test/kotlin/linking/EnumValuesLinkingTest.kt @@ -11,11 +11,13 @@ import org.jsoup.Jsoup import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test +import utils.OnlyDescriptors import utils.TestOutputWriterPlugin import java.nio.file.Paths class EnumValuesLinkingTest : BaseAbstractTest() { + @OnlyDescriptors // TODO @Test fun `check if enum values are correctly linked`() { val writerPlugin = TestOutputWriterPlugin() diff --git a/plugins/base/src/test/kotlin/markdown/LinkTest.kt b/plugins/base/src/test/kotlin/markdown/LinkTest.kt index 526ff0eb..8ee5a20d 100644 --- a/plugins/base/src/test/kotlin/markdown/LinkTest.kt +++ b/plugins/base/src/test/kotlin/markdown/LinkTest.kt @@ -11,8 +11,10 @@ import org.jetbrains.dokka.pages.MemberPageNode import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test +import utils.UsingJDK class LinkTest : BaseAbstractTest() { + @UsingJDK @Test fun linkToClassLoader() { val configuration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/markdown/ParserTest.kt b/plugins/base/src/test/kotlin/markdown/ParserTest.kt index 41b086ee..e10a2260 100644 --- a/plugins/base/src/test/kotlin/markdown/ParserTest.kt +++ b/plugins/base/src/test/kotlin/markdown/ParserTest.kt @@ -1473,6 +1473,7 @@ class ParserTest : KDocTest() { executeTest(kdoc, expectedDocumentationNode) } + @Test fun `exception thrown by empty header should point to location of a file`() { val kdoc = """ @@ -1481,9 +1482,10 @@ class ParserTest : KDocTest() { val expectedDocumentationNode = DocumentationNode(emptyList()) val exception = runCatching { executeTest(kdoc, expectedDocumentationNode) }.exceptionOrNull() - assertEquals( - "Wrong AST Tree. Header does not contain expected content in Test.kt/example.Test, element starts from offset 0 and ends 3: ###", - exception?.message + val expectedMessage = "Wrong AST Tree. Header does not contain expected content in Test.kt/example.Test, element starts from offset 0 and ends 3: ###" + assert( + exception?.message == expectedMessage + || /* for K2 */ exception?.cause?.cause?.message == expectedMessage ) } diff --git a/plugins/base/src/test/kotlin/model/ClassesTest.kt b/plugins/base/src/test/kotlin/model/ClassesTest.kt index 807ede78..e358945e 100644 --- a/plugins/base/src/test/kotlin/model/ClassesTest.kt +++ b/plugins/base/src/test/kotlin/model/ClassesTest.kt @@ -7,10 +7,7 @@ import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.KotlinModifier.* import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test -import utils.AbstractModelTest -import utils.assertNotNull -import utils.name -import utils.supers +import utils.* class ClassesTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "classes") { @@ -411,6 +408,7 @@ class ClassesTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "class } } + @OnlyDescriptors("Bug in descriptors, DRI of entry should have [EnumEntryDRIExtra]") @Test fun javaAnnotationClass() { inlineModelTest( @@ -523,6 +521,7 @@ class ClassesTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "class } } + @UsingJDK @Test fun doublyTypealiasedException() { inlineModelTest( diff --git a/plugins/base/src/test/kotlin/model/ExtensionsTest.kt b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt index e28b442f..b54f8deb 100644 --- a/plugins/base/src/test/kotlin/model/ExtensionsTest.kt +++ b/plugins/base/src/test/kotlin/model/ExtensionsTest.kt @@ -8,6 +8,7 @@ import org.jetbrains.dokka.model.Documentable import org.jetbrains.dokka.model.properties.WithExtraProperties import org.junit.jupiter.api.Test import utils.AbstractModelTest +import utils.UsingJDK class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "classes") { private fun <T : WithExtraProperties<R>, R : Documentable> T.checkExtension(name: String = "extension") = @@ -66,6 +67,7 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl } } + @UsingJDK @Test fun `should be extension for external classes`() { inlineModelTest( @@ -85,7 +87,7 @@ class ExtensionsTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "cl } } } - + @Test fun `should be extension for typealias`() { inlineModelTest( diff --git a/plugins/base/src/test/kotlin/model/FunctionsTest.kt b/plugins/base/src/test/kotlin/model/FunctionsTest.kt index fa65a477..3410f9ef 100644 --- a/plugins/base/src/test/kotlin/model/FunctionsTest.kt +++ b/plugins/base/src/test/kotlin/model/FunctionsTest.kt @@ -3,10 +3,7 @@ package model import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.* import org.junit.jupiter.api.Test -import utils.AbstractModelTest -import utils.assertNotNull -import utils.comments -import utils.name +import utils.* class FunctionTest : AbstractModelTest("/src/main/kotlin/function/Test.kt", "function") { @@ -213,6 +210,7 @@ class FunctionTest : AbstractModelTest("/src/main/kotlin/function/Test.kt", "fun } } + @OnlyDescriptors("Bug in descriptors, DRI of entry should have [EnumEntryDRIExtra]") @Test fun functionWithAnnotatedParam() { inlineModelTest( @@ -274,6 +272,7 @@ class FunctionTest : AbstractModelTest("/src/main/kotlin/function/Test.kt", "fun } } + @OnlyDescriptors("Bug in descriptors, DRI of entry should have [EnumEntryDRIExtra]") @Test fun annotatedFunctionWithAnnotationParameters() { inlineModelTest( diff --git a/plugins/base/src/test/kotlin/model/JavaTest.kt b/plugins/base/src/test/kotlin/model/JavaTest.kt index f57c3c8c..a0605a5e 100644 --- a/plugins/base/src/test/kotlin/model/JavaTest.kt +++ b/plugins/base/src/test/kotlin/model/JavaTest.kt @@ -9,12 +9,11 @@ import org.jetbrains.dokka.model.doc.Param import org.jetbrains.dokka.model.doc.Text import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -import utils.AbstractModelTest +import utils.* import utils.assertContains -import utils.assertNotNull -import utils.name import kotlin.test.assertEquals +@JavaCode class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { val configuration = dokkaConfiguration { sourceSets { diff --git a/plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt b/plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt index 5fe17fc8..6f08c89d 100644 --- a/plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt +++ b/plugins/base/src/test/kotlin/model/MultiLanguageInheritanceTest.kt @@ -9,9 +9,11 @@ import org.jetbrains.dokka.model.withDescendants import org.jetbrains.dokka.utilities.firstIsInstanceOrNull import org.junit.jupiter.api.Test import translators.documentationOf +import utils.JavaCode import utils.docs import kotlin.test.assertEquals +@JavaCode class MultiLanguageInheritanceTest : BaseAbstractTest() { val configuration = dokkaConfiguration { suppressObviousFunctions = false diff --git a/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt b/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt index d6564343..35997681 100644 --- a/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt +++ b/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsForParametersTest.kt @@ -6,9 +6,11 @@ import org.jetbrains.dokka.model.* import org.jetbrains.dokka.utilities.cast import org.junit.jupiter.api.Test import utils.AbstractModelTest +import utils.JavaCode import kotlin.test.assertEquals import kotlin.test.assertTrue +@JavaCode class JavaAnnotationsForParametersTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { @Test diff --git a/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt b/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt index e704bf71..0abf504e 100644 --- a/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt +++ b/plugins/base/src/test/kotlin/model/annotations/JavaAnnotationsTest.kt @@ -4,11 +4,13 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.model.* import org.junit.jupiter.api.Test import translators.findClasslike +import utils.JavaCode import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue +@JavaCode class JavaAnnotationsTest : BaseAbstractTest() { val configuration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt b/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt index f4615216..4b376c73 100644 --- a/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt +++ b/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt @@ -10,10 +10,12 @@ import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.utilities.firstIsInstanceOrNull import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import utils.JavaCode import utils.docs import utils.text import kotlin.test.assertNotNull +@JavaCode class JavadocParserTest : BaseAbstractTest() { private val configuration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt b/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt index 5e2560bf..fb2c53cd 100644 --- a/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt +++ b/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt @@ -3,6 +3,7 @@ package renderers.html import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.junit.jupiter.api.Test import utils.TestOutputWriterPlugin +import utils.UsingJDK import utils.navigationHtml import utils.selectNavigationGrid import kotlin.test.assertEquals @@ -182,6 +183,7 @@ class NavigationIconTest : BaseAbstractTest() { ) } + @UsingJDK @Test fun `should add icon styles to kotlin exception class navigation item`() { assertNavigationIcon( diff --git a/plugins/base/src/test/kotlin/signatures/DivergentSignatureTest.kt b/plugins/base/src/test/kotlin/signatures/DivergentSignatureTest.kt index ca287216..af10cbee 100644 --- a/plugins/base/src/test/kotlin/signatures/DivergentSignatureTest.kt +++ b/plugins/base/src/test/kotlin/signatures/DivergentSignatureTest.kt @@ -1,8 +1,11 @@ package signatures import org.junit.jupiter.api.Test +import utils.OnlyDescriptors +import utils.OnlyDescriptorsMPP import utils.TestOutputWriterPlugin +@OnlyDescriptorsMPP class DivergentSignatureTest : AbstractRenderingTest() { @Test diff --git a/plugins/base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt b/plugins/base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt index c9787b67..588b3d50 100644 --- a/plugins/base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt +++ b/plugins/base/src/test/kotlin/signatures/FunctionalTypeConstructorsSignatureTest.kt @@ -5,10 +5,7 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.jdk import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import utils.A -import utils.Span -import utils.TestOutputWriterPlugin -import utils.match +import utils.* class FunctionalTypeConstructorsSignatureTest : BaseAbstractTest() { private val configuration = dokkaConfiguration { @@ -254,6 +251,7 @@ class FunctionalTypeConstructorsSignatureTest : BaseAbstractTest() { } } + @JavaCode @Test fun `java with java function`() { val source = """ @@ -280,6 +278,7 @@ class FunctionalTypeConstructorsSignatureTest : BaseAbstractTest() { } } + @JavaCode @Test fun `java with kotlin function`() { val source = """ diff --git a/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt b/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt index 0767b2df..4cd9a94d 100644 --- a/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt +++ b/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt @@ -3,12 +3,10 @@ package signatures import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.junit.jupiter.api.Test -import utils.A -import utils.Span -import utils.TestOutputWriterPlugin -import utils.match +import utils.* import kotlin.test.assertEquals +@JavaCode class InheritedAccessorsSignatureTest : BaseAbstractTest() { private val configuration = dokkaConfiguration { @@ -24,6 +22,7 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { } } + @OnlyDescriptors("'var' expected but found: 'open var'") @Test fun `should collapse accessor functions inherited from java into the property`() { val writerPlugin = TestOutputWriterPlugin() @@ -76,6 +75,7 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { } } + @OnlyDescriptors("'var' expected but found: 'open var'") @Test fun `should render as val if inherited java property has no setter`() { val writerPlugin = TestOutputWriterPlugin() @@ -178,6 +178,7 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { } } + @OnlyDescriptors("'var' expected but found: 'open var'") @Test fun `should keep inherited java accessor lookalikes if underlying function is public`() { val writerPlugin = TestOutputWriterPlugin() @@ -228,6 +229,7 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { } } + @JavaCode @Test fun `should keep kotlin property with no accessors when java inherits kotlin a var`() { val writerPlugin = TestOutputWriterPlugin() @@ -265,6 +267,7 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { } } + @JavaCode @Test fun `kotlin property with compute get and set`() { val writerPlugin = TestOutputWriterPlugin() @@ -326,6 +329,7 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { } } + @OnlyDescriptors("'var' expected but found: 'open var'") @Test fun `inherited property should inherit getter's visibility`() { val configWithProtectedVisibility = dokkaConfiguration { @@ -399,6 +403,7 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { } } + @OnlyDescriptors("'var' expected but found: 'open var'") @Test fun `should resolve protected java property as protected`() { val configWithProtectedVisibility = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/signatures/SignatureTest.kt b/plugins/base/src/test/kotlin/signatures/SignatureTest.kt index 38ae2be3..00d98102 100644 --- a/plugins/base/src/test/kotlin/signatures/SignatureTest.kt +++ b/plugins/base/src/test/kotlin/signatures/SignatureTest.kt @@ -549,6 +549,7 @@ class SignatureTest : BaseAbstractTest() { } } } + @OnlyDescriptorsMPP @Test fun `actual typealias should have generic parameters and fully qualified name of the expansion type`() { val writerPlugin = TestOutputWriterPlugin() @@ -583,6 +584,7 @@ class SignatureTest : BaseAbstractTest() { } } + @OnlyDescriptorsMPP @Test fun `type with an actual typealias`() { val writerPlugin = TestOutputWriterPlugin() @@ -763,6 +765,7 @@ class SignatureTest : BaseAbstractTest() { } } + @OnlyDescriptors("Order of constructors is different in K2") @Test fun `generic constructor params`() { val writerPlugin = TestOutputWriterPlugin() @@ -977,6 +980,7 @@ class SignatureTest : BaseAbstractTest() { } } + @OnlyDescriptors("'var' expected but found: 'open var'") @Test fun `java property without accessors should be var`() { val writerPlugin = TestOutputWriterPlugin() diff --git a/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt b/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt index a189894c..14c2752a 100644 --- a/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt +++ b/plugins/base/src/test/kotlin/superFields/DescriptorSuperPropertiesTest.kt @@ -7,10 +7,12 @@ import org.jetbrains.dokka.model.InheritedMember import org.jetbrains.dokka.model.IsVar import org.jetbrains.dokka.model.KotlinVisibility import org.junit.jupiter.api.Test +import utils.JavaCode import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +@JavaCode class DescriptorSuperPropertiesTest : BaseAbstractTest() { private val commonTestConfiguration = dokkaConfiguration { @@ -173,6 +175,7 @@ class DescriptorSuperPropertiesTest : BaseAbstractTest() { } } + // incorrect test https://github.com/Kotlin/dokka/issues/3128 @Test fun `kotlin inheriting java should not append anything since field is public api`() { val configuration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt b/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt index 9c1265a6..5c7124cd 100644 --- a/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt +++ b/plugins/base/src/test/kotlin/superFields/PsiSuperFieldsTest.kt @@ -9,9 +9,11 @@ import org.jetbrains.dokka.model.isJvmField import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test +import utils.JavaCode import kotlin.test.assertEquals +@JavaCode class PsiSuperFieldsTest : BaseAbstractTest() { private val commonTestConfiguration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt b/plugins/base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt index 9cde40a5..826df64e 100644 --- a/plugins/base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt +++ b/plugins/base/src/test/kotlin/transformers/InheritedEntriesDocumentableFilterTransfromerTest.kt @@ -3,6 +3,7 @@ package transformers import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.model.DEnum import org.junit.jupiter.api.Test +import utils.OnlyDescriptors import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -133,6 +134,7 @@ class InheritedEntriesDocumentableFilterTransformerTest : BaseAbstractTest() { } } + @OnlyDescriptors("Entry does not have `name` and `ordinal`") // TODO @Test fun `should work with enum entries when not suppressing`(){ testInline( diff --git a/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt b/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt index 39d725bb..e6cc393d 100644 --- a/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt +++ b/plugins/base/src/test/kotlin/transformers/MergeImplicitExpectActualDeclarationsTest.kt @@ -9,6 +9,7 @@ import org.jetbrains.dokka.model.dfs import org.jetbrains.dokka.model.firstChildOfType import org.jetbrains.dokka.pages.* import org.junit.jupiter.api.Test +import utils.OnlyDescriptors import utils.assertNotNull import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -269,6 +270,7 @@ class MergeImplicitExpectActualDeclarationsTest : BaseAbstractTest() { fun PageNode.childrenRec(): List<PageNode> = listOf(this) + children.flatMap { it.childrenRec() } + @OnlyDescriptors("Enum entry [SMTH] does not have functions") // TODO @Test fun `should merge enum entries`() { testInline( diff --git a/plugins/base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt b/plugins/base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt index 85db2d27..134e7f59 100644 --- a/plugins/base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt +++ b/plugins/base/src/test/kotlin/transformers/ModuleAndPackageDocumentationTransformerFunctionalTest.kt @@ -5,11 +5,13 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import transformers.AbstractContextModuleAndPackageDocumentationReaderTest.Companion.texts +import utils.OnlyDescriptorsMPP import java.nio.file.Path import kotlin.test.assertEquals class ModuleAndPackageDocumentationTransformerFunctionalTest : BaseAbstractTest() { + @OnlyDescriptorsMPP @Test fun `multiplatform project`(@TempDir tempDir: Path) { val include = tempDir.resolve("include.md").toFile() diff --git a/plugins/base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt b/plugins/base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt index 3618c8fb..ffb5c9c8 100644 --- a/plugins/base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt +++ b/plugins/base/src/test/kotlin/transformers/ObviousAndInheritedFunctionsDocumentableFilterTest.kt @@ -5,6 +5,7 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import testApi.testRunner.dokkaConfiguration +import utils.JavaCode import kotlin.test.assertEquals class ObviousAndInheritedFunctionsDocumentableFilterTest : BaseAbstractTest() { @@ -196,6 +197,7 @@ class ObviousAndInheritedFunctionsDocumentableFilterTest : BaseAbstractTest() { @ParameterizedTest @MethodSource(value = ["nonSuppressingObviousConfiguration", "nonSuppressingInheritedConfiguration"]) + @JavaCode fun `should not suppress toString, equals and hashcode if custom config is provided in Java`(nonSuppressingConfiguration: DokkaConfigurationImpl) { testInline( """ diff --git a/plugins/base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt b/plugins/base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt index 469c1a1e..532c9e81 100644 --- a/plugins/base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt +++ b/plugins/base/src/test/kotlin/transformers/SourceLinkTransformerTest.kt @@ -6,6 +6,7 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jsoup.nodes.Element import org.junit.jupiter.api.Test import signatures.renderedContent +import utils.OnlyDescriptorsMPP import utils.TestOutputWriterPlugin import java.net.URL import kotlin.test.assertEquals @@ -66,6 +67,7 @@ class SourceLinkTransformerTest : BaseAbstractTest() { } } + @OnlyDescriptorsMPP @Test fun `source link should be for actual typealias`() { val mppConfiguration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/transformers/isExceptionTest.kt b/plugins/base/src/test/kotlin/transformers/isExceptionTest.kt index ba00cf15..dd888ad6 100644 --- a/plugins/base/src/test/kotlin/transformers/isExceptionTest.kt +++ b/plugins/base/src/test/kotlin/transformers/isExceptionTest.kt @@ -5,8 +5,11 @@ import org.jetbrains.dokka.model.DClass import org.jetbrains.dokka.model.DTypeAlias import org.junit.jupiter.api.Test import utils.AbstractModelTest +import utils.JavaCode +import utils.UsingJDK class IsExceptionKotlinTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "classes") { + @UsingJDK @Test fun `isException should work for kotlin exception`(){ inlineModelTest( @@ -20,6 +23,7 @@ class IsExceptionKotlinTest : AbstractModelTest("/src/main/kotlin/classes/Test.k } } + @UsingJDK @Test fun `isException should work for java exceptions`(){ inlineModelTest( @@ -33,6 +37,7 @@ class IsExceptionKotlinTest : AbstractModelTest("/src/main/kotlin/classes/Test.k } } + @UsingJDK @Test fun `isException should work for RuntimeException`(){ inlineModelTest( @@ -46,6 +51,7 @@ class IsExceptionKotlinTest : AbstractModelTest("/src/main/kotlin/classes/Test.k } } + @UsingJDK @Test fun `isException should work if exception is typealiased`(){ inlineModelTest( @@ -59,6 +65,7 @@ class IsExceptionKotlinTest : AbstractModelTest("/src/main/kotlin/classes/Test.k } } + @UsingJDK @Test fun `isException should work if exception is extending a typaliased class`(){ inlineModelTest( @@ -100,6 +107,7 @@ class IsExceptionKotlinTest : AbstractModelTest("/src/main/kotlin/classes/Test.k } } +@JavaCode class IsExceptionJavaTest: AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { @Test fun `isException should work for java exceptions`(){ diff --git a/plugins/base/src/test/kotlin/translators/Bug1341.kt b/plugins/base/src/test/kotlin/translators/Bug1341.kt index a8c9e342..3e1b403d 100644 --- a/plugins/base/src/test/kotlin/translators/Bug1341.kt +++ b/plugins/base/src/test/kotlin/translators/Bug1341.kt @@ -4,8 +4,10 @@ import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest import org.jetbrains.dokka.links.DRI import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import utils.JavaCode class Bug1341 : BaseAbstractTest() { + @JavaCode @Test fun `reproduce bug #1341`() { val configuration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt b/plugins/base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt index 57165fd1..4ae8f7ac 100644 --- a/plugins/base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt +++ b/plugins/base/src/test/kotlin/translators/DefaultDescriptorToDocumentableTranslatorTest.kt @@ -10,6 +10,7 @@ import org.jetbrains.dokka.model.doc.* import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import utils.OnlyDescriptors import utils.text import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -980,6 +981,7 @@ val soapXml = node("soap-env:Envelope", soapAttrs, } } + @OnlyDescriptors("Fix kdoc link") // TODO @Test fun `should have documentation for synthetic Enum valueOf functions`() { testInline( diff --git a/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt b/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt index a763cbd2..b3f83d79 100644 --- a/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt +++ b/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt @@ -12,7 +12,9 @@ import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement import org.jetbrains.dokka.DokkaConfiguration.Visibility import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test +import utils.JavaCode +@JavaCode class DefaultPsiToDocumentableTranslatorTest : BaseAbstractTest() { val configuration = dokkaConfiguration { sourceSets { diff --git a/plugins/base/src/test/kotlin/translators/ExternalDocumentablesTest.kt b/plugins/base/src/test/kotlin/translators/ExternalDocumentablesTest.kt index a9c865b4..c4087b20 100644 --- a/plugins/base/src/test/kotlin/translators/ExternalDocumentablesTest.kt +++ b/plugins/base/src/test/kotlin/translators/ExternalDocumentablesTest.kt @@ -10,8 +10,11 @@ import org.jetbrains.dokka.analysis.kotlin.internal.ExternalDocumentablesProvide import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import utils.OnlyDescriptors +import utils.UsingJDK class ExternalDocumentablesTest : BaseAbstractTest() { + @UsingJDK @Test fun `external documentable from java stdlib`() { val configuration = dokkaConfiguration { @@ -54,6 +57,10 @@ class ExternalDocumentablesTest : BaseAbstractTest() { } } + + // typealias CompletionHandler = (cause: Throwable?) -> Unit + // FunctionalTypeConstructor(dri=kotlinx.coroutines/CompletionHandler///PointingToDeclaration/, projections=[], isExtensionFunction=false, isSuspendable=false, presentableName=null, extra=PropertyContainer(map={})) + @OnlyDescriptors(reason = "FunctionType has not parameters") // TODO @Test fun `external documentable from dependency`() { val coroutinesPath = diff --git a/plugins/base/src/test/kotlin/translators/JavadocInheritDocsTest.kt b/plugins/base/src/test/kotlin/translators/JavadocInheritDocsTest.kt index 7fc6b7fa..a357491f 100644 --- a/plugins/base/src/test/kotlin/translators/JavadocInheritDocsTest.kt +++ b/plugins/base/src/test/kotlin/translators/JavadocInheritDocsTest.kt @@ -8,7 +8,9 @@ import org.jetbrains.dokka.model.doc.Text import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import utils.JavaCode +@JavaCode class JavadocInheritDocsTest : BaseAbstractTest() { val configuration = dokkaConfiguration { sourceSets { @@ -211,6 +213,7 @@ class JavadocInheritDocsTest : BaseAbstractTest() { } + @JavaCode @Test fun `work with multiple supertypes`() { testInline( diff --git a/plugins/base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt b/plugins/base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt index ba0d95d5..1e3d784a 100644 --- a/plugins/base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt +++ b/plugins/base/src/test/kotlin/translators/JavadocInheritedDocTagsTest.kt @@ -7,9 +7,11 @@ import org.jetbrains.dokka.model.DModule import org.jetbrains.dokka.model.doc.* import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import utils.JavaCode import org.jetbrains.dokka.model.doc.Deprecated as DokkaDeprecatedTag import org.jetbrains.dokka.model.doc.Throws as DokkaThrowsTag +@JavaCode class JavadocInheritedDocTagsTest : BaseAbstractTest() { @Suppress("DEPRECATION") // for includeNonPublic private val configuration = dokkaConfiguration { diff --git a/plugins/base/src/test/kotlin/translators/JavadocParserTest.kt b/plugins/base/src/test/kotlin/translators/JavadocParserTest.kt index 2c1173c0..8bc307df 100644 --- a/plugins/base/src/test/kotlin/translators/JavadocParserTest.kt +++ b/plugins/base/src/test/kotlin/translators/JavadocParserTest.kt @@ -9,8 +9,10 @@ import org.jetbrains.dokka.model.firstChildOfType import org.jetbrains.dokka.model.firstMemberOfType import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import utils.JavaCode import utils.text +@JavaCode class JavadocParserTest : BaseAbstractTest() { private fun performJavadocTest(testOperation: (DModule) -> Unit) { diff --git a/plugins/base/src/test/kotlin/utils/TagsAnnotations.kt b/plugins/base/src/test/kotlin/utils/TagsAnnotations.kt new file mode 100644 index 00000000..181d1f1d --- /dev/null +++ b/plugins/base/src/test/kotlin/utils/TagsAnnotations.kt @@ -0,0 +1,71 @@ +package utils + +import org.junit.jupiter.api.Tag + + +/** + * Run a test only for descriptors, not symbols. + * + * In theory, these tests can be fixed + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +@Tag("onlyDescriptors") +annotation class OnlyDescriptors(val reason: String = "") + +/** + * Run a test only for descriptors, not symbols. + * + * These tests cannot be fixed until Analysis API does not support MPP + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +@Tag("onlyDescriptorsMPP") +annotation class OnlyDescriptorsMPP(val reason: String = "") + + +/** + * For test containing .java code + * These tests are disabled in K2 due to Standlone prototype. https://github.com/Kotlin/dokka/issues/3114 + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +@Tag("javaCode") +annotation class JavaCode + +/** + * For Kotlin test using JDK + * These tests are disabled in K2 due to Standlone prototype. https://github.com/Kotlin/dokka/issues/3114 + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +@Tag("usingJDK") +annotation class UsingJDK
\ No newline at end of file diff --git a/plugins/mathjax/build.gradle.kts b/plugins/mathjax/build.gradle.kts index 4c572450..1d10d838 100644 --- a/plugins/mathjax/build.gradle.kts +++ b/plugins/mathjax/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.registerDokkaArtifactPublication plugins { id("org.jetbrains.conventions.kotlin-jvm") id("org.jetbrains.conventions.maven-publish") + id("org.jetbrains.conventions.base-unit-test") } dependencies { @@ -13,12 +14,17 @@ dependencies { implementation(kotlin("reflect")) testImplementation(libs.jsoup) - testImplementation(projects.plugins.base.baseTestUtils) testImplementation(projects.core.contentMatcherTestUtils) testImplementation(kotlin("test-junit")) testImplementation(projects.core.testApi) testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter) + + symbolsTestConfiguration(project(path = ":subprojects:analysis-kotlin-symbols", configuration = "shadow")) + descriptorsTestConfiguration(project(path = ":subprojects:analysis-kotlin-descriptors", configuration = "shadow")) + testImplementation(projects.plugins.base.baseTestUtils) { + exclude(module = "analysis-kotlin-descriptors") + } } registerDokkaArtifactPublication("mathjaxPlugin") { diff --git a/settings.gradle.kts b/settings.gradle.kts index 2320ca69..d3e2cba2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,8 +67,6 @@ include( ":subprojects:analysis-kotlin-descriptors:compiler", ":subprojects:analysis-kotlin-descriptors:ide", ":subprojects:analysis-kotlin-symbols", - ":subprojects:analysis-kotlin-symbols:compiler", - ":subprojects:analysis-kotlin-symbols:ide", ":subprojects:analysis-markdown-jb", ":runners:gradle-plugin", diff --git a/subprojects/analysis-java-psi/api/analysis-java-psi.api b/subprojects/analysis-java-psi/api/analysis-java-psi.api index 404249f8..6b4d444f 100644 --- a/subprojects/analysis-java-psi/api/analysis-java-psi.api +++ b/subprojects/analysis-java-psi/api/analysis-java-psi.api @@ -146,3 +146,7 @@ public final class org/jetbrains/dokka/analysis/java/util/PsiDocumentableSource public final fun getPsi ()Lcom/intellij/psi/PsiNamedElement; } +public final class org/jetbrains/dokka/analysis/java/util/PsiUtilKt { + public static final fun from (Lorg/jetbrains/dokka/links/DRI$Companion;Lcom/intellij/psi/PsiElement;)Lorg/jetbrains/dokka/links/DRI; +} + diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt index ed58eb56..eb2058d8 100644 --- a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt +++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt @@ -12,7 +12,8 @@ import org.jetbrains.dokka.utilities.firstIsInstanceOrNull internal val PsiElement.parentsWithSelf: Sequence<PsiElement> get() = generateSequence(this) { if (it is PsiFile) null else it.parent } -internal fun DRI.Companion.from(psi: PsiElement) = psi.parentsWithSelf.run { +@InternalDokkaApi +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 } diff --git a/subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api b/subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api index e69de29b..4bddfcf1 100644 --- a/subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api +++ b/subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api @@ -0,0 +1,19 @@ +public final class org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V +} + +public class org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider : org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun close ()V + public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; + public fun getSample (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/lang/String;)Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider$SampleSnippet; + protected fun processBody (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; + protected fun processImports (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; +} + +public final class org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProviderFactory : org/jetbrains/dokka/analysis/kotlin/internal/SampleProviderFactory { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun build ()Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider; + public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; +} + diff --git a/subprojects/analysis-kotlin-symbols/build.gradle.kts b/subprojects/analysis-kotlin-symbols/build.gradle.kts index c000df58..9fddcde1 100644 --- a/subprojects/analysis-kotlin-symbols/build.gradle.kts +++ b/subprojects/analysis-kotlin-symbols/build.gradle.kts @@ -8,9 +8,74 @@ plugins { } dependencies { - implementation(projects.subprojects.analysisKotlinApi) - implementation(projects.subprojects.analysisKotlinSymbols.compiler) - implementation(projects.subprojects.analysisKotlinSymbols.ide) + compileOnly(projects.core) + compileOnly(projects.subprojects.analysisKotlinApi) + + implementation(projects.subprojects.analysisMarkdownJb) + implementation(projects.subprojects.analysisJavaPsi) + + + // ----------- IDE dependencies ---------------------------------------------------------------------------- + + listOf( + libs.intellij.platform.util.rt, + libs.intellij.platform.util.api, + libs.intellij.java.psi.api, + libs.intellij.java.psi.impl + ).forEach { + runtimeOnly(it) { isTransitive = false } + } + + implementation(libs.intellij.java.psi.api) { isTransitive = false } + + + // TODO move to toml + listOf( + "com.jetbrains.intellij.platform:util-class-loader", + "com.jetbrains.intellij.platform:util-text-matching", + "com.jetbrains.intellij.platform:util-base", + "com.jetbrains.intellij.platform:util-xml-dom", + "com.jetbrains.intellij.platform:core-impl", + "com.jetbrains.intellij.platform:extensions", + ).forEach { + runtimeOnly("$it:213.7172.25") { isTransitive = false } + } + + implementation("com.jetbrains.intellij.platform:core:213.7172.25") { + isTransitive = false + } // for Standalone prototype + + // ----------- Analysis dependencies ---------------------------------------------------------------------------- + + listOf( + libs.kotlin.high.level.api.api, + libs.kotlin.analysis.api.standalone, + libs.kotlin.high.level.api.impl // for Standalone prototype + ).forEach { + implementation(it) { + isTransitive = false // see KTIJ-19820 + } + } + listOf( + libs.kotlin.high.level.api.impl, + libs.kotlin.high.level.api.fir, + libs.kotlin.high.level.api.fe10, + libs.kotlin.low.level.api.fir, + libs.kotlin.analysis.project.structure, + libs.kotlin.analysis.api.providers, + libs.kotlin.symbol.light.classes + ).forEach { + runtimeOnly(it) { + isTransitive = false // see KTIJ-19820 + } + } + runtimeOnly(libs.kotlinx.collections.immutable) + implementation(libs.kotlin.compiler.k2) { + isTransitive = false + } + + // TODO [beresnev] get rid of it + compileOnly(libs.kotlinx.coroutines.core) } tasks { diff --git a/subprojects/analysis-kotlin-symbols/compiler/api/compiler.api b/subprojects/analysis-kotlin-symbols/compiler/api/compiler.api deleted file mode 100644 index 39870f57..00000000 --- a/subprojects/analysis-kotlin-symbols/compiler/api/compiler.api +++ /dev/null @@ -1,4 +0,0 @@ -public final class org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { - public fun <init> ()V -} - diff --git a/subprojects/analysis-kotlin-symbols/compiler/build.gradle.kts b/subprojects/analysis-kotlin-symbols/compiler/build.gradle.kts deleted file mode 100644 index 876d87ca..00000000 --- a/subprojects/analysis-kotlin-symbols/compiler/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id("org.jetbrains.conventions.kotlin-jvm") -} - -dependencies { - compileOnly(projects.core) - compileOnly(projects.subprojects.analysisKotlinApi) - - // TODO - - // TODO [beresnev] get rid of it - compileOnly(libs.kotlinx.coroutines.core) -} diff --git a/subprojects/analysis-kotlin-symbols/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin.kt b/subprojects/analysis-kotlin-symbols/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin.kt deleted file mode 100644 index 4a39e0d8..00000000 --- a/subprojects/analysis-kotlin-symbols/compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/compiler/CompilerSymbolsAnalysisPlugin.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.jetbrains.dokka.analysis.kotlin.symbols.compiler - -import org.jetbrains.dokka.plugability.DokkaPlugin -import org.jetbrains.dokka.plugability.DokkaPluginApiPreview -import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement - -class CompilerSymbolsAnalysisPlugin : DokkaPlugin() { - - @OptIn(DokkaPluginApiPreview::class) - override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement -} diff --git a/subprojects/analysis-kotlin-symbols/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-kotlin-symbols/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin deleted file mode 100644 index 47163f6e..00000000 --- a/subprojects/analysis-kotlin-symbols/compiler/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin +++ /dev/null @@ -1 +0,0 @@ -org.jetbrains.dokka.analysis.kotlin.symbols.compiler.CompilerSymbolsAnalysisPlugin diff --git a/subprojects/analysis-kotlin-symbols/ide/api/ide.api b/subprojects/analysis-kotlin-symbols/ide/api/ide.api deleted file mode 100644 index 9be46c0b..00000000 --- a/subprojects/analysis-kotlin-symbols/ide/api/ide.api +++ /dev/null @@ -1,4 +0,0 @@ -public final class org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { - public fun <init> ()V -} - diff --git a/subprojects/analysis-kotlin-symbols/ide/build.gradle.kts b/subprojects/analysis-kotlin-symbols/ide/build.gradle.kts deleted file mode 100644 index 876d87ca..00000000 --- a/subprojects/analysis-kotlin-symbols/ide/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id("org.jetbrains.conventions.kotlin-jvm") -} - -dependencies { - compileOnly(projects.core) - compileOnly(projects.subprojects.analysisKotlinApi) - - // TODO - - // TODO [beresnev] get rid of it - compileOnly(libs.kotlinx.coroutines.core) -} diff --git a/subprojects/analysis-kotlin-symbols/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin.kt b/subprojects/analysis-kotlin-symbols/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin.kt deleted file mode 100644 index 33f555cb..00000000 --- a/subprojects/analysis-kotlin-symbols/ide/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/ide/IdeSymbolsAnalysisPlugin.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.jetbrains.dokka.analysis.kotlin.symbols.ide - -import org.jetbrains.dokka.plugability.DokkaPlugin -import org.jetbrains.dokka.plugability.DokkaPluginApiPreview -import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement - -class IdeSymbolsAnalysisPlugin : DokkaPlugin() { - - @OptIn(DokkaPluginApiPreview::class) - override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement -} diff --git a/subprojects/analysis-kotlin-symbols/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-kotlin-symbols/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin deleted file mode 100644 index 59245578..00000000 --- a/subprojects/analysis-kotlin-symbols/ide/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin +++ /dev/null @@ -1 +0,0 @@ -org.jetbrains.dokka.analysis.kotlin.symbols.ide.IdeSymbolsAnalysisPlugin diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt new file mode 100644 index 00000000..c1b8651a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt @@ -0,0 +1,170 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.analysis.java.parsers.JavadocParser +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType +import org.jetbrains.kotlin.psi.psiUtil.isPropertyParameter +import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly + +internal fun KtAnalysisSession.getJavaDocDocumentationFrom( + symbol: KtSymbol, + javadocParser: JavadocParser +): DocumentationNode? { + if (symbol.origin == KtSymbolOrigin.JAVA) { + return (symbol.psi as? PsiNamedElement)?.let { + javadocParser.parseDocumentation(it) + } + } else if (symbol.origin == KtSymbolOrigin.SOURCE && symbol is KtCallableSymbol) { + // Note: javadocParser searches in overridden JAVA declarations for JAVA method, not Kotlin + symbol.getAllOverriddenSymbols().forEach { overrider -> + if (overrider.origin == KtSymbolOrigin.JAVA) + return@getJavaDocDocumentationFrom (overrider.psi as? PsiNamedElement)?.let { + javadocParser.parseDocumentation(it) + } + } + } + return null +} + +internal fun KtAnalysisSession.getKDocDocumentationFrom(symbol: KtSymbol, logger: DokkaLogger) = findKDoc(symbol)?.let { kDocContent -> + + val ktElement = symbol.psi + val kdocLocation = ktElement?.containingFile?.name?.let { + val name = when(symbol) { + is KtCallableSymbol -> symbol.callableIdIfNonLocal?.toString() + is KtClassOrObjectSymbol -> symbol.classIdIfNonLocal?.toString() + is KtNamedSymbol -> symbol.name.asString() + else -> null + }?.replace('/', '.') // replace to be compatible with K1 + + if (name != null) "$it/$name" + else it + } + + + parseFromKDocTag( + kDocTag = kDocContent.contentTag, + externalDri = { link -> resolveKDocLink(link).logIfNotResolved(link.getLinkText(), logger) }, + kdocLocation = kdocLocation + ) +} + + + + +// ----------- copy-paste from IDE ---------------------------------------------------------------------------- + +internal data class KDocContent( + val contentTag: KDocTag, + val sections: List<KDocSection> +) + +internal fun KtAnalysisSession.findKDoc(symbol: KtSymbol): KDocContent? { + // for generated function (e.g. `copy`) psi returns class, see test `data class kdocs over generated methods` + if (symbol.origin != KtSymbolOrigin.SOURCE) return null + val ktElement = symbol.psi as? KtElement + ktElement?.findKDoc()?.let { + return it + } + + if (symbol is KtCallableSymbol) { + symbol.getAllOverriddenSymbols().forEach { overrider -> + findKDoc(overrider)?.let { + return it + } + } + } + return null +} + + +internal fun KtElement.findKDoc(): KDocContent? = this.lookupOwnedKDoc() + ?: this.lookupKDocInContainer() + + + +private fun KtElement.lookupOwnedKDoc(): KDocContent? { + // KDoc for primary constructor is located inside of its class KDoc + val psiDeclaration = when (this) { + is KtPrimaryConstructor -> getContainingClassOrObject() + else -> this + } + + if (psiDeclaration is KtDeclaration) { + val kdoc = psiDeclaration.docComment + if (kdoc != null) { + if (this is KtConstructor<*>) { + // ConstructorDescriptor resolves to the same JetDeclaration + val constructorSection = kdoc.findSectionByTag(KDocKnownTag.CONSTRUCTOR) + if (constructorSection != null) { + // if annotated with @constructor tag and the caret is on constructor definition, + // then show @constructor description as the main content, and additional sections + // that contain @param tags (if any), as the most relatable ones + // practical example: val foo = Fo<caret>o("argument") -- show @constructor and @param content + val paramSections = kdoc.findSectionsContainingTag(KDocKnownTag.PARAM) + return KDocContent(constructorSection, paramSections) + } + } + return KDocContent(kdoc.getDefaultSection(), kdoc.getAllSections()) + } + } + + return null +} + +/** + * Looks for sections that have a deeply nested [tag], + * as opposed to [KDoc.findSectionByTag], which only looks among the top level + */ +private fun KDoc.findSectionsContainingTag(tag: KDocKnownTag): List<KDocSection> { + return getChildrenOfType<KDocSection>() + .filter { it.findTagByName(tag.name.toLowerCaseAsciiOnly()) != null } +} + +private fun KtElement.lookupKDocInContainer(): KDocContent? { + val subjectName = name + val containingDeclaration = + PsiTreeUtil.findFirstParent(this, true) { + it is KtDeclarationWithBody && it !is KtPrimaryConstructor + || it is KtClassOrObject + } + + val containerKDoc = containingDeclaration?.getChildOfType<KDoc>() + if (containerKDoc == null || subjectName == null) return null + val propertySection = containerKDoc.findSectionByTag(KDocKnownTag.PROPERTY, subjectName) + val paramTag = + containerKDoc.findDescendantOfType<KDocTag> { it.knownTag == KDocKnownTag.PARAM && it.getSubjectName() == subjectName } + + val primaryContent = when { + // class Foo(val <caret>s: String) + this is KtParameter && this.isPropertyParameter() -> propertySection ?: paramTag + // fun some(<caret>f: String) || class Some<<caret>T: Base> || Foo(<caret>s = "argument") + this is KtParameter || this is KtTypeParameter -> paramTag + // if this property is declared separately (outside primary constructor), but it's for some reason + // annotated as @property in class's description, instead of having its own KDoc + this is KtProperty && containingDeclaration is KtClassOrObject -> propertySection + else -> null + } + return primaryContent?.let { + // makes little sense to include any other sections, since we found + // documentation for a very specific element, like a property/param + KDocContent(it, sections = emptyList()) + } +} + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt new file mode 100644 index 00000000..ec23b0c8 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KdocMarkdownParser.kt @@ -0,0 +1,97 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser.Companion.fqDeclarationName +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.Suppress +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.psiUtil.allChildren + +internal fun parseFromKDocTag( + kDocTag: KDocTag?, + externalDri: (KDocLink) -> DRI?, + kdocLocation: String?, + parseWithChildren: Boolean = true +): DocumentationNode { + return if (kDocTag == null) { + DocumentationNode(emptyList()) + } else { + fun parseStringToDocNode(text: String, externalDRIProvider: (String) -> DRI?) = + MarkdownParser(externalDRIProvider, kdocLocation).parseStringToDocNode(text) + + fun pointedLink(tag: KDocTag): DRI? = tag.getSubjectLink()?.let(externalDri) + + val allTags = + listOf(kDocTag) + if (kDocTag.canHaveParent() && parseWithChildren) getAllKDocTags(findParent(kDocTag)) else emptyList() + DocumentationNode( + allTags.map { tag -> + val links = tag.allChildren.filterIsInstance<KDocLink>().associate { it.getLinkText() to externalDri(it) } + val externalDRIProvider = { linkText: String -> links[linkText] } + + when (tag.knownTag) { + null -> if (tag.name == null) Description(parseStringToDocNode(tag.getContent(), externalDRIProvider)) else CustomTagWrapper( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.name!! + ) + KDocKnownTag.AUTHOR -> Author(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.THROWS -> { + val dri = pointedLink(tag) + Throws( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + dri?.fqDeclarationName() ?: tag.getSubjectName().orEmpty(), + dri, + ) + } + KDocKnownTag.EXCEPTION -> { + val dri = pointedLink(tag) + Throws( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + dri?.fqDeclarationName() ?: tag.getSubjectName().orEmpty(), + dri + ) + } + KDocKnownTag.PARAM -> Param( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.getSubjectName().orEmpty() + ) + KDocKnownTag.RECEIVER -> Receiver(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.RETURN -> Return(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.SEE -> { + val dri = pointedLink(tag) + See( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + dri?.fqDeclarationName() ?: tag.getSubjectName().orEmpty(), + dri, + ) + } + KDocKnownTag.SINCE -> Since(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.CONSTRUCTOR -> Constructor(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + KDocKnownTag.PROPERTY -> Property( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.getSubjectName().orEmpty() + ) + KDocKnownTag.SAMPLE -> Sample( + parseStringToDocNode(tag.getContent(), externalDRIProvider), + tag.getSubjectName().orEmpty() + ) + KDocKnownTag.SUPPRESS -> Suppress(parseStringToDocNode(tag.getContent(), externalDRIProvider)) + } + } + ) + } +} + +private fun findParent(kDoc: PsiElement): PsiElement = + if (kDoc.canHaveParent()) findParent(kDoc.parent) else kDoc + +private fun PsiElement.canHaveParent(): Boolean = this is KDocSection && knownTag != KDocKnownTag.PROPERTY + +private fun getAllKDocTags(kDocImpl: PsiElement): List<KDocTag> = + kDocImpl.children.filterIsInstance<KDocTag>().filterNot { it is KDocSection } + kDocImpl.children.flatMap { + getAllKDocTags(it) + } diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt new file mode 100644 index 00000000..9d26d917 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromSymbol +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink +import org.jetbrains.kotlin.kdoc.psi.impl.KDocName +import org.jetbrains.kotlin.psi.KtPsiFactory + +internal fun DRI?.logIfNotResolved(link: String, logger: DokkaLogger): DRI? { + if(this == null) + logger.warn("Couldn't resolve link for $link") + return this +} + +/** + * It resolves KDoc link via creating PSI. + * + */ +internal fun KtAnalysisSession.resolveKDocTextLink(link: String, context: PsiElement? = null): DRI? { + val psiFactory = context?.let { KtPsiFactory.contextual(it) } ?: KtPsiFactory(this.useSiteModule.project) + val kDoc = psiFactory.createComment( + """ + /** + * [$link] + */ + """.trimIndent() + ) as? KDoc + val kDocLink = kDoc?.getDefaultSection()?.children?.filterIsInstance<KDocLink>()?.singleOrNull() + return kDocLink?.let { resolveKDocLink(it) } +} + +internal fun KtAnalysisSession.resolveKDocLink(link: KDocLink): DRI? { + val lastNameSegment = link.children.filterIsInstance<KDocName>().lastOrNull() + val linkedSymbol = lastNameSegment?.mainReference?.resolveToSymbols()?.firstOrNull() + return if (linkedSymbol == null) null // logger.warn("Couldn't resolve link for $link") + else getDRIFromSymbol(linkedSymbol) +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt new file mode 100644 index 00000000..9b108f80 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc + +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtPossibleMemberSymbol +import org.jetbrains.kotlin.builtins.StandardNames + +private const val ENUM_ENTRIES_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumEntries.kt.template" +private const val ENUM_VALUEOF_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumValueOf.kt.template" +private const val ENUM_VALUES_TEMPLATE_PATH = "/dokka/docs/kdoc/EnumValues.kt.template" + +internal fun KtAnalysisSession.hasGeneratedKDocDocumentation(symbol: KtSymbol): Boolean = + getDocumentationTemplatePath(symbol) != null + +private fun KtAnalysisSession.getDocumentationTemplatePath(symbol: KtSymbol): String? = + when (symbol) { + is KtPropertySymbol -> if (isEnumEntriesProperty(symbol)) ENUM_ENTRIES_TEMPLATE_PATH else null + is KtFunctionSymbol -> { + when { + isEnumValuesMethod(symbol) -> ENUM_VALUES_TEMPLATE_PATH + isEnumValueOfMethod(symbol) -> ENUM_VALUEOF_TEMPLATE_PATH + else -> null + } + } + + else -> null + } + +private fun KtAnalysisSession.isEnumSpecialMember(symbol: KtPossibleMemberSymbol): Boolean = + symbol.origin == KtSymbolOrigin.SOURCE_MEMBER_GENERATED + && (symbol.getContainingSymbol() as? KtClassOrObjectSymbol)?.classKind == KtClassKind.ENUM_CLASS + +private fun KtAnalysisSession.isEnumEntriesProperty(symbol: KtPropertySymbol): Boolean = + symbol.name == StandardNames.ENUM_ENTRIES && isEnumSpecialMember(symbol) + +private fun KtAnalysisSession.isEnumValuesMethod(symbol: KtFunctionSymbol): Boolean = + symbol.name == StandardNames.ENUM_VALUES && isEnumSpecialMember(symbol) + +private fun KtAnalysisSession.isEnumValueOfMethod(symbol: KtFunctionSymbol): Boolean = + symbol.name == StandardNames.ENUM_VALUE_OF && isEnumSpecialMember(symbol) + +internal fun KtAnalysisSession.getGeneratedKDocDocumentationFrom(symbol: KtSymbol): DocumentationNode? { + val templatePath = getDocumentationTemplatePath(symbol) ?: return null + return loadTemplate(templatePath) +} + +private fun KtAnalysisSession.loadTemplate(filePath: String): DocumentationNode? { + val kdoc = loadContent(filePath) ?: return null + val externalDriProvider = { link: String -> + resolveKDocTextLink(link) + } + + val parser = MarkdownParser(externalDriProvider, filePath) + return parser.parse(kdoc) +} + +private fun loadContent(filePath: String): String? = + SymbolsAnalysisPlugin::class.java.getResource(filePath)?.readText() diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorDocumentationContent.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorDocumentationContent.kt new file mode 100644 index 00000000..a3651603 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorDocumentationContent.kt @@ -0,0 +1,15 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.dokka.analysis.java.JavadocTag +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag + +internal data class DescriptorDocumentationContent( + val resolveDocContext: ResolveDocContext, + val element: KDocTag, + override val tag: JavadocTag, +) : DocumentationContent { + override fun resolveSiblings(): List<DocumentationContent> { + return listOf(this) + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorKotlinDocCommentCreator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorKotlinDocCommentCreator.kt new file mode 100644 index 00000000..399d005a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/DescriptorKotlinDocCommentCreator.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.analysis.java.doccomment.DocCommentCreator +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.findKDoc +import org.jetbrains.kotlin.psi.KtElement + +internal class DescriptorKotlinDocCommentCreator : DocCommentCreator { + override fun create(element: PsiNamedElement): DocComment? { + val ktElement = element.navigationElement as? KtElement ?: return null + val kdoc = ktElement.findKDoc() ?: return null + + return KotlinDocComment(kdoc.contentTag, ResolveDocContext(ktElement)) + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocComment.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocComment.kt new file mode 100644 index 00000000..385b5328 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocComment.kt @@ -0,0 +1,81 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import org.jetbrains.dokka.analysis.java.* +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.KtElement + +internal class ResolveDocContext(val ktElement: KtElement) + +internal class KotlinDocComment( + val comment: KDocTag, + val resolveDocContext: ResolveDocContext +) : DocComment { + + private val tagsWithContent: List<KDocTag> = comment.children.mapNotNull { (it as? KDocTag) } + + override fun hasTag(tag: JavadocTag): Boolean { + return when (tag) { + is DescriptionJavadocTag -> comment.getContent().isNotEmpty() + is ThrowingExceptionJavadocTag -> tagsWithContent.any { it.hasException(tag) } + else -> tagsWithContent.any { it.text.startsWith("@${tag.name}") } + } + } + + private fun KDocTag.hasException(tag: ThrowingExceptionJavadocTag) = + text.startsWith("@${tag.name}") && getSubjectName() == tag.exceptionQualifiedName + + override fun resolveTag(tag: JavadocTag): List<DocumentationContent> { + return when (tag) { + is DescriptionJavadocTag -> listOf(DescriptorDocumentationContent(resolveDocContext, comment, tag)) + is ParamJavadocTag -> { + val resolvedContent = resolveGeneric(tag) + listOf(resolvedContent[tag.paramIndex]) + } + + is ThrowsJavadocTag -> resolveThrowingException(tag) + is ExceptionJavadocTag -> resolveThrowingException(tag) + else -> resolveGeneric(tag) + } + } + + private fun resolveThrowingException(tag: ThrowingExceptionJavadocTag): List<DescriptorDocumentationContent> { + val exceptionName = tag.exceptionQualifiedName ?: return resolveGeneric(tag) + + return comment.children + .filterIsInstance<KDocTag>() + .filter { it.name == tag.name && it.getSubjectName() == exceptionName } + .map { DescriptorDocumentationContent(resolveDocContext, it, tag) } + } + + private fun resolveGeneric(tag: JavadocTag): List<DescriptorDocumentationContent> { + return comment.children.mapNotNull { element -> + if (element is KDocTag && element.name == tag.name) { + DescriptorDocumentationContent(resolveDocContext, element, tag) + } else { + null + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KotlinDocComment + + if (comment != other.comment) return false + //if (resolveDocContext.name != other.resolveDocContext.name) return false + if (tagsWithContent != other.tagsWithContent) return false + + return true + } + + override fun hashCode(): Int { + var result = comment.hashCode() + // result = 31 * result + resolveDocContext.name.hashCode() + result = 31 * result + tagsWithContent.hashCode() + return result + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt new file mode 100644 index 00000000..c8ecc786 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt @@ -0,0 +1,46 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.java.doccomment.DocComment +import org.jetbrains.dokka.analysis.java.parsers.DocCommentParser +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logIfNotResolved +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.parseFromKDocTag +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocLink +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.kotlin.analysis.api.analyze + +internal class KotlinDocCommentParser( + private val context: DokkaContext +) : DocCommentParser { + + override fun canParse(docComment: DocComment): Boolean { + return docComment is KotlinDocComment + } + + override fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode { + val kotlinDocComment = docComment as KotlinDocComment + return parseDocumentation(kotlinDocComment) + } + + fun parseDocumentation(element: KotlinDocComment, parseWithChildren: Boolean = true): DocumentationNode { + val sourceSet = context.configuration.sourceSets.let { sourceSets -> + sourceSets.firstOrNull { it.sourceSetID.sourceSetName == "jvmMain" } + ?: sourceSets.first { it.analysisPlatform == Platform.jvm } + } + val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + return analyze(kotlinAnalysis[sourceSet].mainModule) { + parseFromKDocTag( + kDocTag = element.comment, + externalDri = { link -> resolveKDocLink(link).logIfNotResolved(link.getLinkText(), context.logger) }, + kdocLocation = null, + parseWithChildren = parseWithChildren + ) + } + } +} + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinInheritDocTagContentProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinInheritDocTagContentProvider.kt new file mode 100644 index 00000000..6d2bfff4 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinInheritDocTagContentProvider.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java + +import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.java.parsers.doctag.DocTagParserContext +import org.jetbrains.dokka.analysis.java.parsers.doctag.InheritDocTagContentProvider +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query + +internal class KotlinInheritDocTagContentProvider( + context: DokkaContext +) : InheritDocTagContentProvider { + + val parser: KotlinDocCommentParser by lazy { + context.plugin<JavaAnalysisPlugin>().query { docCommentParsers } + .single { it is KotlinDocCommentParser } as KotlinDocCommentParser + } + + override fun canConvert(content: DocumentationContent): Boolean = content is DescriptorDocumentationContent + + override fun convertToHtml(content: DocumentationContent, docTagParserContext: DocTagParserContext): String { + val descriptorContent = content as DescriptorDocumentationContent + val inheritedDocNode = parser.parseDocumentation( + KotlinDocComment(descriptorContent.element, descriptorContent.resolveDocContext), + parseWithChildren = false + ) + val id = docTagParserContext.store(inheritedDocNode) + return """<inheritdoc id="$id"/>""" + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/IllegalModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/IllegalModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..6e3f215a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/IllegalModuleAndPackageDocumentation.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.DokkaException + +internal class IllegalModuleAndPackageDocumentation( + source: ModuleAndPackageDocumentationSource, message: String +) : DokkaException("[$source] $message") diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..46f714cb --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentation.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.model.doc.DocumentationNode + +internal data class ModuleAndPackageDocumentation( + val name: String, + val classifier: Classifier, + val documentation: DocumentationNode +) { + enum class Classifier { Module, Package } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationFragment.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationFragment.kt new file mode 100644 index 00000000..1eafc688 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationFragment.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + + +internal data class ModuleAndPackageDocumentationFragment( + val name: String, + val classifier: ModuleAndPackageDocumentation.Classifier, + val documentation: String, + val source: ModuleAndPackageDocumentationSource +) diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt new file mode 100644 index 00000000..cf627e77 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt @@ -0,0 +1,47 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logIfNotResolved +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Module +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Package +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLink +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.name.FqName + +internal fun interface ModuleAndPackageDocumentationParsingContext { + fun markdownParserFor(fragment: ModuleAndPackageDocumentationFragment, location: String): MarkdownParser +} + +internal fun ModuleAndPackageDocumentationParsingContext.parse( + fragment: ModuleAndPackageDocumentationFragment +): DocumentationNode { + return markdownParserFor(fragment, fragment.source.sourceDescription).parse(fragment.documentation) +} + +internal fun ModuleAndPackageDocumentationParsingContext( + logger: DokkaLogger, + kotlinAnalysis: KotlinAnalysis? = null, + sourceSet: DokkaConfiguration.DokkaSourceSet? = null +) = ModuleAndPackageDocumentationParsingContext { fragment, sourceLocation -> + + if(kotlinAnalysis == null || sourceSet == null) { + MarkdownParser(externalDri = { null }, sourceLocation) + } else { + val analysisContext = kotlinAnalysis[sourceSet] + analyze(analysisContext.mainModule) { + val contextSymbol = when (fragment.classifier) { + Module -> ROOT_PACKAGE_SYMBOL + Package -> getPackageSymbolIfPackageExists(FqName(fragment.name)) + } + + MarkdownParser( + externalDri = { resolveKDocTextLink(it, contextSymbol?.psi).logIfNotResolved(it, logger) }, + sourceLocation + ) + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationReader.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationReader.kt new file mode 100644 index 00000000..82259b89 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationReader.kt @@ -0,0 +1,110 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.Deprecated +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.associateWithNotNull +import org.jetbrains.dokka.analysis.kotlin.internal.ModuleAndPackageDocumentationReader + +internal fun ModuleAndPackageDocumentationReader(context: DokkaContext): ModuleAndPackageDocumentationReader = + ContextModuleAndPackageDocumentationReader(context) + +private class ContextModuleAndPackageDocumentationReader( + private val context: DokkaContext +) : ModuleAndPackageDocumentationReader { + + private val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + + private val documentationFragments: SourceSetDependent<List<ModuleAndPackageDocumentationFragment>> = + context.configuration.sourceSets.associateWith { sourceSet -> + sourceSet.includes.flatMap { include -> parseModuleAndPackageDocumentationFragments(include) } + } + + private fun findDocumentationNodes( + sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, + predicate: (ModuleAndPackageDocumentationFragment) -> Boolean + ): SourceSetDependent<DocumentationNode> { + return sourceSets.associateWithNotNull { sourceSet -> + val fragments = documentationFragments[sourceSet].orEmpty().filter(predicate) + kotlinAnalysis[sourceSet] // test: to throw exception for unknown sourceSet + val documentations = fragments.map { fragment -> + parseModuleAndPackageDocumentation( + context = ModuleAndPackageDocumentationParsingContext(context.logger, kotlinAnalysis, sourceSet), + fragment = fragment + ) + } + when (documentations.size) { + 0 -> null + 1 -> documentations.single().documentation + else -> DocumentationNode(documentations.flatMap { it.documentation.children } + .mergeDocumentationNodes()) + } + } + } + + private val ModuleAndPackageDocumentationFragment.canonicalPackageName: String + get() { + check(classifier == Classifier.Package) + if (name == "[root]") return "" + return name + } + + override fun read(module: DModule): SourceSetDependent<DocumentationNode> { + return findDocumentationNodes(module.sourceSets) { fragment -> + fragment.classifier == Classifier.Module && (fragment.name == module.name) + } + } + + override fun read(pkg: DPackage): SourceSetDependent<DocumentationNode> { + return findDocumentationNodes(pkg.sourceSets) { fragment -> + fragment.classifier == Classifier.Package && fragment.canonicalPackageName == pkg.dri.packageName + } + } + + override fun read(module: DokkaConfiguration.DokkaModuleDescription): DocumentationNode? { + val parsingContext = ModuleAndPackageDocumentationParsingContext(context.logger) + + val documentationFragment = module.includes + .flatMap { include -> parseModuleAndPackageDocumentationFragments(include) } + .firstOrNull { fragment -> fragment.classifier == Classifier.Module && fragment.name == module.name } + ?: return null + + val moduleDocumentation = parseModuleAndPackageDocumentation(parsingContext, documentationFragment) + return moduleDocumentation.documentation + } + + private fun List<TagWrapper>.mergeDocumentationNodes(): List<TagWrapper> = + groupBy { it::class }.values.map { + it.reduce { acc, tagWrapper -> + val newRoot = CustomDocTag( + acc.children + tagWrapper.children, + name = (tagWrapper as? NamedTagWrapper)?.name.orEmpty() + ) + when (acc) { + is See -> acc.copy(newRoot) + is Param -> acc.copy(newRoot) + is Throws -> acc.copy(newRoot) + is Sample -> acc.copy(newRoot) + is Property -> acc.copy(newRoot) + is CustomTagWrapper -> acc.copy(newRoot) + is Description -> acc.copy(newRoot) + is Author -> acc.copy(newRoot) + is Version -> acc.copy(newRoot) + is Since -> acc.copy(newRoot) + is Return -> acc.copy(newRoot) + is Receiver -> acc.copy(newRoot) + is Constructor -> acc.copy(newRoot) + is Deprecated -> acc.copy(newRoot) + is org.jetbrains.dokka.model.doc.Suppress -> acc.copy(newRoot) + } + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationSource.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationSource.kt new file mode 100644 index 00000000..d7877559 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationSource.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import java.io.File + +internal abstract class ModuleAndPackageDocumentationSource { + abstract val sourceDescription: String + abstract val documentation: String + override fun toString(): String = sourceDescription +} + +internal data class ModuleAndPackageDocumentationFile(private val file: File) : ModuleAndPackageDocumentationSource() { + override val sourceDescription: String = file.path + override val documentation: String by lazy(LazyThreadSafetyMode.PUBLICATION) { file.readText() } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentation.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentation.kt new file mode 100644 index 00000000..b92adf4a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentation.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +internal fun parseModuleAndPackageDocumentation( + context: ModuleAndPackageDocumentationParsingContext, + fragment: ModuleAndPackageDocumentationFragment +): ModuleAndPackageDocumentation { + return ModuleAndPackageDocumentation( + name = fragment.name, + classifier = fragment.classifier, + documentation = context.parse(fragment) + ) +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentationFragments.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentationFragments.kt new file mode 100644 index 00000000..ae728a28 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/parseModuleAndPackageDocumentationFragments.kt @@ -0,0 +1,55 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs + +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Module +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Package +import java.io.File + +internal fun parseModuleAndPackageDocumentationFragments(source: File): List<ModuleAndPackageDocumentationFragment> { + return parseModuleAndPackageDocumentationFragments(ModuleAndPackageDocumentationFile(source)) +} + +internal fun parseModuleAndPackageDocumentationFragments( + source: ModuleAndPackageDocumentationSource +): List<ModuleAndPackageDocumentationFragment> { + val fragmentStrings = source.documentation.split(Regex("(|^)#\\s*(?=(Module|Package))")) + return fragmentStrings + .filter(String::isNotBlank) + .map { fragmentString -> parseModuleAndPackageDocFragment(source, fragmentString) } +} + +private fun parseModuleAndPackageDocFragment( + source: ModuleAndPackageDocumentationSource, + fragment: String +): ModuleAndPackageDocumentationFragment { + val firstLineAndDocumentation = fragment.split("\r\n", "\n", "\r", limit = 2) + val firstLine = firstLineAndDocumentation[0] + + val classifierAndName = firstLine.split(Regex("\\s+"), limit = 2) + + val classifier = when (classifierAndName[0].trim()) { + "Module" -> Module + "Package" -> Package + else -> throw IllegalStateException( + """Unexpected classifier: "${classifierAndName[0]}", expected either "Module" or "Package". + |For more information consult the specification: https://kotlinlang.org/docs/dokka-module-and-package-docs.html""".trimMargin() + ) + } + + if (classifierAndName.size != 2 && classifier == Module) { + throw IllegalModuleAndPackageDocumentation(source, "Missing Module name") + } + + val name = classifierAndName.getOrNull(1)?.trim().orEmpty() + if (classifier == Package && name.contains(Regex("\\s"))) { + throw IllegalModuleAndPackageDocumentation( + source, "Package name cannot contain whitespace in '$firstLine'" + ) + } + + return ModuleAndPackageDocumentationFragment( + name = name, + classifier = classifier, + documentation = firstLineAndDocumentation.getOrNull(1)?.trim().orEmpty(), + source = source + ) +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/AnalysisContext.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/AnalysisContext.kt new file mode 100644 index 00000000..eb7c5d70 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/AnalysisContext.kt @@ -0,0 +1,139 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.plugin + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule +import java.io.Closeable +import java.io.File + +@Suppress("FunctionName", "UNUSED_PARAMETER") +internal fun SamplesKotlinAnalysis( + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + context: DokkaContext, + projectKotlinAnalysis: KotlinAnalysis +): KotlinAnalysis { + val environments = sourceSets + .filter { it.samples.isNotEmpty() } + .associateWith { sourceSet -> + createAnalysisContext( + classpath = sourceSet.classpath, + sourceRoots = sourceSet.samples, + sourceSet = sourceSet + ) + } + + return EnvironmentKotlinAnalysis(environments, projectKotlinAnalysis) +} + +internal fun ProjectKotlinAnalysis( + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + context: DokkaContext, +): KotlinAnalysis { + val environments = sourceSets.associateWith { sourceSet -> + createAnalysisContext( + context = context, + sourceSets = sourceSets, + sourceSet = sourceSet + ) + } + return EnvironmentKotlinAnalysis(environments) +} + + +@Suppress("UNUSED_PARAMETER") +internal fun createAnalysisContext( + context: DokkaContext, + sourceSets: List<DokkaConfiguration.DokkaSourceSet>, + sourceSet: DokkaConfiguration.DokkaSourceSet +): AnalysisContext { + val parentSourceSets = sourceSets.filter { it.sourceSetID in sourceSet.dependentSourceSets } + val classpath = sourceSet.classpath + parentSourceSets.flatMap { it.classpath } + val sources = sourceSet.sourceRoots + parentSourceSets.flatMap { it.sourceRoots } + + return createAnalysisContext(classpath, sources, sourceSet) +} + +internal fun createAnalysisContext( + classpath: List<File>, + sourceRoots: Set<File>, + sourceSet: DokkaConfiguration.DokkaSourceSet +): AnalysisContext { + val applicationDisposable: Disposable = Disposer.newDisposable("StandaloneAnalysisAPISession.application") + val projectDisposable: Disposable = Disposer.newDisposable("StandaloneAnalysisAPISession.project") + + val analysis= createAnalysisSession( + classpath = classpath, + sourceRoots = sourceRoots, + analysisPlatform = sourceSet.analysisPlatform, + languageVersion = sourceSet.languageVersion, + apiVersion = sourceSet.apiVersion, + applicationDisposable = applicationDisposable, + projectDisposable = projectDisposable + ) + return AnalysisContextImpl( + mainModule = analysis.second, + analysisSession = analysis.first, + applicationDisposable = applicationDisposable, + projectDisposable = projectDisposable + ) +} + + +/** + * First child delegation. It does not close [parent]. + */ +internal abstract class KotlinAnalysis( + private val parent: KotlinAnalysis? = null +) : Closeable { + + operator fun get(key: DokkaConfiguration.DokkaSourceSet): AnalysisContext { + return get(key.sourceSetID) + } + + internal operator fun get(key: DokkaSourceSetID): AnalysisContext { + return find(key) + ?: parent?.get(key) + ?: throw IllegalStateException("Missing EnvironmentAndFacade for sourceSet $key") + } + + internal abstract fun find(sourceSetID: DokkaSourceSetID): AnalysisContext? +} + +internal open class EnvironmentKotlinAnalysis( + private val environments: SourceSetDependent<AnalysisContext>, + parent: KotlinAnalysis? = null, +) : KotlinAnalysis(parent = parent) { + + override fun find(sourceSetID: DokkaSourceSetID): AnalysisContext? = + environments.entries.firstOrNull { (sourceSet, _) -> sourceSet.sourceSetID == sourceSetID }?.value + + override fun close() { + environments.values.forEach(AnalysisContext::close) + } +} + +internal interface AnalysisContext: Closeable { + val project: Project + val mainModule: KtSourceModule +} + +private class AnalysisContextImpl( + override val mainModule: KtSourceModule, + private val analysisSession: StandaloneAnalysisAPISession, + private val applicationDisposable: Disposable, + private val projectDisposable: Disposable +) : AnalysisContext { + override val project: Project + get() = analysisSession.project + + override fun close() { + Disposer.dispose(applicationDisposable) + Disposer.dispose(projectDisposable) + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/KotlinAnalysis.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/KotlinAnalysis.kt new file mode 100644 index 00000000..8b9e552d --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/KotlinAnalysis.kt @@ -0,0 +1,245 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.plugin + +import com.intellij.core.CoreApplicationEnvironment +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.PsiManager +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.ProjectScope +import com.intellij.util.io.URLUtil +import org.jetbrains.dokka.Platform +import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils +import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider +import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule +import org.jetbrains.kotlin.analysis.project.structure.builder.KtModuleBuilder +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSdkModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.platform.CommonPlatforms +import org.jetbrains.kotlin.platform.js.JsPlatforms +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.platform.konan.NativePlatforms +import org.jetbrains.kotlin.psi.KtFile +import java.io.File +import java.io.IOException +import java.nio.file.* +import java.nio.file.attribute.BasicFileAttributes + +internal fun Platform.toTargetPlatform() = when (this) { + Platform.js, Platform.wasm -> JsPlatforms.defaultJsPlatform + Platform.common -> CommonPlatforms.defaultCommonPlatform + Platform.native -> NativePlatforms.unspecifiedNativePlatform + Platform.jvm -> JvmPlatforms.defaultJvmPlatform +} + +private fun getJdkHomeFromSystemProperty(): File? { + val javaHome = File(System.getProperty("java.home")) + if (!javaHome.exists()) { + // messageCollector.report(CompilerMessageSeverity.WARNING, "Set existed java.home to use JDK") + return null + } + return javaHome +} + +internal fun getLanguageVersionSettings( + languageVersionString: String?, + apiVersionString: String? +): LanguageVersionSettingsImpl { + val languageVersion = LanguageVersion.fromVersionString(languageVersionString) ?: LanguageVersion.LATEST_STABLE + val apiVersion = + apiVersionString?.let { ApiVersion.parse(it) } ?: ApiVersion.createByLanguageVersion(languageVersion) + return LanguageVersionSettingsImpl( + languageVersion = languageVersion, + apiVersion = apiVersion, analysisFlags = hashMapOf( + // special flag for Dokka + // force to resolve light classes (lazily by default) + AnalysisFlags.eagerResolveOfLightClasses to true + ) + ) +} + +// it should be changed after https://github.com/Kotlin/dokka/issues/3114 +internal fun createAnalysisSession( + classpath: List<File>, + sourceRoots: Set<File>, + analysisPlatform: Platform, + languageVersion: String?, + apiVersion: String?, + applicationDisposable: Disposable, + projectDisposable: Disposable +): Pair<StandaloneAnalysisAPISession, KtSourceModule> { + + var sourceModule: KtSourceModule? = null + val analysisSession = buildStandaloneAnalysisAPISession( + applicationDisposable = applicationDisposable, + projectDisposable = projectDisposable, + withPsiDeclarationFromBinaryModuleProvider = false + ) { + val project = project + val targetPlatform = analysisPlatform.toTargetPlatform() + fun KtModuleBuilder.addModuleDependencies(moduleName: String) { + val libraryRoots = classpath + addRegularDependency( + buildKtLibraryModule { + contentScope = ProjectScope.getLibrariesScope(project) + this.platform = targetPlatform + this.project = project + binaryRoots = libraryRoots.map { it.toPath() } + libraryName = "Library for $moduleName" + } + ) + getJdkHomeFromSystemProperty()?.let { jdkHome -> + val vfm = VirtualFileManager.getInstance() + val jdkHomePath = jdkHome.toPath() + val jdkHomeVirtualFile = vfm.findFileByNioPath(jdkHome.toPath())//vfm.findFileByPath(jdkHomePath) + val binaryRoots = LibraryUtils.findClassesFromJdkHome(jdkHomePath).map { + Paths.get(URLUtil.extractPath(it)) + } + addRegularDependency( + buildKtSdkModule { + contentScope = GlobalSearchScope.fileScope(project, jdkHomeVirtualFile) + this.platform = targetPlatform + this.project = project + this.binaryRoots = binaryRoots + sdkName = "JDK for $moduleName" + } + ) + } + } + sourceModule = buildKtSourceModule { + this.languageVersionSettings = getLanguageVersionSettings(languageVersion, apiVersion) + + //val fs = StandardFileSystems.local() + //val psiManager = PsiManager.getInstance(project) + // TODO: We should handle (virtual) file changes announced via LSP with the VFS + /*val ktFiles = sources + .flatMap { Files.walk(it).toList() } + .mapNotNull { fs.findFileByPath(it.toString()) } + .mapNotNull { psiManager.findFile(it) } + .map { it as KtFile }*/ + val sourcePaths = sourceRoots.map { it.absolutePath } + val (ktFilePath, javaFilePath) = getSourceFilePaths(sourcePaths).partition { it.endsWith(KotlinFileType.EXTENSION) } + val javaFiles: List<PsiFileSystemItem> = getPsiFilesFromPaths(project, javaFilePath) + val ktFiles: List<KtFile> = getPsiFilesFromPaths(project, getSourceFilePaths(ktFilePath)) + addSourceRoots(ktFiles + javaFiles) + contentScope = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, ktFiles) + platform = targetPlatform + moduleName = "<module>" + this.project = project + addModuleDependencies(moduleName) + } + + buildKtModuleProvider { + platform = targetPlatform + this.project = project + addModule(sourceModule!!) + } + } + // TODO remove further + CoreApplicationEnvironment.registerExtensionPoint( + analysisSession.project.extensionArea, + KtResolveExtensionProvider.EP_NAME.name, + KtResolveExtensionProvider::class.java + ) + return Pair(analysisSession, sourceModule ?: throw IllegalStateException()) +} + +// ----------- copy-paste from Analysis API ---------------------------------------------------------------------------- +/** + * Collect source file path from the given [root] store them in [result]. + * + * E.g., for `project/app/src` as a [root], this will walk the file tree and + * collect all `.kt` and `.java` files under that folder. + * + * Note that this util gracefully skips [IOException] during file tree traversal. + */ +internal fun collectSourceFilePaths( + root: Path, + result: MutableSet<String> +) { + // NB: [Files#walk] throws an exception if there is an issue during IO. + // With [Files#walkFileTree] with a custom visitor, we can take control of exception handling. + Files.walkFileTree( + root, + object : SimpleFileVisitor<Path>() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + return if (Files.isReadable(dir)) + FileVisitResult.CONTINUE + else + FileVisitResult.SKIP_SUBTREE + } + + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + if (!Files.isRegularFile(file) || !Files.isReadable(file)) + return FileVisitResult.CONTINUE + val ext = file.toFile().extension + if (ext == KotlinFileType.EXTENSION || ext == "java"/*JavaFileType.DEFAULT_EXTENSION*/) { + result.add(file.toString()) + } + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult { + // TODO: report or log [IOException]? + // NB: this intentionally swallows the exception, hence fail-safe. + // Skipping subtree doesn't make any sense, since this is not a directory. + // Skipping sibling may drop valid file paths afterward, so we just continue. + return FileVisitResult.CONTINUE + } + } + ) +} + +/** + * Collect source file path as [String] from the given source roots in [sourceRoot]. + * + * this util collects all `.kt` and `.java` files under source roots. + */ +internal fun getSourceFilePaths( + sourceRoot: Collection<String>, + includeDirectoryRoot: Boolean = false, +): Set<String> { + val result = mutableSetOf<String>() + sourceRoot.forEach { srcRoot -> + val path = Paths.get(srcRoot) + if (Files.isDirectory(path)) { + // E.g., project/app/src + collectSourceFilePaths(path, result) + if (includeDirectoryRoot) { + result.add(srcRoot) + } + } else { + // E.g., project/app/src/some/pkg/main.kt + result.add(srcRoot) + } + } + + return result +} + +internal inline fun <reified T : PsiFileSystemItem> getPsiFilesFromPaths( + project: Project, + paths: Collection<String>, +): List<T> { + val fs = StandardFileSystems.local() + val psiManager = PsiManager.getInstance(project) + val result = mutableListOf<T>() + for (path in paths) { + val vFile = fs.findFileByPath(path) ?: continue + val psiFileSystemItem = + if (vFile.isDirectory) + psiManager.findDirectory(vFile) as? T + else + psiManager.findFile(vFile) as? T + psiFileSystemItem?.let { result.add(it) } + } + return result +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt new file mode 100644 index 00000000..71fdfadb --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt @@ -0,0 +1,121 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.plugin + +import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute +import com.intellij.psi.PsiAnnotation +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.analysis.java.BreakingAbstractionKotlinLightMethodChecker +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.KotlinInheritDocTagContentProvider +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.DescriptorKotlinDocCommentCreator +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.KotlinDocCommentParser +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentationReader +import org.jetbrains.dokka.analysis.kotlin.symbols.services.KotlinAnalysisSourceRootsExtractor +import org.jetbrains.dokka.analysis.kotlin.symbols.services.* +import org.jetbrains.dokka.analysis.kotlin.symbols.services.KotlinDocumentableSourceLanguageParser +import org.jetbrains.dokka.analysis.kotlin.symbols.services.SymbolExternalDocumentablesProvider +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.DefaultSymbolToDocumentableTranslator +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.renderers.PostAction +import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation + +@Suppress("unused") +public class SymbolsAnalysisPlugin : DokkaPlugin() { + + internal val kotlinAnalysis by extensionPoint<KotlinAnalysis>() + + internal val defaultKotlinAnalysis by extending { + kotlinAnalysis providing { ctx -> + ProjectKotlinAnalysis( + sourceSets = ctx.configuration.sourceSets, + context = ctx + ) + } + } + + internal val disposeKotlinAnalysisPostAction by extending { + CoreExtensions.postActions with PostAction { querySingle { kotlinAnalysis }.close() } + } + + internal val symbolToDocumentableTranslator by extending { + CoreExtensions.sourceToDocumentableTranslator providing ::DefaultSymbolToDocumentableTranslator + } + + private val javaAnalysisPlugin by lazy { plugin<JavaAnalysisPlugin>() } + + internal val projectProvider by extending { + javaAnalysisPlugin.projectProvider providing { KotlinAnalysisProjectProvider() } + } + + internal val sourceRootsExtractor by extending { + javaAnalysisPlugin.sourceRootsExtractor providing { KotlinAnalysisSourceRootsExtractor() } + } + + internal val kotlinDocCommentCreator by extending { + javaAnalysisPlugin.docCommentCreators providing { + DescriptorKotlinDocCommentCreator() + } + } + + internal val kotlinDocCommentParser by extending { + javaAnalysisPlugin.docCommentParsers providing { context -> + KotlinDocCommentParser( + context + ) + } + } + internal val inheritDocTagProvider by extending { + javaAnalysisPlugin.inheritDocTagContentProviders providing ::KotlinInheritDocTagContentProvider + } + internal val kotlinLightMethodChecker by extending { + javaAnalysisPlugin.kotlinLightMethodChecker providing { + object : BreakingAbstractionKotlinLightMethodChecker { + override fun isLightAnnotation(annotation: PsiAnnotation): Boolean { + return annotation is KtLightAbstractAnnotation + } + + override fun isLightAnnotationAttribute(attribute: JvmAnnotationAttribute): Boolean { + return attribute is KtLightAbstractAnnotation + } + } + } + } + + + internal val symbolAnalyzerImpl by extending { + plugin<InternalKotlinAnalysisPlugin>().documentableSourceLanguageParser providing { KotlinDocumentableSourceLanguageParser() } + } + internal val symbolFullClassHierarchyBuilder by extending { + plugin<InternalKotlinAnalysisPlugin>().fullClassHierarchyBuilder providing { SymbolFullClassHierarchyBuilder() } + } + + internal val symbolSyntheticDocumentableDetector by extending { + plugin<InternalKotlinAnalysisPlugin>().syntheticDocumentableDetector providing { SymbolSyntheticDocumentableDetector() } + } + + internal val moduleAndPackageDocumentationReader by extending { + plugin<InternalKotlinAnalysisPlugin>().moduleAndPackageDocumentationReader providing ::ModuleAndPackageDocumentationReader + } + + /* internal val kotlinToJavaMapper by extending { + plugin<InternalKotlinAnalysisPlugin>().kotlinToJavaService providing { DescriptorKotlinToJavaMapper() } + } + + intern val descriptorInheritanceBuilder by extending { + plugin<InternalKotlinAnalysisPlugin>().inheritanceBuilder providing { DescriptorInheritanceBuilder() } + } + */ + internal val symbolExternalDocumentablesProvider by extending { + plugin<InternalKotlinAnalysisPlugin>().externalDocumentablesProvider providing ::SymbolExternalDocumentablesProvider + } + + internal val kotlinSampleProviderFactory by extending { + plugin<InternalKotlinAnalysisPlugin>().sampleProviderFactory providing ::KotlinSampleProviderFactory + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisProjectProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisProjectProvider.kt new file mode 100644 index 00000000..47bcb156 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisProjectProvider.kt @@ -0,0 +1,16 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.openapi.project.Project +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.ProjectProvider +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle + +internal class KotlinAnalysisProjectProvider : ProjectProvider { + override fun getProject(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): Project { + val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + return kotlinAnalysis[sourceSet].project + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisSourceRootsExtractor.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisSourceRootsExtractor.kt new file mode 100644 index 00000000..43aceccd --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinAnalysisSourceRootsExtractor.kt @@ -0,0 +1,12 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.SourceRootsExtractor +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File + +internal class KotlinAnalysisSourceRootsExtractor : SourceRootsExtractor { + override fun extract(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): List<File> { + return sourceSet.sourceRoots.filter { directory -> directory.isDirectory || directory.extension == "java" } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinDocumentableSourceLanguageParser.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinDocumentableSourceLanguageParser.kt new file mode 100644 index 00000000..799cd7b6 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinDocumentableSourceLanguageParser.kt @@ -0,0 +1,26 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.WithSources +import org.jetbrains.dokka.analysis.kotlin.internal.DocumentableLanguage +import org.jetbrains.dokka.analysis.kotlin.internal.DocumentableSourceLanguageParser + +internal class KotlinDocumentableSourceLanguageParser : DocumentableSourceLanguageParser { + + /** + * For members inherited from Java in Kotlin - it returns [DocumentableLanguage.KOTLIN] + */ + override fun getLanguage( + documentable: Documentable, + sourceSet: DokkaConfiguration.DokkaSourceSet, + ): DocumentableLanguage? { + val documentableSource = (documentable as? WithSources)?.sources?.get(sourceSet) ?: return null + return when (documentableSource) { + is PsiDocumentableSource -> DocumentableLanguage.JAVA + is KtPsiDocumentableSource -> DocumentableLanguage.KOTLIN + else -> error("Unknown language sources: ${documentableSource::class}") + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt new file mode 100644 index 00000000..dc61b35b --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProvider +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SamplesKotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtFile + +class KotlinSampleProviderFactory(val context: DokkaContext): SampleProviderFactory { + override fun build(): SampleProvider { + return KotlinSampleProvider(context) + } + +} +/** + * It's declared as open since StdLib has its own sample transformer + * with [processBody] and [processImports] + */ +@InternalDokkaApi +open class KotlinSampleProvider(val context: DokkaContext): SampleProvider { + private val kotlinAnalysis = SamplesKotlinAnalysis( + sourceSets = context.configuration.sourceSets, + context = context, + projectKotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + ) + + protected open fun processBody(psiElement: PsiElement): String { + val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() + val lines = text.split("\n") + val indent = lines.filter(String::isNotBlank).minOfOrNull { it.takeWhile(Char::isWhitespace).count() } ?: 0 + return lines.joinToString("\n") { it.drop(indent) } + } + + private fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { + is KtDeclarationWithBody -> { + when (val bodyExpression = psiElement.bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + protected open fun processImports(psiElement: PsiElement): String { + val psiFile = psiElement.containingFile + return when(val text = (psiFile as? KtFile)?.importList?.text) { + is String -> text + else -> "" + } + } + + /** + * @return [SampleProvider.SampleSnippet] or null if it has not found by [fqLink] + */ + override fun getSample(sourceSet: DokkaConfiguration.DokkaSourceSet, fqLink: String): SampleProvider.SampleSnippet? { + val analysisContext = kotlinAnalysis[sourceSet] + val psiElement = analyze(analysisContext.mainModule) { + val lastDotIndex = fqLink.lastIndexOf('.') + + val functionName = if (lastDotIndex == -1) fqLink else fqLink.substring(lastDotIndex + 1, fqLink.length) + val packageName = if (lastDotIndex == -1) "" else fqLink.substring(0, lastDotIndex) + getTopLevelCallableSymbols(FqName(packageName), Name.identifier(functionName)).firstOrNull()?.psi + } + ?: return null.also { context.logger.warn("Cannot find PsiElement corresponding to $fqLink") } + val imports = + processImports(psiElement) + val body = processBody(psiElement) + + return SampleProvider.SampleSnippet(imports, body) + } + override fun close() { + kotlinAnalysis.close() + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KtPsiDocumentableSource.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KtPsiDocumentableSource.kt new file mode 100644 index 00000000..3a819931 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KtPsiDocumentableSource.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.model.DocumentableSource +import org.jetbrains.kotlin.lexer.KtTokens + + +internal class KtPsiDocumentableSource(val psi: PsiElement?) : DocumentableSource { + override val path = psi?.containingFile?.virtualFile?.path ?: "" + + override fun computeLineNumber(): Int? { + return psi?.let { + val range = it.node?.findChildByType(KtTokens.IDENTIFIER)?.textRange ?: it.textRange + val doc = PsiDocumentManager.getInstance(it.project).getDocument(it.containingFile) + // IJ uses 0-based line-numbers; external source browsers use 1-based + doc?.getLineNumber(range.startOffset)?.plus(1) + } + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt new file mode 100644 index 00000000..c328fe57 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolExternalDocumentablesProvider.kt @@ -0,0 +1,34 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.DokkaSymbolVisitor +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getClassIdFromDRI +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromSymbol +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DClasslike +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.symbols.KtNamedClassOrObjectSymbol +import org.jetbrains.dokka.analysis.kotlin.internal.ExternalDocumentablesProvider + +internal class SymbolExternalDocumentablesProvider(val context: DokkaContext) : ExternalDocumentablesProvider { + private val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + + override fun findClasslike(dri: DRI, sourceSet: DokkaSourceSet): DClasslike? { + val classId = getClassIdFromDRI(dri) + + val analysisContext = kotlinAnalysis[sourceSet] + return analyze(analysisContext.mainModule) { + val symbol = getClassOrObjectSymbolByClassId(classId) as? KtNamedClassOrObjectSymbol?: return@analyze null + val translator = DokkaSymbolVisitor(sourceSet, sourceSet.displayName, analysisContext, logger = context.logger) + + val parentDRI = symbol.getContainingSymbol()?.let { getDRIFromSymbol(it) } ?: /* top level */ DRI(dri.packageName) + with(translator) { + return@analyze visitNamedClassOrObjectSymbol(symbol, parentDRI) + } + } + } +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolFullClassHierarchyBuilder.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolFullClassHierarchyBuilder.kt new file mode 100644 index 00000000..d4269b11 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolFullClassHierarchyBuilder.kt @@ -0,0 +1,85 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiClass +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.analysis.java.util.from +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromClassLike +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.types.KtType +import org.jetbrains.dokka.analysis.kotlin.internal.ClassHierarchy +import org.jetbrains.dokka.analysis.kotlin.internal.FullClassHierarchyBuilder +import org.jetbrains.dokka.analysis.kotlin.internal.Supertypes +import org.jetbrains.kotlin.psi.KtClassOrObject +import java.util.concurrent.ConcurrentHashMap + + +internal class SymbolFullClassHierarchyBuilder : FullClassHierarchyBuilder { + override suspend fun build(module: DModule): ClassHierarchy { + val map = module.sourceSets.associateWith { ConcurrentHashMap<DRI, List<DRI>>() } + module.packages.forEach { visitDocumentable(it, map) } + return map + } + + private fun KtAnalysisSession.collectSupertypesFromKtType( + driWithKType: Pair<DRI, KtType>, + supersMap: MutableMap<DRI, Supertypes> + ) { + val (dri, kotlinType) = driWithKType + val supertypes = kotlinType.getDirectSuperTypes().filterNot { it.isAny } + val supertypesDriWithKType = supertypes.mapNotNull { supertype -> + supertype.expandedClassSymbol?.let { + getDRIFromClassLike(it) to supertype + } + } + + if (supersMap[dri] == null) { + supersMap[dri] = supertypesDriWithKType.map { it.first } + supertypesDriWithKType.forEach{ collectSupertypesFromKtType(it, supersMap) } + } + } + + private fun collectSupertypesFromPsiClass( + driWithPsiClass: Pair<DRI, PsiClass>, + supersMap: MutableMap<DRI, Supertypes> + ) { + val (dri, psiClass) = driWithPsiClass + val supertypes = psiClass.superTypes.mapNotNull { it.resolve() } + .filterNot { it.qualifiedName == "java.lang.Object" } + val supertypesDriWithPsiClass = supertypes.map { DRI.from(it) to it } + + if (supersMap[dri] == null) { + supersMap[dri] = supertypesDriWithPsiClass.map { it.first } + supertypesDriWithPsiClass.forEach { collectSupertypesFromPsiClass(it, supersMap) } + } + } + + private fun visitDocumentable( + documentable: Documentable, + hierarchy: SourceSetDependent<MutableMap<DRI, List<DRI>>> + ) { + if (documentable is WithScope) { + documentable.classlikes.forEach { visitDocumentable(it, hierarchy) } + } + if (documentable is DClasslike) { + // to build a full class graph, + // using supertypes from Documentable is not enough since it keeps only one level of hierarchy + documentable.sources.forEach { (sourceSet, source) -> + if (source is KtPsiDocumentableSource) { + (source.psi as? KtClassOrObject)?.let { psi -> + analyze(psi) { + val type = psi.getNamedClassOrObjectSymbol()?.buildSelfClassType() ?: return@analyze + hierarchy[sourceSet]?.let { collectSupertypesFromKtType(documentable.dri to type, it) } + } + } + } else if (source is PsiDocumentableSource) { + val psi = source.psi as PsiClass + hierarchy[sourceSet]?.let { collectSupertypesFromPsiClass(documentable.dri to psi, it) } + } + } + } + } + +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSyntheticDocumentableDetector.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSyntheticDocumentableDetector.kt new file mode 100644 index 00000000..20d2508b --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSyntheticDocumentableDetector.kt @@ -0,0 +1,41 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.services + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.util.PsiDocumentableSource +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.InheritedMember +import org.jetbrains.dokka.model.WithSources +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.analysis.kotlin.internal.SyntheticDocumentableDetector + +internal class SymbolSyntheticDocumentableDetector : SyntheticDocumentableDetector { + + /** + * Currently, it's used only for [org.jetbrains.dokka.base.transformers.documentables.ReportUndocumentedTransformer] + * + * For so-called fake-ovveride declarations - we have [InheritedMember] extra. + * For synthesized declaration - we do not have PSI source. + * + * @see org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin.SOURCE_MEMBER_GENERATED + */ + override fun isSynthetic(documentable: Documentable, sourceSet: DokkaConfiguration.DokkaSourceSet): Boolean { + @Suppress("UNCHECKED_CAST") + val extra = (documentable as? WithExtraProperties<Documentable>)?.extra + val isInherited = extra?.get(InheritedMember)?.inheritedFrom?.get(sourceSet) != null + // TODO the same for JAVA? + val isSynthesized = documentable.getPsi(sourceSet) == null + return isInherited || isSynthesized + } + + private fun Documentable.getPsi(sourceSet: DokkaConfiguration.DokkaSourceSet): PsiElement? { + val documentableSource = (this as? WithSources)?.sources?.get(sourceSet) ?: return null + return when (documentableSource) { + is PsiDocumentableSource -> documentableSource.psi + is KtPsiDocumentableSource -> documentableSource.psi + else -> error("Unknown language sources: ${documentableSource::class}") + } + } + + +} diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/AnnotationTranslator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/AnnotationTranslator.kt new file mode 100644 index 00000000..02198518 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/AnnotationTranslator.kt @@ -0,0 +1,134 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.withEnumEntryExtra +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.ClassValue +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.annotations.* +import org.jetbrains.kotlin.analysis.api.base.KtConstantValue +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin +import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtFile + +/** + * Map [KtAnnotationApplication] to Dokka [Annotations.Annotation] + */ +internal class AnnotationTranslator { + private fun KtAnalysisSession.getFileLevelAnnotationsFrom(symbol: KtSymbol) = + if (symbol.origin != KtSymbolOrigin.SOURCE) + null + else + (symbol.psi?.containingFile as? KtFile)?.getFileSymbol()?.annotations + ?.map { toDokkaAnnotation(it) } + + private fun KtAnalysisSession.getDirectAnnotationsFrom(annotated: KtAnnotated) = + annotated.annotations.map { toDokkaAnnotation(it) } + + /** + * @return direct annotations and file-level annotations + */ + fun KtAnalysisSession.getAllAnnotationsFrom(annotated: KtAnnotated): List<Annotations.Annotation> { + val directAnnotations = getDirectAnnotationsFrom(annotated) + val fileLevelAnnotations = (annotated as? KtSymbol)?.let { getFileLevelAnnotationsFrom(it) } ?: emptyList() + return directAnnotations + fileLevelAnnotations + } + + private fun KtAnnotationApplication.isNoExistedInSource() = psi == null + private fun AnnotationUseSiteTarget.toDokkaAnnotationScope(): Annotations.AnnotationScope = when (this) { + AnnotationUseSiteTarget.PROPERTY_GETTER -> Annotations.AnnotationScope.DIRECT // due to compatibility with Dokka K1 + AnnotationUseSiteTarget.PROPERTY_SETTER -> Annotations.AnnotationScope.DIRECT // due to compatibility with Dokka K1 + AnnotationUseSiteTarget.FILE -> Annotations.AnnotationScope.FILE + else -> Annotations.AnnotationScope.DIRECT + } + + private fun KtAnalysisSession.mustBeDocumented(annotationApplication: KtAnnotationApplication): Boolean { + if (annotationApplication.isNoExistedInSource()) return false + val annotationClass = getClassOrObjectSymbolByClassId(annotationApplication.classId ?: return false) + return annotationClass?.hasAnnotation(mustBeDocumentedAnnotation) + ?: false + } + + private fun KtAnalysisSession.toDokkaAnnotation(annotationApplication: KtAnnotationApplication) = + Annotations.Annotation( + dri = annotationApplication.classId?.createDRI() + ?: DRI(packageName = "", classNames = ERROR_CLASS_NAME), // classId might be null on a non-existing annotation call, + params = if (annotationApplication is KtAnnotationApplicationWithArgumentsInfo) annotationApplication.arguments.associate { + it.name.asString() to toDokkaAnnotationValue( + it.expression + ) + } else emptyMap(), + mustBeDocumented = mustBeDocumented(annotationApplication), + scope = annotationApplication.useSiteTarget?.toDokkaAnnotationScope() ?: Annotations.AnnotationScope.DIRECT + ) + + @OptIn(ExperimentalUnsignedTypes::class) + private fun KtAnalysisSession.toDokkaAnnotationValue(annotationValue: KtAnnotationValue): AnnotationParameterValue = + when (annotationValue) { + is KtConstantAnnotationValue -> { + when (val value = annotationValue.constantValue) { + is KtConstantValue.KtNullConstantValue -> NullValue + is KtConstantValue.KtFloatConstantValue -> FloatValue(value.value) + is KtConstantValue.KtDoubleConstantValue -> DoubleValue(value.value) + is KtConstantValue.KtLongConstantValue -> LongValue(value.value) + is KtConstantValue.KtIntConstantValue -> IntValue(value.value) + is KtConstantValue.KtBooleanConstantValue -> BooleanValue(value.value) + is KtConstantValue.KtByteConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtCharConstantValue -> StringValue(value.value.toString()) + is KtConstantValue.KtErrorConstantValue -> StringValue(value.renderAsKotlinConstant()) + is KtConstantValue.KtShortConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtStringConstantValue -> StringValue(value.value) + is KtConstantValue.KtUnsignedByteConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtUnsignedIntConstantValue -> IntValue(value.value.toInt()) + is KtConstantValue.KtUnsignedLongConstantValue -> LongValue(value.value.toLong()) + is KtConstantValue.KtUnsignedShortConstantValue -> IntValue(value.value.toInt()) + } + } + + is KtEnumEntryAnnotationValue -> EnumValue( + with(annotationValue.callableId) { this?.className?.asString() + "." + this?.callableName?.asString() }, + getDRIFrom(annotationValue) + ) + + is KtArrayAnnotationValue -> ArrayValue(annotationValue.values.map { toDokkaAnnotationValue(it) }) + is KtAnnotationApplicationValue -> AnnotationValue(toDokkaAnnotation(annotationValue.annotationValue)) + is KtKClassAnnotationValue.KtNonLocalKClassAnnotationValue -> ClassValue( + annotationValue.classId.relativeClassName.asString(), + annotationValue.classId.createDRI() + ) + + is KtKClassAnnotationValue.KtLocalKClassAnnotationValue -> throw IllegalStateException("Unexpected a local class in annotation") + is KtKClassAnnotationValue.KtErrorClassAnnotationValue -> ClassValue( + annotationValue.unresolvedQualifierName ?: "", + DRI(packageName = "", classNames = ERROR_CLASS_NAME) + ) + + KtUnsupportedAnnotationValue -> TODO() + } + + private fun getDRIFrom(enumEntry: KtEnumEntryAnnotationValue): DRI { + val callableId = + enumEntry.callableId ?: throw IllegalStateException("Can't get `callableId` for enum entry from annotation") + return DRI( + packageName = callableId.packageName.asString(), + classNames = callableId.className?.asString() + "." + callableId.callableName.asString(), + ).withEnumEntryExtra() + } + + companion object { + val mustBeDocumentedAnnotation = ClassId(FqName("kotlin.annotation"), FqName("MustBeDocumented"), false) + private val parameterNameAnnotation = ClassId(FqName("kotlin"), FqName("ParameterName"), false) + + /** + * Functional types can have **generated** [ParameterName] annotation + */ + internal fun KtAnnotated.getPresentableName(): String? = + this.annotationsByClassId(parameterNameAnnotation) + .firstOrNull()?.arguments?.firstOrNull { it.name == Name.identifier("name") }?.expression?.let { it as? KtConstantAnnotationValue } + ?.let { it.constantValue.value.toString() } + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DRIFactory.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DRIFactory.kt new file mode 100644 index 00000000..ed5ed532 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DRIFactory.kt @@ -0,0 +1,140 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.links.* +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolKind +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithKind +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithTypeParameters +import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +internal fun ClassId.createDRI(): DRI = DRI( + packageName = this.packageFqName.asString(), classNames = this.relativeClassName.asString() +) + +private fun CallableId.createDRI(receiver: TypeReference?, params: List<TypeReference>): DRI = DRI( + packageName = this.packageName.asString(), + classNames = this.className?.asString(), + callable = Callable( + this.callableName.asString(), + params = params, + receiver = receiver + ) +) + +internal fun getDRIFromNonErrorClassType(nonErrorClassType: KtNonErrorClassType): DRI = + nonErrorClassType.classId.createDRI() + +private val KtCallableSymbol.callableId + get() = this.callableIdIfNonLocal ?: throw IllegalStateException("Can not get callable Id due to it is local") + +// because of compatibility with Dokka K1, DRI of entry is kept as non-callable +internal fun getDRIFromEnumEntry(symbol: KtEnumEntrySymbol): DRI = + symbol.callableId.let { + DRI( + packageName = it.packageName.asString(), + classNames = it.className?.asString() + "." + it.callableName.asString(), + ) + }.withEnumEntryExtra() + + +internal fun KtAnalysisSession.getDRIFromTypeParameter(symbol: KtTypeParameterSymbol): DRI { + val containingSymbol = + (symbol.getContainingSymbol() as? KtSymbolWithTypeParameters) + ?: throw IllegalStateException("Containing symbol is null for type parameter") + val typeParameters = containingSymbol.typeParameters + val index = typeParameters.indexOfFirst { symbol.name == it.name } + return getDRIFromSymbol(containingSymbol).copy(target = PointingToGenericParameters(index)) +} + +internal fun KtAnalysisSession.getDRIFromConstructor(symbol: KtConstructorSymbol): DRI = + (symbol.containingClassIdIfNonLocal + ?: throw IllegalStateException("Can not get class Id due to it is local")).createDRI().copy( + callable = Callable( + name = symbol.containingClassIdIfNonLocal?.relativeClassName?.asString() ?: "", + params = symbol.valueParameters.map { getTypeReferenceFrom(it.returnType) }) + ) + +internal fun KtAnalysisSession.getDRIFromVariableLike(symbol: KtVariableLikeSymbol): DRI { + val receiver = symbol.receiverType?.let { + getTypeReferenceFrom(it) + } + return symbol.callableId.createDRI(receiver, emptyList()) +} + +internal fun KtAnalysisSession.getDRIFromFunctionLike(symbol: KtFunctionLikeSymbol): DRI { + val params = symbol.valueParameters.map { getTypeReferenceFrom(it.returnType) } + val receiver = symbol.receiverType?.let { + getTypeReferenceFrom(it) + } + return symbol.callableIdIfNonLocal?.createDRI(receiver, params) + ?: getDRIFromLocalFunction(symbol) +} + +internal fun getDRIFromClassLike(symbol: KtClassLikeSymbol): DRI = + symbol.classIdIfNonLocal?.createDRI() ?: throw IllegalStateException("Can not get class Id due to it is local") + +internal fun getDRIFromPackage(symbol: KtPackageSymbol): DRI = + DRI(packageName = symbol.fqName.asString()) + +internal fun KtAnalysisSession.getDRIFromValueParameter(symbol: KtValueParameterSymbol): DRI { + val function = (symbol.getContainingSymbol() as? KtFunctionLikeSymbol) + ?: throw IllegalStateException("Containing symbol is null for type parameter") + val index = function.valueParameters.indexOfFirst { it.name == symbol.name } + val funDRI = getDRIFromFunctionLike(function) + return funDRI.copy(target = PointingToCallableParameters(index)) +} + +internal fun KtAnalysisSession.getDRIFromSymbol(symbol: KtSymbol): DRI = + when (symbol) { + is KtEnumEntrySymbol -> getDRIFromEnumEntry(symbol) + is KtTypeParameterSymbol -> getDRIFromTypeParameter(symbol) + is KtConstructorSymbol -> getDRIFromConstructor(symbol) + is KtValueParameterSymbol -> getDRIFromValueParameter(symbol) + is KtVariableLikeSymbol -> getDRIFromVariableLike(symbol) + is KtFunctionLikeSymbol -> getDRIFromFunctionLike(symbol) + is KtClassLikeSymbol -> getDRIFromClassLike(symbol) + is KtPackageSymbol -> getDRIFromPackage(symbol) + else -> throw IllegalStateException("Unknown symbol while creating DRI ") + } + +private fun KtAnalysisSession.getDRIFromNonCallablePossibleLocalSymbol(symbol: KtSymbol): DRI { + if ((symbol as? KtSymbolWithKind)?.symbolKind == KtSymbolKind.LOCAL) { + return symbol.getContainingSymbol()?.let { getDRIFromNonCallablePossibleLocalSymbol(it) } + ?: throw IllegalStateException("Can't get containing symbol for local symbol") + } + return getDRIFromSymbol(symbol) +} + +/** + * Currently, it's used only for functions from enum entry, + * For its members: `memberSymbol.callableIdIfNonLocal=null` + */ +private fun KtAnalysisSession.getDRIFromLocalFunction(symbol: KtFunctionLikeSymbol): DRI { + /** + * A function is inside local object + */ + val containingSymbolDRI = symbol.getContainingSymbol()?.let { getDRIFromNonCallablePossibleLocalSymbol(it) } + ?: throw IllegalStateException("Can't get containing symbol for local function") + return containingSymbolDRI.copy( + callable = Callable( + (symbol as? KtNamedSymbol)?.name?.asString() ?: "", + params = symbol.valueParameters.map { getTypeReferenceFrom(it.returnType) }, + receiver = symbol.receiverType?.let { + getTypeReferenceFrom(it) + } + ) + ) +} + +// ----------- DRI => compiler identifiers ---------------------------------------------------------------------------- +internal fun getClassIdFromDRI(dri: DRI) = ClassId( + FqName(dri.packageName ?: ""), + FqName(dri.classNames ?: throw IllegalStateException("DRI must have `classNames` to get ClassID")), + false +) + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt new file mode 100644 index 00000000..0c79b8a0 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt @@ -0,0 +1,949 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + + +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.* +import com.intellij.psi.util.PsiLiteralUtil +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.java.parsers.JavadocParser +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.getGeneratedKDocDocumentationFrom +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.AnalysisContext +import org.jetbrains.dokka.analysis.kotlin.symbols.services.KtPsiDocumentableSource +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.getJavaDocDocumentationFrom +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.getKDocDocumentationFrom +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.hasGeneratedKDocDocumentation +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.AnnotationTranslator.Companion.getPresentableName +import org.jetbrains.dokka.analysis.kotlin.symbols.utils.typeConstructorsBeingExceptions +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.Visibility +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.sources.AsyncSourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.analysis.api.* +import org.jetbrains.kotlin.analysis.api.annotations.KtAnnotated +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithModality +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithVisibility +import org.jetbrains.kotlin.analysis.api.types.* +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.descriptors.java.JavaVisibilities +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier +import org.jetbrains.kotlin.psi.psiUtil.hasExpectModifier +import java.nio.file.Paths + +internal class DefaultSymbolToDocumentableTranslator(context: DokkaContext) : AsyncSourceToDocumentableTranslator { + private val kotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + private val javadocParser = JavadocParser( + docCommentParsers = context.plugin<JavaAnalysisPlugin>().query { docCommentParsers }, + docCommentFinder = context.plugin<JavaAnalysisPlugin>().docCommentFinder + ) + + override suspend fun invokeSuspending( + sourceSet: DokkaConfiguration.DokkaSourceSet, + context: DokkaContext + ): DModule { + val analysisContext = kotlinAnalysis[sourceSet] + @Suppress("unused") + return DokkaSymbolVisitor( + sourceSet = sourceSet, + moduleName = context.configuration.moduleName, + analysisContext = analysisContext, + logger = context.logger, + javadocParser = javadocParser + ).visitModule() + } +} + +internal fun <T : Bound> T.wrapWithVariance(variance: org.jetbrains.kotlin.types.Variance) = + when (variance) { + org.jetbrains.kotlin.types.Variance.INVARIANT -> Invariance(this) + org.jetbrains.kotlin.types.Variance.IN_VARIANCE -> Contravariance(this) + org.jetbrains.kotlin.types.Variance.OUT_VARIANCE -> Covariance(this) + } + +/** + * Maps [KtSymbol] to Documentable model [Documentable] + */ +internal class DokkaSymbolVisitor( + private val sourceSet: DokkaConfiguration.DokkaSourceSet, + private val moduleName: String, + private val analysisContext: AnalysisContext, + private val logger: DokkaLogger, + private val javadocParser: JavadocParser? = null +) { + private var annotationTranslator = AnnotationTranslator() + private var typeTranslator = TypeTranslator(sourceSet, annotationTranslator) + + /** + * To avoid recursive classes + * e.g. + * open class Klass() { + * object Default : Klass() + * } + */ + private val visitedNamedClassOrObjectSymbol: MutableSet<ClassId> = + mutableSetOf() + + private fun <T> T.toSourceSetDependent() = if (this != null) mapOf(sourceSet to this) else emptyMap() + + // KT-54846 will replace + private val KtDeclarationSymbol.isActual + get() = (psi as? KtModifierListOwner)?.hasActualModifier() == true + private val KtDeclarationSymbol.isExpect + get() = (psi as? KtModifierListOwner)?.hasExpectModifier() == true + + private fun <T : KtSymbol> Collection<T>.filterSymbolsInSourceSet() = filter { + it.psi?.containingFile?.virtualFile?.path?.let { path -> + path.isNotBlank() && sourceSet.sourceRoots.any { root -> + Paths.get(path).startsWith(root.toPath()) + } + } == true + } + + fun visitModule(): DModule { + val ktFiles: List<KtFile> = getPsiFilesFromPaths( + analysisContext.project, + getSourceFilePaths(sourceSet.sourceRoots.map { it.canonicalPath }) + ) + val processedPackages: MutableSet<FqName> = mutableSetOf() + return analyze(analysisContext.mainModule) { + val packageSymbols: List<DPackage> = ktFiles + .mapNotNull { + if (processedPackages.contains(it.packageFqName)) + return@mapNotNull null + processedPackages.add(it.packageFqName) + getPackageSymbolIfPackageExists(it.packageFqName)?.let { it1 -> + visitPackageSymbol( + it1 + ) + } + } + + DModule( + name = moduleName, + packages = packageSymbols, + documentation = emptyMap(), + expectPresentInSet = null, + sourceSets = setOf(sourceSet) + ) + } + } + + private fun KtAnalysisSession.visitPackageSymbol(packageSymbol: KtPackageSymbol): DPackage { + val dri = getDRIFromPackage(packageSymbol) + val scope = packageSymbol.getPackageScope() + val callables = scope.getCallableSymbols().toList().filterSymbolsInSourceSet() + val classifiers = scope.getClassifierSymbols().toList().filterSymbolsInSourceSet() + + val functions = callables.filterIsInstance<KtFunctionSymbol>().map { visitFunctionSymbol(it, dri) } + val properties = callables.filterIsInstance<KtPropertySymbol>().map { visitPropertySymbol(it, dri) } + val classlikes = + classifiers.filterIsInstance<KtNamedClassOrObjectSymbol>() + .map { visitNamedClassOrObjectSymbol(it, dri) } + val typealiases = classifiers.filterIsInstance<KtTypeAliasSymbol>().map { visitTypeAliasSymbol(it, dri) } + + return DPackage( + dri = dri, + functions = functions, + properties = properties, + classlikes = classlikes, + typealiases = typealiases, + documentation = emptyMap(), + sourceSets = setOf(sourceSet) + ) + } + + private fun KtAnalysisSession.visitTypeAliasSymbol( + typeAliasSymbol: KtTypeAliasSymbol, + parent: DRI + ): DTypeAlias = withExceptionCatcher(typeAliasSymbol) { + val name = typeAliasSymbol.name.asString() + val dri = parent.withClass(name) + + val ancestryInfo = with(typeTranslator) { buildAncestryInformationFrom(typeAliasSymbol.expandedType) } + + val generics = + typeAliasSymbol.typeParameters.mapIndexed { index, symbol -> visitVariantTypeParameter(index, symbol, dri) } + + return DTypeAlias( + dri = dri, + name = name, + type = GenericTypeConstructor( + dri = dri, + projections = generics.map { it.variantTypeParameter }), // this property can be removed in DTypeAlias + expectPresentInSet = null, + underlyingType = toBoundFrom(typeAliasSymbol.expandedType).toSourceSetDependent(), + visibility = typeAliasSymbol.getDokkaVisibility().toSourceSetDependent(), + documentation = getDocumentation(typeAliasSymbol)?.toSourceSetDependent() ?: emptyMap(), + sourceSets = setOf(sourceSet), + generics = generics, + sources = typeAliasSymbol.getSource(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(typeAliasSymbol)?.toSourceSetDependent()?.toAnnotations(), + ancestryInfo.exceptionInSupertypesOrNull(), + ) + ) + } + + fun KtAnalysisSession.visitNamedClassOrObjectSymbol( + namedClassOrObjectSymbol: KtNamedClassOrObjectSymbol, + parent: DRI + ): DClasslike = withExceptionCatcher(namedClassOrObjectSymbol) { + namedClassOrObjectSymbol.classIdIfNonLocal?.let { visitedNamedClassOrObjectSymbol.add(it) } + + val name = namedClassOrObjectSymbol.name.asString() + val dri = parent.withClass(name) + + val isExpect = namedClassOrObjectSymbol.isExpect + val isActual = namedClassOrObjectSymbol.isActual + val documentation = getDocumentation(namedClassOrObjectSymbol)?.toSourceSetDependent() ?: emptyMap() + + val (constructors, functions, properties, classlikes) = getDokkaScopeFrom(namedClassOrObjectSymbol, dri) + + val generics = namedClassOrObjectSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + val ancestryInfo = + with(typeTranslator) { buildAncestryInformationFrom(namedClassOrObjectSymbol.buildSelfClassType()) } + val supertypes = + //(ancestryInfo.interfaces.map{ it.typeConstructor } + listOfNotNull(ancestryInfo.superclass?.typeConstructor)) + namedClassOrObjectSymbol.superTypes.filterNot { it.isAny } + .map { with(typeTranslator) { toTypeConstructorWithKindFrom(it) } } + .toSourceSetDependent() + + return@withExceptionCatcher when (namedClassOrObjectSymbol.classKind) { + KtClassKind.OBJECT, KtClassKind.COMPANION_OBJECT -> + DObject( + dri = dri, + name = name, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + documentation = documentation, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent() + ?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()), + ancestryInfo.exceptionInSupertypesOrNull() + ) + ) + + KtClassKind.CLASS -> DClass( + dri = dri, + name = name, + constructors = constructors, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + generics = generics, + documentation = documentation, + modifier = namedClassOrObjectSymbol.getDokkaModality().toSourceSetDependent(), + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()), + ancestryInfo.exceptionInSupertypesOrNull() + ) + ) + + KtClassKind.INTERFACE -> DInterface( + dri = dri, + name = name, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), // + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + generics = generics, + documentation = documentation, + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()), + ancestryInfo.exceptionInSupertypesOrNull() + ) + ) + + KtClassKind.ANNOTATION_CLASS -> DAnnotation( + dri = dri, + name = name, + documentation = documentation, + functions = functions, + properties = properties, + classlikes = classlikes, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + generics = generics, + constructors = constructors, + sources = namedClassOrObjectSymbol.getSource(), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ) + ) + + KtClassKind.ANONYMOUS_OBJECT -> throw NotImplementedError("ANONYMOUS_OBJECT does not support") + KtClassKind.ENUM_CLASS -> { + val entries = namedClassOrObjectSymbol.getEnumEntries().map { visitEnumEntrySymbol(it) } + + DEnum( + dri = dri, + name = name, + entries = entries, + constructors = constructors, + functions = functions, + properties = properties, + classlikes = classlikes, + sources = namedClassOrObjectSymbol.getSource(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = namedClassOrObjectSymbol.getDokkaVisibility().toSourceSetDependent(), + supertypes = supertypes, + documentation = documentation, + companion = namedClassOrObjectSymbol.companionObject?.let { + visitNamedClassOrObjectSymbol( + it, + dri + ) + } as? DObject, + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + namedClassOrObjectSymbol.additionalExtras()?.toSourceSetDependent() + ?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(namedClassOrObjectSymbol)?.toSourceSetDependent()?.toAnnotations(), + ImplementedInterfaces(ancestryInfo.allImplementedInterfaces().toSourceSetDependent()) + ) + ) + } + } + } + + private data class DokkaScope( + val constructors: List<DFunction>, + val functions: List<DFunction>, + val properties: List<DProperty>, + val classlikes: List<DClasslike> + ) + private fun KtAnalysisSession.getDokkaScopeFrom( + namedClassOrObjectSymbol: KtNamedClassOrObjectSymbol, + dri: DRI + ): DokkaScope { + // e.g. getStaticMemberScope contains `valueOf`, `values` and `entries` members for Enum + val scope = listOf(namedClassOrObjectSymbol.getMemberScope(), namedClassOrObjectSymbol.getStaticMemberScope()).asCompositeScope() + val constructors = scope.getConstructors().map { visitConstructorSymbol(it) }.toList() + + val callables = scope.getCallableSymbols().toList() + val classifiers = scope.getClassifierSymbols().toList() + + val syntheticJavaProperties = + namedClassOrObjectSymbol.buildSelfClassType().getSyntheticJavaPropertiesScope()?.getCallableSignatures() + ?.map { it.symbol } + ?.filterIsInstance<KtSyntheticJavaPropertySymbol>() ?: emptySequence() + + fun List<KtJavaFieldSymbol>.filterOutSyntheticJavaPropBackingField() = + filterNot { javaField -> syntheticJavaProperties.any { it.hasBackingField && javaField.name == it.name } } + + val javaFields = callables.filterIsInstance<KtJavaFieldSymbol>() + .filterOutSyntheticJavaPropBackingField() + + fun List<KtFunctionSymbol>.filterOutSyntheticJavaPropAccessors() = filterNot { fn -> + if (fn.origin == KtSymbolOrigin.JAVA && fn.callableIdIfNonLocal != null) + syntheticJavaProperties.any { fn.callableIdIfNonLocal == it.javaGetterSymbol.callableIdIfNonLocal || fn.callableIdIfNonLocal == it.javaSetterSymbol?.callableIdIfNonLocal } + else false + } + + val functions = callables.filterIsInstance<KtFunctionSymbol>() + .filterOutSyntheticJavaPropAccessors().map { visitFunctionSymbol(it, dri) } + + + val properties = callables.filterIsInstance<KtPropertySymbol>().map { visitPropertySymbol(it, dri) } + + syntheticJavaProperties.map { visitPropertySymbol(it, dri) } + + javaFields.map { visitJavaFieldSymbol(it, dri) } + + + // hack, by default, compiler adds an empty companion object for enum + // TODO check if it is empty + fun List<KtNamedClassOrObjectSymbol>.filterOutEnumCompanion() = + if (namedClassOrObjectSymbol.classKind == KtClassKind.ENUM_CLASS) + filterNot { + it.name.asString() == "Companion" && it.classKind == KtClassKind.COMPANION_OBJECT + } else this + + fun List<KtNamedClassOrObjectSymbol>.filterOutAndMarkAlreadyVisited() = filterNot { symbol -> + visitedNamedClassOrObjectSymbol.contains(symbol.classIdIfNonLocal) + .also { + if (!it) symbol.classIdIfNonLocal?.let { classId -> + visitedNamedClassOrObjectSymbol.add( + classId + ) + } + } + } + + val classlikes = classifiers.filterIsInstance<KtNamedClassOrObjectSymbol>() + .filterOutEnumCompanion() // hack to filter out companion for enum + .filterOutAndMarkAlreadyVisited() + .map { visitNamedClassOrObjectSymbol(it, dri) } + + return DokkaScope( + constructors = constructors, + functions = functions, + properties = properties, + classlikes = classlikes + ) + } + + private fun KtAnalysisSession.visitEnumEntrySymbol( + enumEntrySymbol: KtEnumEntrySymbol + ): DEnumEntry = withExceptionCatcher(enumEntrySymbol) { + val dri = getDRIFromEnumEntry(enumEntrySymbol) + val isExpect = false + + val scope = enumEntrySymbol.getMemberScope() + val callables = scope.getCallableSymbols().toList() + val classifiers = scope.getClassifierSymbols().toList() + + val functions = callables.filterIsInstance<KtFunctionSymbol>().map { visitFunctionSymbol(it, dri) } + val properties = callables.filterIsInstance<KtPropertySymbol>().map { visitPropertySymbol(it, dri) } + val classlikes = + classifiers.filterIsInstance<KtNamedClassOrObjectSymbol>() + .map { visitNamedClassOrObjectSymbol(it, dri) } + + return DEnumEntry( + dri = dri, + name = enumEntrySymbol.name.asString(), + documentation = getDocumentation(enumEntrySymbol)?.toSourceSetDependent() ?: emptyMap(), + functions = functions, + properties = properties, + classlikes = classlikes, + sourceSets = setOf(sourceSet), + expectPresentInSet = sourceSet.takeIf { isExpect }, + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(enumEntrySymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + } + + private fun KtAnalysisSession.visitPropertySymbol(propertySymbol: KtPropertySymbol, parent: DRI): DProperty = + withExceptionCatcher(propertySymbol) { + val dri = createDRIWithOverridden(propertySymbol).origin + val inheritedFrom = dri.getInheritedFromDRI(parent) + val isExpect = propertySymbol.isExpect + val isActual = propertySymbol.isActual + val generics = + propertySymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DProperty( + dri = dri, + name = propertySymbol.name.asString(), + receiver = propertySymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + sources = propertySymbol.getSource(), + getter = propertySymbol.getter?.let { visitPropertyAccessor(it, propertySymbol, dri) }, + setter = propertySymbol.setter?.let { visitPropertyAccessor(it, propertySymbol, dri) }, + visibility = propertySymbol.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = getDocumentation(propertySymbol)?.toSourceSetDependent() ?: emptyMap(), // TODO + modifier = propertySymbol.modality.toDokkaModifier().toSourceSetDependent(), + type = toBoundFrom(propertySymbol.returnType), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + generics = generics, + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + propertySymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(propertySymbol)?.toSourceSetDependent()?.toAnnotations(), + propertySymbol.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) }, + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + takeUnless { propertySymbol.isVal }?.let { IsVar }, + takeIf { propertySymbol.psi is KtParameter }?.let { IsAlsoParameter(listOf(sourceSet)) } + ) + ) + } + + private fun KtAnalysisSession.visitJavaFieldSymbol( + javaFieldSymbol: KtJavaFieldSymbol, + parent: DRI + ): DProperty = + withExceptionCatcher(javaFieldSymbol) { + val dri = createDRIWithOverridden(javaFieldSymbol).origin + val inheritedFrom = dri.getInheritedFromDRI(parent) + val isExpect = false + val isActual = false + val generics = + javaFieldSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DProperty( + dri = dri, + name = javaFieldSymbol.name.asString(), + receiver = javaFieldSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + sources = javaFieldSymbol.getSource(), + getter = null, + setter = null, + visibility = javaFieldSymbol.getDokkaVisibility().toSourceSetDependent(), + documentation = getDocumentation(javaFieldSymbol)?.toSourceSetDependent() ?: emptyMap(), // TODO + modifier = javaFieldSymbol.modality.toDokkaModifier().toSourceSetDependent(), + type = toBoundFrom(javaFieldSymbol.returnType), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + generics = generics, + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + javaFieldSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(javaFieldSymbol)?.toSourceSetDependent()?.toAnnotations(), + //javaFieldSymbol.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) }, + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + // non-final java property should be var + takeUnless { javaFieldSymbol.isVal }?.let { IsVar } + ) + ) + } + + private fun KtAnalysisSession.visitPropertyAccessor( + propertyAccessorSymbol: KtPropertyAccessorSymbol, + propertySymbol: KtPropertySymbol, + propertyDRI: DRI + ): DFunction = withExceptionCatcher(propertyAccessorSymbol) { + val isGetter = propertyAccessorSymbol is KtPropertyGetterSymbol + // it also covers @JvmName annotation + val name = (if (isGetter) propertySymbol.javaGetterName else propertySymbol.javaSetterName)?.asString() ?: "" + + // SyntheticJavaProperty has callableIdIfNonLocal, propertyAccessorSymbol.origin = JAVA_SYNTHETIC_PROPERTY + // For Kotlin properties callableIdIfNonLocal=null + val dri = if (propertyAccessorSymbol.callableIdIfNonLocal != null) + getDRIFromFunctionLike(propertyAccessorSymbol) + else + propertyDRI.copy( + callable = Callable(name, null, propertyAccessorSymbol.valueParameters.map { getTypeReferenceFrom(it.returnType) }) + ) + // for SyntheticJavaProperty + val inheritedFrom = if(propertyAccessorSymbol.origin == KtSymbolOrigin.JAVA_SYNTHETIC_PROPERTY) dri.copy(callable = null) else null + + val isExpect = propertyAccessorSymbol.isExpect + val isActual = propertyAccessorSymbol.isActual + + val generics = propertyAccessorSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DFunction( + dri = dri, + name = name, + isConstructor = false, + receiver = propertyAccessorSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + parameters = propertyAccessorSymbol.valueParameters.mapIndexed { index, symbol -> + visitValueParameter(index, symbol, dri) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = propertyAccessorSymbol.getSource(), + visibility = propertyAccessorSymbol.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = generics, + documentation = getDocumentation(propertyAccessorSymbol)?.toSourceSetDependent() ?: emptyMap(), + modifier = propertyAccessorSymbol.modality.toDokkaModifier().toSourceSetDependent(), + type = toBoundFrom(propertyAccessorSymbol.returnType), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + propertyAccessorSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + getDokkaAnnotationsFrom(propertyAccessorSymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + } + + private fun KtAnalysisSession.visitConstructorSymbol( + constructorSymbol: KtConstructorSymbol + ): DFunction = withExceptionCatcher(constructorSymbol) { + val name = constructorSymbol.containingClassIdIfNonLocal?.shortClassName?.asString() + ?: throw IllegalStateException("Unknown containing class of constructor") + val dri = createDRIWithOverridden(constructorSymbol).origin + val isExpect = false // TODO + val isActual = false // TODO + + val generics = constructorSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + val documentation = getDocumentation(constructorSymbol)?.let { docNode -> + if (constructorSymbol.isPrimary) { + docNode.copy(children = (docNode.children.find { it is Constructor }?.root?.let { constructor -> + listOf(Description(constructor)) + } ?: emptyList<TagWrapper>()) + docNode.children.filterIsInstance<Param>()) + } else { + docNode + } + }?.toSourceSetDependent() + + return DFunction( + dri = dri, + name = name, + isConstructor = true, + receiver = constructorSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + parameters = constructorSymbol.valueParameters.mapIndexed { index, symbol -> + visitValueParameter(index, symbol, dri) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = constructorSymbol.getSource(), + visibility = constructorSymbol.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = generics, + documentation = documentation ?: emptyMap(), + modifier = KotlinModifier.Empty.toSourceSetDependent(), + type = toBoundFrom(constructorSymbol.returnType), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(constructorSymbol)?.toSourceSetDependent()?.toAnnotations(), + takeIf { constructorSymbol.isPrimary }?.let { PrimaryConstructorExtra } + ) + ) + } + + private fun KtAnalysisSession.visitFunctionSymbol(functionSymbol: KtFunctionSymbol, parent: DRI): DFunction = + withExceptionCatcher(functionSymbol) { + val dri = createDRIWithOverridden(functionSymbol).origin + val inheritedFrom = dri.getInheritedFromDRI(parent) + val isExpect = functionSymbol.isExpect + val isActual = functionSymbol.isActual + + val generics = + functionSymbol.typeParameters.mapIndexed { index, symbol -> + visitVariantTypeParameter( + index, + symbol, + dri + ) + } + + return DFunction( + dri = dri, + name = functionSymbol.name.asString(), + isConstructor = false, + receiver = functionSymbol.receiverParameter?.let { + visitReceiverParameter( + it, + dri + ) + }, + parameters = functionSymbol.valueParameters.mapIndexed { index, symbol -> + visitValueParameter(index, symbol, dri) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = functionSymbol.getSource(), + visibility = functionSymbol.getDokkaVisibility().toSourceSetDependent(), + generics = generics, + documentation = getDocumentation(functionSymbol)?.toSourceSetDependent() ?: emptyMap(), + modifier = functionSymbol.getDokkaModality().toSourceSetDependent(), + type = toBoundFrom(functionSymbol.returnType), + sourceSets = setOf(sourceSet), + isExpectActual = (isExpect || isActual), + extra = PropertyContainer.withAll( + inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) }, + functionSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(functionSymbol) + ?.toSourceSetDependent()?.toAnnotations(), + ObviousMember.takeIf { isObvious(functionSymbol) }, + ) + ) + } + + private fun KtAnalysisSession.visitValueParameter( + index: Int, valueParameterSymbol: KtValueParameterSymbol, dri: DRI + ) = DParameter( + dri = dri.copy(target = PointingToCallableParameters(index)), + name = valueParameterSymbol.name.asString(), + type = toBoundFrom(valueParameterSymbol.returnType), + expectPresentInSet = null, + documentation = getDocumentation(valueParameterSymbol)?.toSourceSetDependent() ?: emptyMap(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + valueParameterSymbol.additionalExtras()?.toSourceSetDependent()?.toAdditionalModifiers(), + getDokkaAnnotationsFrom(valueParameterSymbol)?.toSourceSetDependent()?.toAnnotations(), + valueParameterSymbol.getDefaultValue()?.let { DefaultValue(it.toSourceSetDependent()) } + ) + ) + + private fun KtAnalysisSession.visitReceiverParameter( + receiverParameterSymbol: KtReceiverParameterSymbol, dri: DRI + ) = DParameter( + dri = dri.copy(target = PointingToDeclaration), + name = null, + type = toBoundFrom(receiverParameterSymbol.type), + expectPresentInSet = null, + documentation = getDocumentation(receiverParameterSymbol)?.toSourceSetDependent() ?: emptyMap(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(receiverParameterSymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + private fun KtValueParameterSymbol.getDefaultValue(): Expression? = + if (origin == KtSymbolOrigin.SOURCE) (psi as? KtParameter)?.defaultValue?.toDefaultValueExpression() + else null + + private fun KtPropertySymbol.getDefaultValue(): Expression? = + (initializer?.initializerPsi as? KtConstantExpression)?.toDefaultValueExpression() // TODO consider [KtConstantInitializerValue], but should we keep an original format, e.g. 0xff or 0b101? + + private fun KtExpression.toDefaultValueExpression(): Expression? = when (node?.elementType) { + KtNodeTypes.INTEGER_CONSTANT -> PsiLiteralUtil.parseLong(node?.text)?.let { IntegerConstant(it) } + KtNodeTypes.FLOAT_CONSTANT -> if (node?.text?.toLowerCase()?.endsWith('f') == true) + PsiLiteralUtil.parseFloat(node?.text)?.let { FloatConstant(it) } + else PsiLiteralUtil.parseDouble(node?.text)?.let { DoubleConstant(it) } + + KtNodeTypes.BOOLEAN_CONSTANT -> BooleanConstant(node?.text == "true") + KtNodeTypes.STRING_TEMPLATE -> StringConstant(node.findChildByType(KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY)?.text.orEmpty()) + else -> node?.text?.let { ComplexExpression(it) } + } + + private fun KtAnalysisSession.visitVariantTypeParameter( + index: Int, + typeParameterSymbol: KtTypeParameterSymbol, + dri: DRI + ): DTypeParameter { + val upperBoundsOrNullableAny = + typeParameterSymbol.upperBounds.takeIf { it.isNotEmpty() } ?: listOf(this.builtinTypes.NULLABLE_ANY) + return DTypeParameter( + variantTypeParameter = TypeParameter( + dri = dri.copy(target = PointingToGenericParameters(index)), + name = typeParameterSymbol.name.asString(), + presentableName = typeParameterSymbol.getPresentableName() + ).wrapWithVariance(typeParameterSymbol.variance), + documentation = getDocumentation(typeParameterSymbol)?.toSourceSetDependent() ?: emptyMap(), + expectPresentInSet = null, + bounds = upperBoundsOrNullableAny.map { toBoundFrom(it) }, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(typeParameterSymbol)?.toSourceSetDependent()?.toAnnotations() + ) + ) + } + // ----------- Utils ---------------------------------------------------------------------------- + + private fun KtAnalysisSession.getDokkaAnnotationsFrom(annotated: KtAnnotated): List<Annotations.Annotation>? = + with(annotationTranslator) { getAllAnnotationsFrom(annotated) }.takeUnless { it.isEmpty() } + + private fun KtAnalysisSession.toBoundFrom(type: KtType) = + with(typeTranslator) { toBoundFrom(type) } + + /** + * `createDRI` returns the DRI of the exact element and potential DRI of an element that is overriding it + * (It can be also FAKE_OVERRIDE which is in fact just inheritance of the symbol) + * + * Looking at what PSIs do, they give the DRI of the element within the classnames where it is actually + * declared and inheritedFrom as the same DRI but truncated callable part. + * Therefore, we set callable to null and take the DRI only if it is indeed coming from different class. + */ + private fun DRI.getInheritedFromDRI(dri: DRI): DRI? { + return this.copy(callable = null) + .takeIf { dri.classNames != this.classNames || dri.packageName != this.packageName } + } + + data class DRIWithOverridden(val origin: DRI, val overridden: DRI? = null) + + private fun KtAnalysisSession.createDRIWithOverridden( + callableSymbol: KtCallableSymbol, + wasOverriddenBy: DRI? = null + ): DRIWithOverridden { + if (callableSymbol is KtPropertySymbol && callableSymbol.isOverride + || callableSymbol is KtFunctionSymbol && callableSymbol.isOverride + ) { + return DRIWithOverridden(getDRIFromSymbol(callableSymbol), wasOverriddenBy) + } + + val overriddenSymbols = callableSymbol.getAllOverriddenSymbols() + // For already + return if (overriddenSymbols.isEmpty()) { + DRIWithOverridden(getDRIFromSymbol(callableSymbol), wasOverriddenBy) + } else { + createDRIWithOverridden(overriddenSymbols.first()) + } + } + + private fun KtAnalysisSession.getDocumentation(symbol: KtSymbol) = + if (symbol.origin == KtSymbolOrigin.SOURCE_MEMBER_GENERATED) + getGeneratedKDocDocumentationFrom(symbol) + else + getKDocDocumentationFrom(symbol, logger) ?: javadocParser?.let { getJavaDocDocumentationFrom(symbol, it) } + + private fun KtAnalysisSession.isObvious(functionSymbol: KtFunctionSymbol): Boolean { + return functionSymbol.origin == KtSymbolOrigin.SOURCE_MEMBER_GENERATED && !hasGeneratedKDocDocumentation(functionSymbol) || + !functionSymbol.isOverride && functionSymbol.callableIdIfNonLocal?.classId?.isObvious() == true + } + + private fun ClassId.isObvious(): Boolean = with(asString()) { + return this == "kotlin/Any" || this == "kotlin/Enum" + || this == "java.lang/Object" || this == "java.lang/Enum" + } + + private fun KtSymbol.getSource() = KtPsiDocumentableSource(psi).toSourceSetDependent() + + private fun AncestryNode.exceptionInSupertypesOrNull(): ExceptionInSupertypes? = + typeConstructorsBeingExceptions().takeIf { it.isNotEmpty() } + ?.let { ExceptionInSupertypes(it.toSourceSetDependent()) } + + + // ----------- Translators of modifiers ---------------------------------------------------------------------------- + private fun KtSymbolWithModality.getDokkaModality() = modality.toDokkaModifier() + private fun KtSymbolWithVisibility.getDokkaVisibility() = visibility.toDokkaVisibility() + private fun KtValueParameterSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.NoInline.takeIf { isNoinline }, + ExtraModifiers.KotlinOnlyModifiers.CrossInline.takeIf { isCrossinline }, + ExtraModifiers.KotlinOnlyModifiers.VarArg.takeIf { isVararg } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtPropertyAccessorSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, +//ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { isOverride } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtPropertySymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { (this as? KtKotlinPropertySymbol)?.isConst == true }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { (this as? KtKotlinPropertySymbol)?.isLateInit == true }, + //ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + //ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + //ExtraModifiers.KotlinOnlyModifiers.Static.takeIf { isStatic }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { isOverride } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtJavaFieldSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isStatic } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtFunctionSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Infix.takeIf { isInfix }, + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, + ExtraModifiers.KotlinOnlyModifiers.Suspend.takeIf { isSuspend }, + ExtraModifiers.KotlinOnlyModifiers.Operator.takeIf { isOperator }, +//ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.TailRec.takeIf { (psi as? KtNamedFunction)?.hasModifier(KtTokens.TAILREC_KEYWORD) == true }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { isOverride } + ).toSet().takeUnless { it.isEmpty() } + + private fun KtNamedClassOrObjectSymbol.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { (this.psi as? KtClass)?.isInline() == true }, + ExtraModifiers.KotlinOnlyModifiers.Value.takeIf { (this.psi as? KtClass)?.isValue() == true }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Inner.takeIf { isInner }, + ExtraModifiers.KotlinOnlyModifiers.Data.takeIf { isData }, + ExtraModifiers.KotlinOnlyModifiers.Fun.takeIf { isFun }, + ).toSet().takeUnless { it.isEmpty() } + + private fun Modality.toDokkaModifier() = when (this) { + Modality.FINAL -> KotlinModifier.Final + Modality.SEALED -> KotlinModifier.Sealed + Modality.OPEN -> KotlinModifier.Open + Modality.ABSTRACT -> KotlinModifier.Abstract + } + + + private fun org.jetbrains.kotlin.descriptors.Visibility.toDokkaVisibility(): Visibility = when (this) { + Visibilities.Public -> KotlinVisibility.Public + Visibilities.Protected -> KotlinVisibility.Protected + Visibilities.Internal -> KotlinVisibility.Internal + Visibilities.Private, Visibilities.PrivateToThis -> KotlinVisibility.Private + JavaVisibilities.ProtectedAndPackage -> KotlinVisibility.Protected + JavaVisibilities.ProtectedStaticVisibility -> KotlinVisibility.Protected + JavaVisibilities.PackageVisibility -> JavaVisibility.Default + else -> KotlinVisibility.Public + } +} + + + + + + diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TranslatorError.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TranslatorError.kt new file mode 100644 index 00000000..8a8e2261 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TranslatorError.kt @@ -0,0 +1,29 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol +import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol + +internal class TranslatorError(message: String, cause: Throwable?) : IllegalStateException(message, cause) + +internal inline fun <R> KtAnalysisSession.withExceptionCatcher(symbol: KtSymbol, action: KtAnalysisSession.() -> R): R = + try { + action() + } catch (e: TranslatorError) { + throw e + } catch (e: Throwable) { + val file = try { + symbol.psi?.containingFile?.virtualFile?.path + } catch (e: Throwable) { + "[$e]" + } + val textRange = try { + symbol.psi?.textRange.toString() + } catch (e: Throwable) { + "[$e]" + } + throw TranslatorError( + "Error in translating of symbol (${(symbol as? KtNamedSymbol)?.name}) $symbol in file: $file, $textRange", + e + ) + }
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeReferenceFactory.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeReferenceFactory.kt new file mode 100644 index 00000000..5acfeb9e --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeReferenceFactory.kt @@ -0,0 +1,78 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.links.* +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.KtStarTypeProjection +import org.jetbrains.kotlin.analysis.api.KtTypeArgumentWithVariance +import org.jetbrains.kotlin.analysis.api.KtTypeProjection +import org.jetbrains.kotlin.analysis.api.types.* + +internal fun KtAnalysisSession.getTypeReferenceFrom(type: KtType): TypeReference = + getTypeReferenceFromPossiblyRecursive(type, emptyList()) + + +// see `deep recursive typebound #1342` test +private fun KtAnalysisSession.getTypeReferenceFromPossiblyRecursive( + type: KtType, + paramTrace: List<KtType> +): TypeReference { + if (type is KtTypeParameterType) { + // compare by symbol since, e.g. T? and T have the different KtType, but the same type parameter + paramTrace.indexOfFirst { it is KtTypeParameterType && type.symbol == it.symbol } + .takeIf { it >= 0 } + ?.let { return RecursiveType(it) } + } + + return when (type) { + is KtNonErrorClassType -> TypeConstructor( + type.classId.asFqNameString(), + type.ownTypeArguments.map { + getTypeReferenceFromTypeProjection( + it, + paramTrace + ) + } + ) + + is KtTypeParameterType -> { + val upperBoundsOrNullableAny = + type.symbol.upperBounds.takeIf { it.isNotEmpty() } ?: listOf(this.builtinTypes.NULLABLE_ANY) + + TypeParam(bounds = upperBoundsOrNullableAny.map { + getTypeReferenceFromPossiblyRecursive( + it, + paramTrace + type + ) + }) + } + + is KtClassErrorType -> TypeConstructor("$ERROR_CLASS_NAME $type", emptyList()) + is KtFlexibleType -> getTypeReferenceFromPossiblyRecursive( + type.upperBound, + paramTrace + ) + + is KtDefinitelyNotNullType -> getTypeReferenceFromPossiblyRecursive( + type.original, + paramTrace + ) + + is KtDynamicType -> TypeConstructor("[dynamic]", emptyList()) + is KtTypeErrorType -> TypeConstructor("$ERROR_CLASS_NAME $type", emptyList()) + is KtCapturedType -> throw NotImplementedError() + is KtIntegerLiteralType -> throw NotImplementedError() + is KtIntersectionType -> throw NotImplementedError() + }.let { + if (type.isMarkedNullable) Nullable(it) else it + } + +} + +private fun KtAnalysisSession.getTypeReferenceFromTypeProjection( + typeProjection: KtTypeProjection, + paramTrace: List<KtType> +): TypeReference = + when (typeProjection) { + is KtStarTypeProjection -> StarProjection + is KtTypeArgumentWithVariance -> getTypeReferenceFromPossiblyRecursive(typeProjection.type, paramTrace) + } diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeTranslator.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeTranslator.kt new file mode 100644 index 00000000..2b79498d --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/TypeTranslator.kt @@ -0,0 +1,186 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.translators + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.symbols.translators.AnnotationTranslator.Companion.getPresentableName +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.KtStarTypeProjection +import org.jetbrains.kotlin.analysis.api.KtTypeArgumentWithVariance +import org.jetbrains.kotlin.analysis.api.KtTypeProjection +import org.jetbrains.kotlin.analysis.api.annotations.* +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.analysis.api.types.* + +internal const val ERROR_CLASS_NAME = "<ERROR CLASS>" + +/** + * Maps [KtType] to Dokka [Bound] or [TypeConstructorWithKind]. + * + * Also, build [AncestryNode] tree from [KtType] + */ +internal class TypeTranslator( + private val sourceSet: DokkaConfiguration.DokkaSourceSet, + private val annotationTranslator: AnnotationTranslator +) { + + private fun <T> T.toSourceSetDependent() = if (this != null) mapOf(sourceSet to this) else emptyMap() + + private fun KtAnalysisSession.toProjection(typeProjection: KtTypeProjection): Projection = + when (typeProjection) { + is KtStarTypeProjection -> Star + is KtTypeArgumentWithVariance -> toBoundFrom(typeProjection.type).wrapWithVariance(typeProjection.variance) + } + + private fun KtAnalysisSession.toTypeConstructorFromTypeAliased(classType: KtUsualClassType): TypeAliased { + val classSymbol = classType.classSymbol + return if (classSymbol is KtTypeAliasSymbol) + TypeAliased( + typeAlias = GenericTypeConstructor( + dri = getDRIFromNonErrorClassType(classType), + projections = classType.ownTypeArguments.map { toProjection(it) }), + inner = toBoundFrom(classSymbol.expandedType), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(classType)?.toSourceSetDependent()?.toAnnotations() + ) + ) else + throw IllegalStateException("Expected type alias symbol in type") + } + + private fun KtAnalysisSession.toTypeConstructorFrom(classType: KtUsualClassType) = + GenericTypeConstructor( + dri = getDRIFromNonErrorClassType(classType), + projections = classType.ownTypeArguments.map { toProjection(it) }, + presentableName = classType.getPresentableName(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(classType)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + private fun KtAnalysisSession.toFunctionalTypeConstructorFrom(functionalType: KtFunctionalType) = + FunctionalTypeConstructor( + dri = getDRIFromNonErrorClassType(functionalType), + projections = functionalType.ownTypeArguments.map { toProjection(it) }, + isExtensionFunction = functionalType.receiverType != null, + isSuspendable = functionalType.isSuspend, + presentableName = functionalType.getPresentableName(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(functionalType)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + fun KtAnalysisSession.toBoundFrom(type: KtType): Bound = + when (type) { + is KtUsualClassType -> { + if (type.classSymbol is KtTypeAliasSymbol) toTypeConstructorFromTypeAliased(type) + else toTypeConstructorFrom(type) + } + + is KtTypeParameterType -> TypeParameter( + dri = getDRIFromTypeParameter(type.symbol), + name = type.name.asString(), + presentableName = type.getPresentableName(), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(type)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + is KtClassErrorType -> UnresolvedBound(type.toString()) + is KtFunctionalType -> toFunctionalTypeConstructorFrom(type) + is KtDynamicType -> Dynamic + is KtDefinitelyNotNullType -> DefinitelyNonNullable( + toBoundFrom(type.original) + ) + + is KtFlexibleType -> TypeAliased( + toBoundFrom(type.upperBound), + toBoundFrom(type.lowerBound), + extra = PropertyContainer.withAll( + getDokkaAnnotationsFrom(type)?.toSourceSetDependent()?.toAnnotations() + ) + ) + + is KtTypeErrorType -> UnresolvedBound(type.toString()) + is KtCapturedType -> throw NotImplementedError() + is KtIntegerLiteralType -> throw NotImplementedError() + is KtIntersectionType -> throw NotImplementedError() + }.let { + if (type.isMarkedNullable) Nullable(it) else it + } + + fun KtAnalysisSession.buildAncestryInformationFrom( + type: KtType + ): AncestryNode { + val (interfaces, superclass) = type.getDirectSuperTypes().filterNot { it.isAny } + .partition { + val typeConstructorWithKind = toTypeConstructorWithKindFrom(it) + typeConstructorWithKind.kind == KotlinClassKindTypes.INTERFACE || + typeConstructorWithKind.kind == JavaClassKindTypes.INTERFACE + } + + return AncestryNode( + typeConstructor = toTypeConstructorWithKindFrom(type).typeConstructor, + superclass = superclass.map { buildAncestryInformationFrom(it) }.singleOrNull(), + interfaces = interfaces.map { buildAncestryInformationFrom(it) } + ) + } + + internal fun KtAnalysisSession.toTypeConstructorWithKindFrom(type: KtType): TypeConstructorWithKind = when (type) { + is KtUsualClassType -> + when (val classSymbol = type.classSymbol) { + is KtNamedClassOrObjectSymbol -> TypeConstructorWithKind( + toTypeConstructorFrom(type), + classSymbol.classKind.toDokkaClassKind() + ) + + is KtAnonymousObjectSymbol -> throw NotImplementedError() + is KtTypeAliasSymbol -> toTypeConstructorWithKindFrom(classSymbol.expandedType) + } + + is KtClassErrorType -> TypeConstructorWithKind( + GenericTypeConstructor( + dri = DRI(packageName = "", classNames = "$ERROR_CLASS_NAME $type"), + projections = emptyList(), + + ), + KotlinClassKindTypes.CLASS + ) + + is KtTypeErrorType -> TypeConstructorWithKind( + GenericTypeConstructor( + dri = DRI(packageName = "", classNames = "$ERROR_CLASS_NAME $type"), + projections = emptyList(), + + ), + KotlinClassKindTypes.CLASS + ) + + is KtFunctionalType -> TypeConstructorWithKind( + toFunctionalTypeConstructorFrom(type), + KotlinClassKindTypes.CLASS + ) + + is KtDefinitelyNotNullType -> toTypeConstructorWithKindFrom(type.original) + + is KtCapturedType -> throw NotImplementedError() + is KtDynamicType -> throw NotImplementedError() + is KtFlexibleType -> throw NotImplementedError() + is KtIntegerLiteralType -> throw NotImplementedError() + is KtIntersectionType -> throw NotImplementedError() + is KtTypeParameterType -> throw NotImplementedError() + } + + private fun KtAnalysisSession.getDokkaAnnotationsFrom(annotated: KtAnnotated): List<Annotations.Annotation>? = + with(annotationTranslator) { getAllAnnotationsFrom(annotated) }.takeUnless { it.isEmpty() } + + private fun KtClassKind.toDokkaClassKind() = when (this) { + KtClassKind.CLASS -> KotlinClassKindTypes.CLASS + KtClassKind.ENUM_CLASS -> KotlinClassKindTypes.ENUM_CLASS + KtClassKind.ANNOTATION_CLASS -> KotlinClassKindTypes.ANNOTATION_CLASS + KtClassKind.OBJECT -> KotlinClassKindTypes.OBJECT + KtClassKind.COMPANION_OBJECT -> KotlinClassKindTypes.OBJECT + KtClassKind.INTERFACE -> KotlinClassKindTypes.INTERFACE + KtClassKind.ANONYMOUS_OBJECT -> KotlinClassKindTypes.OBJECT + } +}
\ No newline at end of file diff --git a/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/utils/isException.kt b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/utils/isException.kt new file mode 100644 index 00000000..9333878a --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/utils/isException.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.analysis.kotlin.symbols.utils + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.AncestryNode +import org.jetbrains.dokka.model.TypeConstructor + +internal fun AncestryNode.typeConstructorsBeingExceptions(): List<TypeConstructor> { + fun traverseSupertypes(ancestry: AncestryNode): List<TypeConstructor> = + listOf(ancestry.typeConstructor) + (ancestry.superclass?.let(::traverseSupertypes) ?: emptyList()) + + return traverseSupertypes(this).filter { type -> type.dri.isDirectlyAnException() } +} + +internal fun DRI.isDirectlyAnException(): Boolean = + toString().let { stringed -> + stringed == "kotlin/Exception///PointingToDeclaration/" || + stringed == "java.lang/Exception///PointingToDeclaration/" + } diff --git a/subprojects/analysis-kotlin-symbols/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/subprojects/analysis-kotlin-symbols/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..442f3148 --- /dev/null +++ b/subprojects/analysis-kotlin-symbols/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin |