Skip to content

Commit 8148aa2

Browse files
authored
Fix unnecesary logging for unresolved links in module documentation (#4413)
* Reproduce the same issue, as in #4191, but in different context * Improve handling of unresolved links by adding more context information (specifically, sourceSet ID) * Use `KtFile.contextModule` to pass module information to dangling file, as in some cases, modules can have no files * Don't run modules/packages transformer on empty modules as we will filter them anyway
1 parent bf3b5ee commit 8148aa2

File tree

7 files changed

+147
-62
lines changed

7 files changed

+147
-62
lines changed

dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc
66

77
import com.intellij.psi.PsiNamedElement
8+
import org.jetbrains.dokka.DokkaConfiguration
89
import org.jetbrains.dokka.analysis.java.parsers.JavadocParser
910
import org.jetbrains.dokka.model.doc.DocumentationNode
1011
import org.jetbrains.dokka.utilities.DokkaLogger
@@ -13,7 +14,8 @@ import org.jetbrains.kotlin.analysis.api.KaSession
1314
import org.jetbrains.kotlin.analysis.api.analyze
1415
import org.jetbrains.kotlin.analysis.api.symbols.*
1516
import org.jetbrains.kotlin.analysis.api.symbols.markers.KaNamedSymbol
16-
import org.jetbrains.kotlin.psi.*
17+
import org.jetbrains.kotlin.psi.KtDeclaration
18+
import org.jetbrains.kotlin.psi.KtNonPublicApi
1719

1820
internal fun KaSession.getJavaDocDocumentationFrom(
1921
symbol: KaSymbol,
@@ -39,24 +41,28 @@ internal fun KaSession.getJavaDocDocumentationFrom(
3941
}
4042

4143
@OptIn(KaNonPublicApi::class, KtNonPublicApi::class)
42-
internal fun KaSession.getKDocDocumentationFrom(symbol: KaSymbol, logger: DokkaLogger) = (symbol as? KaDeclarationSymbol)?.findKDoc()?.let { kDocContent ->
43-
val ktElement = symbol.psi
44-
val kdocLocation = ktElement?.containingFile?.name?.let {
45-
val name = when(symbol) {
46-
is KaCallableSymbol -> symbol.callableId?.toString()
47-
is KaClassSymbol -> symbol.classId?.toString()
48-
is KaNamedSymbol -> symbol.name.asString()
49-
else -> null
50-
}?.replace('/', '.') // replace to be compatible with K1
51-
52-
if (name != null) "$it/$name"
53-
else it
44+
internal fun KaSession.getKDocDocumentationFrom(
45+
symbol: KaSymbol,
46+
logger: DokkaLogger,
47+
sourceSet: DokkaConfiguration.DokkaSourceSet,
48+
): DocumentationNode? = (symbol as? KaDeclarationSymbol)?.findKDoc()?.let { kDocContent ->
49+
val kdocSymbolName = when (symbol) {
50+
is KaCallableSymbol -> symbol.callableId?.asSingleFqName()?.asString()
51+
is KaClassSymbol -> symbol.classId?.asFqNameString()
52+
is KaNamedSymbol -> symbol.name.asString()
53+
else -> null
54+
}
55+
val kdocFileName = symbol.psi?.containingFile?.name
56+
val kdocLocation = when {
57+
kdocFileName != null && kdocSymbolName != null -> "$kdocFileName/$kdocSymbolName"
58+
kdocFileName != null -> kdocFileName
59+
kdocSymbolName != null -> kdocSymbolName
60+
else -> null
5461
}
55-
5662

5763
parseFromKDocTag(
5864
kDocTag = kDocContent.primaryTag,
59-
externalDri = { link -> resolveKDocLinkToDRI(link).ifUnresolved { logger.logUnresolvedLink(link.getLinkText(), kdocLocation) } },
65+
externalDri = { resolveKDocLink(it, kdocLocation, logger, sourceSet) },
6066
kdocLocation = kdocLocation
6167
)
6268
}

dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,69 @@
44

55
package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc
66

7+
import org.jetbrains.dokka.DokkaConfiguration
78
import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromSymbol
89
import org.jetbrains.dokka.links.DRI
910
import org.jetbrains.dokka.utilities.DokkaLogger
1011
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
1112
import org.jetbrains.kotlin.analysis.api.KaSession
1213
import org.jetbrains.kotlin.analysis.api.analyze
1314
import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule
15+
import org.jetbrains.kotlin.analysis.api.projectStructure.contextModule
1416
import org.jetbrains.kotlin.analysis.api.symbols.*
1517
import org.jetbrains.kotlin.idea.references.mainReference
1618
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
1719
import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink
1820
import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
19-
import org.jetbrains.kotlin.psi.KtFile
2021
import org.jetbrains.kotlin.psi.KtPsiFactory
2122

2223
/**
23-
* Util to print a message about unresolved [link]
24+
* Resolves a KDoc link, logging a warning in case of unresolved links.
25+
*
26+
* For the resolution logic, see [resolveKDocLinkToDRI]
27+
*/
28+
internal fun resolveKDocLink(
29+
link: KDocLink,
30+
location: String?,
31+
logger: DokkaLogger,
32+
sourceSet: DokkaConfiguration.DokkaSourceSet
33+
): DRI? {
34+
val dri = resolveKDocLinkToDRI(link)
35+
if (dri == null) {
36+
logUnresolvedLink(link.getLinkText(), location, logger, sourceSet)
37+
}
38+
return dri
39+
}
40+
41+
/**
42+
* Resolves a KDoc link from text representation, logging a warning in case of unresolved links.
43+
*
44+
* For the resolution logic, see [resolveKDocTextLinkToDRI]
2445
*/
25-
internal fun DokkaLogger.logUnresolvedLink(link: String, location: String?) {
26-
warn("Couldn't resolve link for $link" + if (location != null) " in $location" else "")
46+
internal fun KaSession.resolveKDocTextLink(
47+
link: String,
48+
contextPackageFQN: String?,
49+
location: String?,
50+
logger: DokkaLogger,
51+
sourceSet: DokkaConfiguration.DokkaSourceSet
52+
): DRI? {
53+
val dri = resolveKDocTextLinkToDRI(link, contextPackageFQN)
54+
if (dri == null) {
55+
logUnresolvedLink(link, location, logger, sourceSet)
56+
}
57+
return dri
2758
}
2859

29-
internal inline fun DRI?.ifUnresolved(action: () -> Unit): DRI? = this ?: run {
30-
action()
31-
null
60+
/**
61+
* Util to print a message about unresolved [link]
62+
*/
63+
private fun logUnresolvedLink(
64+
link: String,
65+
location: String?,
66+
logger: DokkaLogger,
67+
sourceSet: DokkaConfiguration.DokkaSourceSet
68+
) {
69+
logger.warn("Couldn't resolve link: [$link]" + (if (location != null) " in $location" else "") + " (${sourceSet.sourceSetID})")
3270
}
3371

3472
/**
@@ -64,27 +102,11 @@ internal fun KaSession.resolveKDocTextLinkToSymbol(link: String): KaSymbol? {
64102
}
65103

66104
private fun KaSession.createKDocLink(link: String, contextPackageFQN: String?): KDocLink? {
67-
/**
68-
* Creates a dangling file stored in-memory
69-
* A such file belongs to [a separate module][org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule]
70-
*
71-
* Additional information: https://kotlin.github.io/analysis-api/in-memory-file-analysis.html#stand-alone-file-analysis
72-
* @see [org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule]
73-
*/
74-
75105
val currentModule: KaSourceModule = useSiteModule as? KaSourceModule
76106
?: throw IllegalStateException("Resolving KDoc links can be done only in a source module, not $useSiteModule")
77107

78-
// To pass context (dependencies) from the current source module to a dangling module,
79-
// a random source file is selected as the context file
80-
val randomKtSourceFile: KtFile =
81-
@OptIn(KaExperimentalApi::class) currentModule.psiRoots.filterIsInstance<KtFile>().firstOrNull()
82-
?: return null
83-
84-
val psiFactory = KtPsiFactory.contextual(randomKtSourceFile)
85-
86108
// creates dummy.kt file
87-
val dummyFileText = if (contextPackageFQN != null && contextPackageFQN.isNotBlank()) """
109+
val dummyFileText = if (!contextPackageFQN.isNullOrBlank()) """
88110
package $contextPackageFQN
89111
90112
/**
@@ -97,7 +119,20 @@ private fun KaSession.createKDocLink(link: String, contextPackageFQN: String?):
97119
*/
98120
"""
99121

100-
val dummyFile = psiFactory.createFile(dummyFileText)
122+
/**
123+
* Creates a dangling file stored in-memory
124+
* Such file belongs to [a separate module][org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule]
125+
*
126+
* Additional information: https://kotlin.github.io/analysis-api/in-memory-file-analysis.html#stand-alone-file-analysis
127+
*/
128+
val dummyFile = KtPsiFactory(currentModule.project).createFile(dummyFileText)
129+
130+
// pass context (dependencies) from the current source module to a dangling module,
131+
// by default, the file will have no context,
132+
// and so will it will not be possible to resolve declarations from outside the file itself.
133+
@OptIn(KaExperimentalApi::class)
134+
dummyFile.contextModule = currentModule
135+
101136
val comments = dummyFile.children.filterIsInstance<KDoc>()
102137
val kDoc = comments.single()
103138

@@ -110,7 +145,7 @@ private fun KaSession.createKDocLink(link: String, contextPackageFQN: String?):
110145
*
111146
* @return [DRI] or null if the [kDocLink] is unresolved
112147
*/
113-
internal fun resolveKDocLinkToDRI(kDocLink: KDocLink): DRI? {
148+
private fun resolveKDocLinkToDRI(kDocLink: KDocLink): DRI? {
114149
/**
115150
* [kDocLink] can belong to [a dangling module][org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule]
116151
* or [a source module][org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule]

dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import com.intellij.psi.PsiNamedElement
88
import org.jetbrains.dokka.Platform
99
import org.jetbrains.dokka.analysis.java.doccomment.DocComment
1010
import org.jetbrains.dokka.analysis.java.parsers.DocCommentParser
11-
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.*
12-
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logUnresolvedLink
1311
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.parseFromKDocTag
14-
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocLinkToDRI
12+
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocLink
1513
import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin
1614
import org.jetbrains.dokka.model.doc.DocumentationNode
1715
import org.jetbrains.dokka.plugability.DokkaContext
@@ -42,7 +40,7 @@ internal class KotlinDocCommentParser(
4240
return analyze(kotlinAnalysis.getModule(sourceSet)) {
4341
parseFromKDocTag(
4442
kDocTag = element.comment,
45-
externalDri = { link -> resolveKDocLinkToDRI(link).ifUnresolved { context.logger.logUnresolvedLink(link.getLinkText(), elementName) } },
43+
externalDri = { resolveKDocLink(it, elementName, context.logger, sourceSet) },
4644
kdocLocation = null,
4745
parseWithChildren = parseWithChildren
4846
)

dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
package org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs
66

77
import org.jetbrains.dokka.DokkaConfiguration
8-
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.ifUnresolved
9-
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logUnresolvedLink
10-
import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.KotlinAnalysis
118
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Module
129
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Package
13-
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLinkToDRI
10+
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLink
11+
import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.KotlinAnalysis
1412
import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser
1513
import org.jetbrains.dokka.model.doc.DocumentationNode
1614
import org.jetbrains.dokka.utilities.DokkaLogger
@@ -40,21 +38,17 @@ internal fun ModuleAndPackageDocumentationParsingContext(
4038
Module -> null
4139
Package -> fragment.name
4240
}
43-
41+
val locationInformation = when (fragment.classifier) {
42+
Module -> "module documentation"
43+
Package -> "'${fragment.name}' package documentation"
44+
}
4445
MarkdownParser(
4546
externalDri = { link ->
4647
analyze(sourceModule) {
47-
resolveKDocTextLinkToDRI(
48-
link,
49-
contextPackageFQN
50-
).ifUnresolved {
51-
logger.logUnresolvedLink(link, fragment.name.ifBlank { "module documentation" })
52-
}
53-
48+
resolveKDocTextLink(link, contextPackageFQN, locationInformation, logger, sourceSet)
5449
}
5550
},
5651
sourceLocation
5752
)
58-
5953
}
6054
}

dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -916,9 +916,9 @@ internal class DokkaSymbolVisitor(
916916
private fun KaSession.getDocumentation(symbol: KaSymbol) =
917917
if (symbol.origin == KaSymbolOrigin.SOURCE_MEMBER_GENERATED)
918918
// a primary (implicit default) constructor can be generated, so we need KDoc from @constructor tag
919-
getGeneratedKDocDocumentationFrom(symbol) ?: if(symbol is KaConstructorSymbol) getKDocDocumentationFrom(symbol, logger) else null
919+
getGeneratedKDocDocumentationFrom(symbol) ?: if(symbol is KaConstructorSymbol) getKDocDocumentationFrom(symbol, logger, sourceSet) else null
920920
else
921-
getKDocDocumentationFrom(symbol, logger) ?: javadocParser?.let { getJavaDocDocumentationFrom(symbol, it) }
921+
getKDocDocumentationFrom(symbol, logger, sourceSet) ?: javadocParser?.let { getJavaDocDocumentationFrom(symbol, it) }
922922

923923
/**
924924
* Unwrap the documentation for property accessors from the [Property] wrapper if its present.

dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/DokkaBase.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ public class DokkaBase : DokkaPlugin() {
125125
}
126126

127127
public val modulesAndPackagesDocumentation: Extension<PreMergeDocumentableTransformer, *, *> by extending {
128-
preMergeDocumentableTransformer providing ::ModuleAndPackageDocumentationTransformer
128+
preMergeDocumentableTransformer providing ::ModuleAndPackageDocumentationTransformer order {
129+
after(emptyModulesFilter)
130+
}
129131
}
130132

131133
public val actualTypealiasAdder: Extension<DocumentableTransformer, *, *> by extending {

dokka-subprojects/plugin-base/src/test/kotlin/markdown/LinkTest.kt

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class LinkTest : BaseAbstractTest() {
3333
}
3434
}
3535
}
36+
3637
@Test
3738
fun linkToClassLoader() {
3839
val configuration = dokkaConfiguration {
@@ -921,6 +922,7 @@ class LinkTest : BaseAbstractTest() {
921922
}
922923
}
923924
}
925+
924926
@Test
925927
fun `link should be stable for overloads in different files`() {
926928
testInline(
@@ -1471,7 +1473,7 @@ class LinkTest : BaseAbstractTest() {
14711473
}
14721474

14731475
@Test
1474-
fun `should resolve KDoc links in package documentation`() {
1476+
fun `should resolve KDoc links in module and package documentation`() {
14751477
val configuration = dokkaConfiguration {
14761478
sourceSets {
14771479
sourceSet {
@@ -1519,6 +1521,54 @@ class LinkTest : BaseAbstractTest() {
15191521
}
15201522
}
15211523

1524+
@Test
1525+
fun `should resolve KDoc links in module and package documentation in source set without sources`() {
1526+
val configuration = dokkaConfiguration {
1527+
sourceSets {
1528+
val a = sourceSet {
1529+
name = "moduleA"
1530+
classpath = listOfNotNull(jvmStdlibPath)
1531+
sourceRoots = listOf("src/")
1532+
includes = listOf("module.md")
1533+
}
1534+
sourceSet {
1535+
name = "moduleB"
1536+
classpath = listOfNotNull(jvmStdlibPath)
1537+
includes = listOf("module.md")
1538+
dependentSourceSets = setOf(a.value.sourceSetID)
1539+
}
1540+
}
1541+
}
1542+
testInline(
1543+
"""
1544+
|/module.md
1545+
|# Module root
1546+
|
1547+
|Link to [example.Foo]
1548+
|
1549+
|# Package example
1550+
|
1551+
|Link to [example.Foo] and [Bar]
1552+
|
1553+
|/src/main/kotlin/Testing.kt
1554+
|package example
1555+
|
1556+
|class Foo
1557+
|class Bar
1558+
""".trimMargin(),
1559+
configuration
1560+
) {
1561+
documentablesMergingStage = {
1562+
// as `moduleB` has no sources, and so has no declarations it will be filtered out
1563+
// still, as we resolve links earlier in the pipeline,
1564+
// unresolved links will cause unnecessary warning messages in logs,
1565+
// while not affecting the output.
1566+
// so the only way to check that all links were resolved is to check, that there were no `logger.warn` calls
1567+
assertEquals(emptyList(), logger.warnMessages)
1568+
}
1569+
}
1570+
}
1571+
15221572
@Test
15231573
fun `should resolve KDoc links in the second line of @param tag`() {
15241574
testInline(

0 commit comments

Comments
 (0)