Kotlin: attribute file-level KDoc to the compilation unit (K1/PSI)

A KDoc written before a file's `package` directive documents the file
itself and has no declaration owner: the PSI extractor's `comment.owner`
is null for it. The extractor previously emitted a "Couldn't get owner of
KDoc" diagnostic and recorded no owner in that case. The K2/FIR extractor,
by contrast, already attributes such a comment to the compilation unit,
because the KDOC node is a direct child of the FILE node in the FIR
lighter AST and the enclosing `IrFile` is discovered as its owner. The two
frontends therefore disagreed on file-level KDoc ownership.

Align the PSI extractor with the FIR one: when a KDoc has no declaration
owner but is a direct child of the `KtFile`, attribute it to the file
(`getLabel(file)` yields the compilation-unit label, the same target the
FIR extractor reaches). The now-spurious "Couldn't get owner" diagnostic
is suppressed for file-level KDoc (it remains for any genuinely ownerless
KDoc elsewhere).

This removes the last owner-attribution divergence for file-level KDoc.
The `comments` and `dataflow/func` test sources, which previously carried
frontend-specific text and a `// Diagnostic Matches` line asserting the
old warning, are unified with their K2 counterparts; the K1 expectations
lose the diagnostic and gain the compilation-unit owner row, matching K2.

Remaining `comments` divergence (owners of enum entries, init blocks and
an anonymous function) stems from those IR nodes carrying no FIR source
metadata under K2 and is addressed separately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Anders Fugmann
2026-07-14 03:28:21 +02:00
parent aeb5cab7d9
commit f495cf6fb4
4 changed files with 22 additions and 14 deletions

View File

@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtVisitor
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
@@ -95,8 +96,21 @@ class CommentExtractorPSI(
}
}
// Only storing the owner of doc comments:
val ownerPsi = getKDocOwner(comment) ?: return
val ownerPsi = getKDocOwner(comment)
if (ownerPsi == null) {
// A file-level KDoc (one written before the file's package
// directive) has no declaration owner. The K2/FIR extractor
// attributes such a comment to the compilation unit (the KDOC
// is a direct child of the FILE node there), so we do the same
// here to keep the two frontends in agreement.
if (comment.parent is KtFile) {
val fileOwnerLabel = getLabel(file)
if (fileOwnerLabel != null) {
tw.writeKtCommentOwners(commentLabel, fileOwnerLabel)
}
}
return
}
val owners = mutableListOf<IrElement>()
file.accept(IrVisitorLookup(psi2Ir, ownerPsi, file), owners)
@@ -111,7 +125,7 @@ class CommentExtractorPSI(
private fun getKDocOwner(comment: KDoc): PsiElement? {
val owner = comment.owner
if (owner == null) {
if (owner == null && comment.parent !is KtFile) {
logger.warn(
"Couldn't get owner of KDoc. The comment is extracted without an owner."
)

View File

@@ -1,5 +1,5 @@
comments
| comments.kt:1:1:1:25 | /** Kdoc with no owner */ | /** Kdoc with no owner */ |
| comments.kt:1:1:1:36 | /** Kdoc owned by CompilationUnit */ | /** Kdoc owned by CompilationUnit */ |
| comments.kt:4:1:11:3 | /**\n * A group of *members*.\n *\n * This class has no useful logic; it's just a documentation example.\n *\n * @property name the name of this group.\n * @constructor Creates an empty group.\n */ | /**\n * A group of *members*.\n *\n * This class has no useful logic; it's just a documentation example.\n *\n * @property name the name of this group.\n * @constructor Creates an empty group.\n */ |
| comments.kt:14:5:16:7 | /**\n * Members of this group.\n */ | /**\n * Members of this group.\n */ |
| comments.kt:19:5:22:7 | /**\n * Adds a [member] to this group.\n * @return the new size of the group.\n */ | /**\n * Adds a [member] to this group.\n * @return the new size of the group.\n */ |
@@ -15,8 +15,8 @@ comments
| comments.kt:71:9:73:11 | /**\n * An anonymous function comment\n */ | /**\n * An anonymous function comment\n */ |
| comments.kt:79:9:81:11 | /**\n * A local function comment\n */ | /**\n * A local function comment\n */ |
| comments.kt:88:10:90:11 | /**\n * An anonymous object comment\n */ | /**\n * An anonymous object comment\n */ |
| comments.kt:95:1:95:163 | // Diagnostic Matches: % Couldn't get owner of KDoc. The comment is extracted without an owner. ...while extracting a file (comments.kt) at %comments.kt:1:1:96:0% | // Diagnostic Matches: % Couldn't get owner of KDoc. The comment is extracted without an owner. ...while extracting a file (comments.kt) at %comments.kt:1:1:96:0% |
commentOwners
| comments.kt:1:1:1:36 | /** Kdoc owned by CompilationUnit */ | comments.kt:0:0:0:0 | comments |
| comments.kt:4:1:11:3 | /**\n * A group of *members*.\n *\n * This class has no useful logic; it's just a documentation example.\n *\n * @property name the name of this group.\n * @constructor Creates an empty group.\n */ | comments.kt:12:1:31:1 | Group |
| comments.kt:14:5:16:7 | /**\n * Members of this group.\n */ | comments.kt:17:13:17:23 | getMembers$private |
| comments.kt:14:5:16:7 | /**\n * Members of this group.\n */ | comments.kt:17:13:17:46 | members |
@@ -35,13 +35,11 @@ commentOwners
| comments.kt:88:10:90:11 | /**\n * An anonymous object comment\n */ | comments.kt:87:15:92:5 | |
| comments.kt:88:10:90:11 | /**\n * An anonymous object comment\n */ | comments.kt:87:15:92:5 | new X(...) { ... } |
commentNoOwners
| comments.kt:1:1:1:25 | /** Kdoc with no owner */ |
| comments.kt:24:9:24:25 | // A line comment |
| comments.kt:28:5:30:6 | /*\n A block comment\n */ |
| comments.kt:42:5:44:7 | /**\n * A variable.\n */ |
| comments.kt:95:1:95:163 | // Diagnostic Matches: % Couldn't get owner of KDoc. The comment is extracted without an owner. ...while extracting a file (comments.kt) at %comments.kt:1:1:96:0% |
commentSections
| comments.kt:1:1:1:25 | /** Kdoc with no owner */ | Kdoc with no owner |
| comments.kt:1:1:1:36 | /** Kdoc owned by CompilationUnit */ | Kdoc owned by CompilationUnit |
| comments.kt:4:1:11:3 | /**\n * A group of *members*.\n *\n * This class has no useful logic; it's just a documentation example.\n *\n * @property name the name of this group.\n * @constructor Creates an empty group.\n */ | A group of *members*.\n\nThis class has no useful logic; it's just a documentation example.\n\n |
| comments.kt:4:1:11:3 | /**\n * A group of *members*.\n *\n * This class has no useful logic; it's just a documentation example.\n *\n * @property name the name of this group.\n * @constructor Creates an empty group.\n */ | Creates an empty group. |
| comments.kt:4:1:11:3 | /**\n * A group of *members*.\n *\n * This class has no useful logic; it's just a documentation example.\n *\n * @property name the name of this group.\n * @constructor Creates an empty group.\n */ | the name of this group. |
@@ -69,7 +67,7 @@ commentSectionContents
| An anonymous object comment | An anonymous object comment |
| An init block comment | An init block comment |
| Creates an empty group. | Creates an empty group. |
| Kdoc with no owner | Kdoc with no owner |
| Kdoc owned by CompilationUnit | Kdoc owned by CompilationUnit |
| Medium is in the middle | Medium is in the middle |
| Members of this group. | Members of this group. |
| This is high | This is high |

View File

@@ -1,4 +1,4 @@
/** Kdoc with no owner */
/** Kdoc owned by CompilationUnit */
package foo.bar
/**
@@ -91,5 +91,3 @@ class XX {
X() {
}
}
// Diagnostic Matches: % Couldn't get owner of KDoc. The comment is extracted without an owner. ...while extracting a file (comments.kt) at %comments.kt:1:1:96:0%

View File

@@ -31,5 +31,3 @@ public fun <T> CoroutineScope.async(
): Deferred<T> {
return null!!
}
// Diagnostic Matches: % Couldn't get owner of KDoc. The comment is extracted without an owner. ...while extracting a file (kotlinx_coroutines_stubs.kt) at %kotlinx_coroutines_stubs.kt:1:1:36:0%