From 2121d74eb8bb426e28e2c5a86a6b63d264e4396d Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Sun, 12 Jul 2026 17:47:06 +0200 Subject: [PATCH] Kotlin: start constructor body block at the `constructor` keyword When a constructor carries leading modifiers or annotations, for example `internal constructor(...) { }` or an annotated `@Ann constructor()`, the K1 frontend's IrBlockBody offsets for the constructor body begin at the modifier/annotation (`3:3`); the K2 frontend begins at the `constructor` keyword (`3:12`), consistent with how declarations exclude leading modifiers from their own span. We converge K1 onto the K2 form. The K2 span cannot be reconstructed from raw offsets under K1, but the `constructor` keyword position is available from the PSI. A new helper getPsiBasedConstructorBodyLocation walks from the block body's start to the enclosing KtConstructor and returns a location from its `constructor` keyword through the block body's own end offset; extractBlockBody uses it in preference to the raw block location. Guards keep the change surgical: - returns null under K2 (getKtFile unavailable; raw offsets already exclude the modifier), leaving K2 untouched. - returns null for non-constructor bodies and for an implicit primary constructor with no `constructor` keyword. - for a constructor without modifiers the keyword coincides with the block start, so the location is unchanged there. Relearned both suites: only modifier/annotation-carrying constructor body-block rows change (K1 now matches K2). internal-constructor-called-from-java/test.expected becomes byte-identical across suites; the residual annotation_classes/PrintAst diffs are pre-existing, unrelated divergences (annotation-argument and stdlib enum-entry locations). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 31 ++++++++++++++++++- .../annotation_classes/PrintAst.expected | 2 +- .../test.expected | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index 864acf81931..679d04fc4dd 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -55,6 +55,7 @@ import org.jetbrains.kotlin.types.Variance import org.jetbrains.kotlin.util.OperatorNameConventions import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtConstructor import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtVariableDeclaration @@ -2937,9 +2938,37 @@ open class KotlinFileExtractor( tw.writeHasLocation(it, locId) } + /** + * Returns a PSI-based location for a constructor body block that starts at the + * `constructor` keyword rather than at any leading modifiers. + * + * For a constructor declared with a visibility modifier (for example + * `internal constructor(...) { }`), the K1 frontend's [IrBlockBody] offsets start + * at the modifier (`3:3`); the K2 frontend starts at the `constructor` keyword + * (`3:12`), consistent with how declarations exclude leading modifiers from their + * own span. We converge K1 onto the K2 form. + * + * The end offset is left as the block body's own [IrBlockBody.endOffset]. For a + * constructor without modifiers the keyword already coincides with the block start, + * so this is a no-op there. Returns null under K2 (where [getKtFile] is unavailable + * and the raw offsets already exclude the modifier), for non-constructor bodies, and + * when the constructor has no `constructor` keyword (an implicit primary + * constructor). + */ + private fun getPsiBasedConstructorBodyLocation(b: IrBlockBody): Label? { + if (b.startOffset < 0 || b.endOffset < 0) return null + val file = currentIrFile ?: return null + val ktCtor = getPsi2Ir()?.getKtFile(file) + ?.findElementAt(b.startOffset) + ?.let { leaf -> generateSequence(leaf) { it.parent }.filterIsInstance>().firstOrNull() } + ?: return null + val keyword = ktCtor.getConstructorKeyword() ?: return null + return tw.getLocation(keyword.startOffset, b.endOffset) + } + private fun extractBlockBody(b: IrBlockBody, callable: Label) { with("block body", b) { - extractBlockBody(callable, tw.getLocation(b)).also { + extractBlockBody(callable, getPsiBasedConstructorBodyLocation(b) ?: tw.getLocation(b)).also { for ((sIdx, stmt) in b.statements.withIndex()) { extractStatement(stmt, callable, it, sIdx) } diff --git a/java/ql/test-kotlin1/library-tests/annotation_classes/PrintAst.expected b/java/ql/test-kotlin1/library-tests/annotation_classes/PrintAst.expected index 1f4b1f2011e..286c0cb18ce 100644 --- a/java/ql/test-kotlin1/library-tests/annotation_classes/PrintAst.expected +++ b/java/ql/test-kotlin1/library-tests/annotation_classes/PrintAst.expected @@ -223,7 +223,7 @@ def.kt: #-----| 1: (Annotations) # 41| 1: [Annotation] Annot0k # 0| 1: [IntegerLiteral] 0 -# 41| 5: [BlockStmt] { ... } +# 42| 5: [BlockStmt] { ... } # 42| 0: [SuperConstructorInvocationStmt] super(...) # 42| 1: [BlockStmt] { ... } use.java: diff --git a/java/ql/test-kotlin1/library-tests/internal-constructor-called-from-java/test.expected b/java/ql/test-kotlin1/library-tests/internal-constructor-called-from-java/test.expected index 44dbca7993e..c3eb1d9284f 100644 --- a/java/ql/test-kotlin1/library-tests/internal-constructor-called-from-java/test.expected +++ b/java/ql/test-kotlin1/library-tests/internal-constructor-called-from-java/test.expected @@ -1 +1 @@ -| User.java:3:31:3:44 | new Test(...) | test.kt:3:3:3:51 | { ... } | +| User.java:3:31:3:44 | new Test(...) | test.kt:3:12:3:51 | { ... } |