Repository: Kotlin/dokka (kotlin)

Issue:
[K2] Broken resolution of kdoc links in module/package documentation
Presumably, after #4167, all kdoc links in module/package documentation become just plain text.

Before ([commit](https://github.com/Kotlin/dokka/commit/6b32cd6a04a9d392be6022538a3b710f370dd5ce)):

<img width="2188" height="1004" alt="Image" src="https://github.com/user-attachments/assets/6ce7c579-1e89-43a8-9151-1f96e9a831d3" />

After ([commit](https://github.com/Kotlin/dokka/commit/e59dc2a61b1c94e1a53c27555899392377e35f3c)):

<img width="2142" height="988" alt="Image" src="https://github.com/user-attachments/assets/e31d8362-869f-4fea-83ff-2c7e221d9c98" />

**Expected behaviour**
Links should be resolved as before

Pull request:

Patch:
diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt
index c1eae7d033..26583a4e0f 100644
--- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt
+++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt
@@ -4,7 +4,6 @@
 
 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
@@ -33,25 +32,29 @@ internal inline fun DRI?.ifUnresolved(action: () -> Unit): DRI? = this ?: run {
 }
 
 /**
- * Resolves KDoc link via creating PSI.
+ * Resolves KDoc link via creating PSI in the context of a module provided by [KaSession]
+ *
  * If the [link] is ambiguous, i.e. leads to more than one declaration,
  * it returns deterministically any declaration.
  *
+ * @param contextPackageFQN FQN of a context package
  * @return [DRI] or null if the [link] is unresolved
  */
-internal fun KaSession.resolveKDocTextLinkToDRI(link: String, context: PsiElement? = null): DRI? {
-    val kDocLink = createKDocLink(link, context)
+internal fun KaSession.resolveKDocTextLinkToDRI(link: String, contextPackageFQN: String? = null): DRI? {
+    val kDocLink = createKDocLink(link, contextPackageFQN)
     return kDocLink?.let { resolveKDocLinkToDRI(it) }
 }
 
 /**
+ * Resolves KDoc link via creating PSI in the context of a module provided by [KaSession]
+ *
  * If the [link] is ambiguous, i.e. leads to more than one declaration,
  * it returns deterministically any declaration.
  *
  * @return [KaSymbol] or null if the [link] is unresolved
  */
-internal fun KaSession.resolveKDocTextLinkToSymbol(link: String, context: PsiElement? = null): KaSymbol? {
-    val kDocLink = createKDocLink(link, context)
+internal fun KaSession.resolveKDocTextLinkToSymbol(link: String): KaSymbol? {
+    val kDocLink = createKDocLink(link, null)
     return kDocLink?.let {
         /**
          *  Get [KaSession] is associated with [a dangling module][org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule]
@@ -60,7 +63,7 @@ internal fun KaSession.resolveKDocTextLinkToSymbol(link: String, context: PsiEle
     }
 }
 
-private fun KaSession.createKDocLink(link: String, context: PsiElement?): KDocLink? {
+private fun KaSession.createKDocLink(link: String, contextPackageFQN: String?): KDocLink? {
     /**
      * Creates a dangling file stored in-memory
      * A such file belongs to [a separate module][org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule]
@@ -68,35 +71,44 @@ private fun KaSession.createKDocLink(link: String, context: PsiElement?): KDocLi
      * Additional information: https://kotlin.github.io/analysis-api/in-memory-file-analysis.html#stand-alone-file-analysis
      * @see [org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule]
      */
-    val psiFactory = if (context != null) KtPsiFactory.contextual(context) else {
-        val currentModule: KaSourceModule = useSiteModule as? KaSourceModule
-            ?: throw IllegalStateException("Resolving KDoc links can be done only in a source module, not $useSiteModule")
 
-        // To pass context (dependencies) from the current source module to a dangling module,
-        // a random source file is selected as the context file
-        val randomKtSourceFile: KtFile =
-            @OptIn(KaExperimentalApi::class) currentModule.psiRoots.filterIsInstance<KtFile>().firstOrNull()
-                ?: return null
+    val currentModule: KaSourceModule = useSiteModule as? KaSourceModule
+        ?: throw IllegalStateException("Resolving KDoc links can be done only in a source module, not $useSiteModule")
 
-        KtPsiFactory.contextual(randomKtSourceFile)
-    }
+    // To pass context (dependencies) from the current source module to a dangling module,
+    // a random source file is selected as the context file
+    val randomKtSourceFile: KtFile =
+        @OptIn(KaExperimentalApi::class) currentModule.psiRoots.filterIsInstance<KtFile>().firstOrNull()
+            ?: return null
 
-    val kDoc = psiFactory.createComment(
-        """
+    val psiFactory = KtPsiFactory.contextual(randomKtSourceFile)
+
+    // creates dummy.kt file
+    val dummyFileText = if (contextPackageFQN != null && contextPackageFQN.isNotBlank()) """
+    package $contextPackageFQN
+    
     /**
     * [$link]
     */
     """.trimIndent()
-    ) as? KDoc
+    else """
+    /**
+    * [$link]
+    */
+    """
+
+    val dummyFile = psiFactory.createFile(dummyFileText)
+    val comments = dummyFile.children.filterIsInstance<KDoc>()
+    val kDoc = comments.single()
 
-    return kDoc?.getDefaultSection()?.children?.filterIsInstance<KDocLink>()?.singleOrNull()
+    return kDoc.getDefaultSection().children.filterIsInstance<KDocLink>().singleOrNull()
 }
 
 /**
- * If the [link] is ambiguous, i.e. leads to more than one declaration,
+ * If the [kDocLink] is ambiguous, i.e. leads to more than one declaration,
  * it returns deterministically any declaration.
  *
- * @return [DRI] or null if the [link] is unresolved
+ * @return [DRI] or null if the [kDocLink] is unresolved
  */
 internal fun resolveKDocLinkToDRI(kDocLink: KDocLink): DRI? {
     /**
diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt
index 94972a3214..b3ae485c56 100644
--- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt
+++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt
@@ -15,7 +15,6 @@ 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
@@ -37,19 +36,17 @@ internal fun ModuleAndPackageDocumentationParsingContext(
         MarkdownParser(externalDri = { null }, sourceLocation)
     } else {
         val sourceModule = kotlinAnalysis.getModule(sourceSet)
-        val contextPsi = analyze(sourceModule) {
-            val contextSymbol = when (fragment.classifier) {
-                Module -> rootPackageSymbol
-                Package -> findPackage(FqName(fragment.name))
-            }
-            contextSymbol?.psi
+        val contextPackageFQN = when (fragment.classifier) {
+            Module -> null
+            Package -> fragment.name
         }
+
         MarkdownParser(
             externalDri = { link ->
                 analyze(sourceModule) {
                     resolveKDocTextLinkToDRI(
                         link,
-                        contextPsi
+                        contextPackageFQN
                     ).ifUnresolved {
                         logger.logUnresolvedLink(link, fragment.name.ifBlank { "module documentation" })
                     }


Test patch:
diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt
index b2ad551a03..892c3c988d 100644
--- a/dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt
+++ b/dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt
@@ -1204,6 +1204,55 @@ class LinkTest : BaseAbstractTest() {
         }
     }
 
+    @Test
+    fun `should resolve KDoc links in package documentation`() {
+        val configuration = dokkaConfiguration {
+            sourceSets {
+                sourceSet {
+                    name = "moduleA"
+                    sourceRoots = listOf("src/")
+                    classpath = listOfNotNull(jvmStdlibPath)
+                    includes = listOf("module.md")
+                }
+            }
+        }
+        testInline(
+            """
+            |/module.md
+            |# Module root
+            |
+            |Link to [example.Foo]
+            |
+            |# Package example
+            |
+            |Link to [example.Foo] and [Bar]
+            |
+            |/src/main/kotlin/Testing.kt
+            |package example
+            |
+            |class Foo
+            |class Bar
+        """.trimMargin(),
+            configuration
+        ) {
+            documentablesMergingStage = { module ->
+                assertEquals(
+                    listOf(
+                        "example.Foo" to DRI("example", "Foo"),
+                    ),
+                    module.getAllLinkDRIFrom("root")
+                )
+                assertEquals(
+                    listOf(
+                        "example.Foo" to DRI("example", "Foo"),
+                        "Bar" to DRI("example", "Bar"),
+                    ),
+                    module.getAllLinkDRIFrom("example")
+                )
+            }
+        }
+    }
+
     private fun DModule.getLinkDRIFrom(name: String): DRI? {
         val doc = this.dfs { it.name == name }?.documentation?.values?.single()
             ?: throw IllegalStateException("Can't find documentation for declaration '$name'")