Skip to content

Commit 70e5761

Browse files
Kotlin: attribute enum-entry and init-block KDoc owners under K2/FIR
Under the K2/FIR frontend the lighter-AST comment extractor finds a KDoc's owner by walking the IR and, for each element that carries FIR metadata, checking whether that metadata's source node has a KDOC child. Enum entries (`IrEnumEntry`) and anonymous initializers (`IrAnonymousInitializer`) carry no FIR metadata, so the search could never reach them: their KDoc comments were recorded with no owner, whereas the PSI frontend attributes them (an enum entry to itself, an init block to its enclosing class). These IR declarations do retain valid source offsets, however. This adds a supplementary pass that walks the file's lighter AST, finds the `ENUM_ENTRY` and `CLASS_INITIALIZER` nodes that carry a KDOC child, and matches each to the metadata-less IR declaration whose source offset lies within the node's range. Matching is fail-closed: a node is only attributed when exactly one candidate declaration falls inside it, so ambiguous or unexpected shapes leave the comment ownerless rather than guessing. Offset containment is used rather than per-kind heuristics (matching enum entries by name, initializers by order) because it is uniform across both node kinds and does not depend on names being unique or declaration order being stable. Only the K2 expectations change: the `test-kotlin2` `comments` enum-entry and init-block KDoc owners now match `test-kotlin1` exactly. The one remaining owner difference in that test is a KDoc on an anonymous function passed as a call argument, which the PSI frontend attributes to the enclosing property via its owner walk-up and the FIR frontend does not; that discrepancy is left as-is (see the updated TODO). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f495cf6 commit 70e5761

2 files changed

Lines changed: 79 additions & 8 deletions

File tree

java/kotlin-extractor/src/main/kotlin/utils/versions/v_1_9_0-Beta/CommentExtractorLighterAST.kt

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
1313
import org.jetbrains.kotlin.ir.visitors.acceptVoid
1414
import org.jetbrains.kotlin.kdoc.lexer.KDocTokens
1515
import org.jetbrains.kotlin.lexer.KtTokens
16+
import org.jetbrains.kotlin.KtNodeTypes
1617
import org.jetbrains.kotlin.util.getChildren
1718

18-
// TODO: This doesn't give owners to as many comments as the PSI extractor does.
19-
// See the library-tests/comments tests for details.
19+
// TODO: This still doesn't give owners to quite as many comments as the PSI
20+
// extractor does (e.g. a KDoc on an anonymous function passed as a call
21+
// argument). See the library-tests/comments tests for details.
2022

2123
class CommentExtractorLighterAST(
2224
fileExtractor: KotlinFileExtractor,
@@ -33,6 +35,7 @@ class CommentExtractorLighterAST(
3335
}
3436

3537
val owners = findKDocOwners(file)
38+
addMetadataLessKDocOwners(treeStructure, file, owners)
3639
extractComments(treeStructure.root, treeStructure, owners)
3740
return true
3841
}
@@ -71,7 +74,7 @@ class CommentExtractorLighterAST(
7174
}
7275
}
7376

74-
private fun findKDocOwners(file: IrFile): Map<Int, List<IrElement>> {
77+
private fun findKDocOwners(file: IrFile): MutableMap<Int, MutableList<IrElement>> {
7578
fun LighterASTNode.isKDocComment() = this.tokenType == KDocTokens.KDOC
7679

7780
val kDocOwners = mutableMapOf<Int, MutableList<IrElement>>()
@@ -110,10 +113,75 @@ class CommentExtractorLighterAST(
110113
return kDocOwners
111114
}
112115

116+
// Enum entries and anonymous initializers carry no FIR metadata under the
117+
// K2/FIR frontend, so the metadata-based search in `findKDocOwners` cannot
118+
// reach them and their KDoc comments would be left without an owner (the PSI
119+
// frontend does attribute them). They do, however, retain valid IR source
120+
// offsets. We recover their ownership by finding the ENUM_ENTRY /
121+
// CLASS_INITIALIZER lighter-AST nodes that carry a KDOC child and matching
122+
// each to the metadata-less IR declaration whose source offset falls within
123+
// the node's range. Matching is fail-closed: a node is only attributed when
124+
// exactly one candidate declaration lies within it.
125+
private fun addMetadataLessKDocOwners(
126+
treeStructure: FlyweightCapableTreeStructure<LighterASTNode>,
127+
file: IrFile,
128+
owners: MutableMap<Int, MutableList<IrElement>>
129+
) {
130+
val candidates = mutableListOf<IrDeclaration>()
131+
file.acceptVoid(
132+
object : IrVisitorVoid() {
133+
override fun visitElement(element: IrElement) {
134+
if (element is IrEnumEntry || element is IrAnonymousInitializer) {
135+
val decl = element as IrDeclaration
136+
if (
137+
decl.startOffset != UNDEFINED_OFFSET &&
138+
decl.startOffset != SYNTHETIC_OFFSET
139+
) {
140+
candidates.add(decl)
141+
}
142+
}
143+
element.acceptChildrenVoid(this)
144+
}
145+
}
146+
)
147+
if (candidates.isEmpty()) {
148+
return
149+
}
150+
151+
fun visit(node: LighterASTNode) {
152+
if (
153+
node.tokenType == KtNodeTypes.ENUM_ENTRY ||
154+
node.tokenType == KtNodeTypes.CLASS_INITIALIZER
155+
) {
156+
node.getChildren(treeStructure)
157+
.firstOrNull { it.tokenType == KDocTokens.KDOC }
158+
?.let { kDoc ->
159+
val startOffset = kDoc.startOffset
160+
if (
161+
startOffset != UNDEFINED_OFFSET && startOffset != SYNTHETIC_OFFSET
162+
) {
163+
val matches =
164+
candidates.filter {
165+
it.startOffset >= node.startOffset &&
166+
it.startOffset < node.endOffset
167+
}
168+
if (matches.size == 1) {
169+
owners
170+
.getOrPut(startOffset, { mutableListOf<IrElement>() })
171+
.add(matches[0])
172+
}
173+
}
174+
}
175+
}
176+
node.getChildren(treeStructure).forEach { visit(it) }
177+
}
178+
visit(treeStructure.root)
179+
}
180+
113181
private fun extractComments(
114182
node: LighterASTNode,
115183
treeStructure: FlyweightCapableTreeStructure<LighterASTNode>,
116-
owners: Map<Int, List<IrElement>>
184+
owners: MutableMap<Int, MutableList<IrElement>>
117185
) {
118186
node.getChildren(treeStructure).forEach {
119187
if (KtTokens.COMMENTS.contains(it.tokenType)) {
@@ -124,7 +192,10 @@ class CommentExtractorLighterAST(
124192
}
125193
}
126194

127-
private fun extractComment(comment: LighterASTNode, owners: Map<Int, List<IrElement>>) {
195+
private fun extractComment(
196+
comment: LighterASTNode,
197+
owners: MutableMap<Int, MutableList<IrElement>>
198+
) {
128199
val type: CommentType =
129200
when (comment.tokenType) {
130201
KtTokens.EOL_COMMENT -> {

java/ql/test-kotlin2/library-tests/comments/comments.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ commentOwners
2222
| comments.kt:14:5:16:7 | /**\n * Members of this group.\n */ | comments.kt:17:13:17:46 | members |
2323
| comments.kt:14:5:16:7 | /**\n * Members of this group.\n */ | comments.kt:17:13:17:46 | members |
2424
| comments.kt:19:5:22:7 | /**\n * Adds a [member] to this group.\n * @return the new size of the group.\n */ | comments.kt:23:5:26:5 | add |
25+
| comments.kt:35:5:35:34 | /** Medium is in the middle */ | comments.kt:36:5:36:14 | Medium |
26+
| comments.kt:37:5:37:23 | /** This is high */ | comments.kt:38:5:38:11 | High |
2527
| comments.kt:48:1:50:3 | /**\n * A type alias comment\n */ | comments.kt:51:1:51:24 | MyType |
28+
| comments.kt:54:5:56:7 | /**\n * An init block comment\n */ | comments.kt:53:1:58:1 | InitBlock |
2629
| comments.kt:61:5:63:7 | /**\n * A prop comment\n */ | comments.kt:64:5:68:17 | prop |
2730
| comments.kt:65:9:67:11 | /**\n * An accessor comment\n */ | comments.kt:68:9:68:17 | getProp |
2831
| comments.kt:79:9:81:11 | /**\n * A local function comment\n */ | comments.kt:82:9:82:24 | localFn |
@@ -31,10 +34,7 @@ commentOwners
3134
commentNoOwners
3235
| comments.kt:24:9:24:25 | // A line comment |
3336
| comments.kt:28:5:30:6 | /*\n A block comment\n */ |
34-
| comments.kt:35:5:35:34 | /** Medium is in the middle */ |
35-
| comments.kt:37:5:37:23 | /** This is high */ |
3637
| comments.kt:42:5:44:7 | /**\n * A variable.\n */ |
37-
| comments.kt:54:5:56:7 | /**\n * An init block comment\n */ |
3838
| comments.kt:71:9:73:11 | /**\n * An anonymous function comment\n */ |
3939
commentSections
4040
| comments.kt:1:1:1:36 | /** Kdoc owned by CompilationUnit */ | Kdoc owned by CompilationUnit |

0 commit comments

Comments
 (0)