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 0000000000..3ece2e8d61 --- /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("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("descriptorsTest") { + classpath += descriptorsTestConfiguration +} + +tasks.check { + dependsOn(symbolsTest) +} + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a09f141300..9170c40b91 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 4dfc972ddb..545f8435b1 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 ef4f9f7b8c..20c3b72748 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 8bea63e8d2..2f9f5863eb 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 = 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 9c4435678a..c70c1b0ba4 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 46239baab6..82159e0d9d 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 961ce5f573..c25c1a1bd8 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 14a3661114..17bd209e3e 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 e505907376..2994fb42d4 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 e74cb49d33..df5efd033b 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 79c1e1ad56..82a04b89a6 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 0cf94c186e..e57e6a8ccf 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 efab9aba54..6423655a64 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 f4ef85b90b..af77d61cd6 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 be75e01f49..ccf62641f8 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 1487583208..9ba428d649 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 526ff0eb3f..8ee5a20d8c 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 41b086eea8..e10a226062 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 807ede7818..e358945e91 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 e28b442fb9..b54f8deb91 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 , 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 fa65a477d2..3410f9ef25 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 f57c3c8c77..a0605a5eb5 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 5fe17fc820..6f08c89d6b 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 d6564343de..35997681c5 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 e704bf7124..0abf504eb0 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 f4615216cf..4b376c73da 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 5e2560bf2d..fb2c53cd49 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 ca2872163f..af10cbee9f 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 c9787b67be..588b3d50ab 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 0767b2df77..4cd9a94de1 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 38ae2be332..00d981022e 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 a189894cc0..14c2752a64 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 9c1265a6ac..5c7124cdc5 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 9cde40a57e..826df64e28 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 39d725bb65..e6cc393d88 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 = 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 85db2d2757..134e7f596b 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 3618c8fbe9..ffb5c9c852 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 469c1a1eb9..532c9e81a8 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 ba00cf158c..dd888ad610 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 a8c9e3420a..3e1b403d27 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 57165fd1b9..4ae8f7aca9 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 a763cbd28e..b3f83d79c7 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 a9c865b45d..c4087b20eb 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 7fc6b7fae4..a357491fb5 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 ba0d95d52a..1e3d784ae4 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 2c1173c02c..8bc307dff3 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 0000000000..181d1f1d08 --- /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 4c5724504f..1d10d83883 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 2320ca6951..d3e2cba2c5 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 404249f818..6b4d444f47 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 ed58eb56c4..eb2058d82d 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 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() val psiField = firstIsInstanceOrNull() val classes = filterIsInstance().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 e69de29bb2..4bddfcf1df 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 ()V +} + +public class org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider : org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider { + public fun (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 (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 c000df5871..9fddcde185 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 39870f571c..0000000000 --- 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 ()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 876d87ca7b..0000000000 --- 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 4a39e0d81e..0000000000 --- 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 47163f6e21..0000000000 --- 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 9be46c0b5f..0000000000 --- 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 ()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 876d87ca7b..0000000000 --- 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 33f555cb07..0000000000 --- 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 592455784a..0000000000 --- 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 0000000000..c1b8651a3e --- /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 +) + +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 = Foo("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 { + return getChildrenOfType() + .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() + if (containerKDoc == null || subjectName == null) return null + val propertySection = containerKDoc.findSectionByTag(KDocKnownTag.PROPERTY, subjectName) + val paramTag = + containerKDoc.findDescendantOfType { it.knownTag == KDocKnownTag.PARAM && it.getSubjectName() == subjectName } + + val primaryContent = when { + // class Foo(val s: String) + this is KtParameter && this.isPropertyParameter() -> propertySection ?: paramTag + // fun some(f: String) || class Some<T: Base> || Foo(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 0000000000..ec23b0c8a4 --- /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().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 = + kDocImpl.children.filterIsInstance().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 0000000000..9d26d91705 --- /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()?.singleOrNull() + return kDocLink?.let { resolveKDocLink(it) } +} + +internal fun KtAnalysisSession.resolveKDocLink(link: KDocLink): DRI? { + val lastNameSegment = link.children.filterIsInstance().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 0000000000..9b108f8093 --- /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 0000000000..a36516030e --- /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 { + 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 0000000000..399d005ae3 --- /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 0000000000..385b5328bc --- /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 = 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 { + 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 { + val exceptionName = tag.exceptionQualifiedName ?: return resolveGeneric(tag) + + return comment.children + .filterIsInstance() + .filter { it.name == tag.name && it.getSubjectName() == exceptionName } + .map { DescriptorDocumentationContent(resolveDocContext, it, tag) } + } + + private fun resolveGeneric(tag: JavadocTag): List { + 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 0000000000..c8ecc786fb --- /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().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 0000000000..6d2bfff4f3 --- /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().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 """""" + } +} 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 0000000000..6e3f215acb --- /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 0000000000..46f714cb95 --- /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 0000000000..1eafc6880f --- /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 0000000000..cf627e77f9 --- /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 0000000000..82259b8989 --- /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().querySingle { kotlinAnalysis } + + private val documentationFragments: SourceSetDependent> = + context.configuration.sourceSets.associateWith { sourceSet -> + sourceSet.includes.flatMap { include -> parseModuleAndPackageDocumentationFragments(include) } + } + + private fun findDocumentationNodes( + sourceSets: Set, + predicate: (ModuleAndPackageDocumentationFragment) -> Boolean + ): SourceSetDependent { + 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 { + return findDocumentationNodes(module.sourceSets) { fragment -> + fragment.classifier == Classifier.Module && (fragment.name == module.name) + } + } + + override fun read(pkg: DPackage): SourceSetDependent { + 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.mergeDocumentationNodes(): List = + 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 0000000000..d7877559da --- /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 0000000000..b92adf4a68 --- /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 0000000000..ae728a2838 --- /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 { + return parseModuleAndPackageDocumentationFragments(ModuleAndPackageDocumentationFile(source)) +} + +internal fun parseModuleAndPackageDocumentationFragments( + source: ModuleAndPackageDocumentationSource +): List { + 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 0000000000..eb7c5d70c1 --- /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, + 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, + 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, + 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, + sourceRoots: Set, + 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, + 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 0000000000..8b9e552d12 --- /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, + sourceRoots: Set, + analysisPlatform: Platform, + languageVersion: String?, + apiVersion: String?, + applicationDisposable: Disposable, + projectDisposable: Disposable +): Pair { + + 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 = getPsiFilesFromPaths(project, javaFilePath) + val ktFiles: List = getPsiFilesFromPaths(project, getSourceFilePaths(ktFilePath)) + addSourceRoots(ktFiles + javaFiles) + contentScope = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, ktFiles) + platform = targetPlatform + moduleName = "" + 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 +) { + // 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() { + 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, + includeDirectoryRoot: Boolean = false, +): Set { + val result = mutableSetOf() + 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 getPsiFilesFromPaths( + project: Project, + paths: Collection, +): List { + val fs = StandardFileSystems.local() + val psiManager = PsiManager.getInstance(project) + val result = mutableListOf() + 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 0000000000..71fdfadb0c --- /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() + + 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() } + + 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().documentableSourceLanguageParser providing { KotlinDocumentableSourceLanguageParser() } + } + internal val symbolFullClassHierarchyBuilder by extending { + plugin().fullClassHierarchyBuilder providing { SymbolFullClassHierarchyBuilder() } + } + + internal val symbolSyntheticDocumentableDetector by extending { + plugin().syntheticDocumentableDetector providing { SymbolSyntheticDocumentableDetector() } + } + + internal val moduleAndPackageDocumentationReader by extending { + plugin().moduleAndPackageDocumentationReader providing ::ModuleAndPackageDocumentationReader + } + + /* internal val kotlinToJavaMapper by extending { + plugin().kotlinToJavaService providing { DescriptorKotlinToJavaMapper() } + } + + intern val descriptorInheritanceBuilder by extending { + plugin().inheritanceBuilder providing { DescriptorInheritanceBuilder() } + } + */ + internal val symbolExternalDocumentablesProvider by extending { + plugin().externalDocumentablesProvider providing ::SymbolExternalDocumentablesProvider + } + + internal val kotlinSampleProviderFactory by extending { + plugin().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 0000000000..47bcb15626 --- /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().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 0000000000..43aceccd98 --- /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 { + 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 0000000000..799cd7b649 --- /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 0000000000..dc61b35b5e --- /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().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 0000000000..3a8199315a --- /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 0000000000..c328fe5760 --- /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().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 0000000000..d4269b119a --- /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>() } + module.packages.forEach { visitDocumentable(it, map) } + return map + } + + private fun KtAnalysisSession.collectSupertypesFromKtType( + driWithKType: Pair, + supersMap: MutableMap + ) { + 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, + supersMap: MutableMap + ) { + 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>> + ) { + 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 0000000000..20d2508b4f --- /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)?.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 0000000000..02198518fb --- /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 { + 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 0000000000..ed5ed532e1 --- /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): 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 0000000000..0c79b8a0e6 --- /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().querySingle { kotlinAnalysis } + private val javadocParser = JavadocParser( + docCommentParsers = context.plugin().query { docCommentParsers }, + docCommentFinder = context.plugin().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.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 = + mutableSetOf() + + private fun 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 Collection.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 = getPsiFilesFromPaths( + analysisContext.project, + getSourceFilePaths(sourceSet.sourceRoots.map { it.canonicalPath }) + ) + val processedPackages: MutableSet = mutableSetOf() + return analyze(analysisContext.mainModule) { + val packageSymbols: List = 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().map { visitFunctionSymbol(it, dri) } + val properties = callables.filterIsInstance().map { visitPropertySymbol(it, dri) } + val classlikes = + classifiers.filterIsInstance() + .map { visitNamedClassOrObjectSymbol(it, dri) } + val typealiases = classifiers.filterIsInstance().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, + val functions: List, + val properties: List, + val classlikes: List + ) + 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() ?: emptySequence() + + fun List.filterOutSyntheticJavaPropBackingField() = + filterNot { javaField -> syntheticJavaProperties.any { it.hasBackingField && javaField.name == it.name } } + + val javaFields = callables.filterIsInstance() + .filterOutSyntheticJavaPropBackingField() + + fun List.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() + .filterOutSyntheticJavaPropAccessors().map { visitFunctionSymbol(it, dri) } + + + val properties = callables.filterIsInstance().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.filterOutEnumCompanion() = + if (namedClassOrObjectSymbol.classKind == KtClassKind.ENUM_CLASS) + filterNot { + it.name.asString() == "Companion" && it.classKind == KtClassKind.COMPANION_OBJECT + } else this + + fun List.filterOutAndMarkAlreadyVisited() = filterNot { symbol -> + visitedNamedClassOrObjectSymbol.contains(symbol.classIdIfNonLocal) + .also { + if (!it) symbol.classIdIfNonLocal?.let { classId -> + visitedNamedClassOrObjectSymbol.add( + classId + ) + } + } + } + + val classlikes = classifiers.filterIsInstance() + .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().map { visitFunctionSymbol(it, dri) } + val properties = callables.filterIsInstance().map { visitPropertySymbol(it, dri) } + val classlikes = + classifiers.filterIsInstance() + .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()) + docNode.children.filterIsInstance()) + } 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? = + 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 0000000000..8a8e2261d2 --- /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 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 0000000000..5acfeb9e42 --- /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 +): 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 +): 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 0000000000..2b79498d5f --- /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 = "" + +/** + * 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.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? = + 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 0000000000..9333878a82 --- /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 { + fun traverseSupertypes(ancestry: AncestryNode): List = + 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 0000000000..442f31488b --- /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