From 240103e7b3aadbd5deb6897b074c7fa35eb1626f Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Sat, 11 Jul 2026 10:12:41 +0200 Subject: [PATCH] Kotlin: converge implicit primary constructor locations onto the K2-native class span The K1 and K2 frontends record different source locations for the compiler-synthesised primary constructor of a class that declares no primary constructor in source. K2 uses the class declaration's raw IR offsets, which include any leading modifier keywords, while K1's raw offsets start at the `class` keyword and omit the modifiers. Since K1 (unlike K2) retains the PSI, we recover the modifier-inclusive span from the enclosing KtClassOrObject so K1 matches the K2-native span. Example, for `open class C0 {}` on line 11 (the `open` modifier is at column 1, the `class` keyword at column 6): before (K1): generics.kt:11:6:11:19 | C0 | C0() after (K1): generics.kt:11:1:11:19 | C0 | C0() (matches K2) The fix is deliberately narrow and leaves all other constructors untouched: - explicit primary constructors (`class C1(val t: T)`, `class C2()`) keep their own parameter-list location, which both frontends already agree on; - explicit secondary constructors keep their own location (only the `isPrimary` constructor is adjusted); - specialised/parameterised copies of a generic constructor (`typeSubstitution != null`) are excluded, so they do not gain a spurious source location and appear in source-filtered queries. Relearned test-kotlin1 (K1) and test-kotlin2 (K2): all 3333 tests pass with database-consistency checks. Only test-kotlin1 expected files change (K2 output is unchanged, as K2 already emits these spans natively). No previously matching row diverges; net K1-vs-K2 divergence decreases on every affected file, with generics, generic-inner-classes and modifiers now fully identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 38 +++++++++++++++++++ .../controlflow/basic/bbStmts.expected | 18 ++++----- .../controlflow/basic/getASuccessor.expected | 6 +-- .../library-tests/exprs/exprs.expected | 10 ++--- .../generic-inner-classes/test.expected | 8 ++-- .../library-tests/generics/generics.expected | 4 +- .../methods/constructors.expected | 2 +- .../library-tests/methods/methods.expected | 2 +- .../modifiers/modifiers.expected | 6 +-- 9 files changed, 66 insertions(+), 28 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index c73b1749a27..eaf9abe8b88 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -53,6 +53,8 @@ import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass import org.jetbrains.kotlin.name.FqName 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.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtVariableDeclaration @@ -2462,6 +2464,7 @@ open class KotlinFileExtractor( val locId = overriddenAttributes?.sourceLoc ?: (if (usesK2) getPsiBasedLocation(f) else null) + ?: (if (f.symbol is IrConstructorSymbol && typeSubstitution == null) getPsiBasedImplicitConstructorLocation(f) else null) ?: getLocation(f, classTypeArgsIncludingOuterClasses) if (f.symbol is IrConstructorSymbol) { @@ -3038,6 +3041,41 @@ open class KotlinFileExtractor( return tw.getLocation(declStart, declEnd) } + /** + * Returns the PSI-based location for the *implicit* (compiler-synthesised) + * primary constructor of [f]'s enclosing class, spanning the full + * [KtClassOrObject] text range (including any leading modifiers such as `open`). + * + * When a class has no primary constructor written in source (for example + * `open class C0 {}`), the compiler synthesises one. The K2 frontend records + * that constructor with the class declaration's raw IR offsets, which include + * the leading modifier keywords. The K1 frontend's raw offsets for the same + * synthesised constructor start at the `class` keyword and omit the modifiers. + * K1 retains the PSI, so we recover the modifier-inclusive span from the + * enclosing [KtClassOrObject] to match K2. + * + * This deliberately does *not* apply to an *explicit* primary constructor + * (e.g. `class C1(val t: T)` or `class C2()`): those keep their own + * parameter-list location, which both frontends already record identically from + * raw IR offsets. It returns null when the [KtFile] is unavailable (as under K2, + * where the raw offsets already carry the class span) or when the enclosing + * class declares an explicit primary constructor. + */ + private fun getPsiBasedImplicitConstructorLocation(f: IrFunction): Label? { + if ((f as? IrConstructor)?.isPrimary != true) return null + val c = f.parentAsClass + val startOffset = c.startOffset + if (startOffset < 0) return null + val file = currentIrFile ?: return null + val ktClass = getPsi2Ir()?.getKtFile(file) + ?.findElementAt(startOffset) + ?.let { leaf -> generateSequence(leaf) { it.parent }.filterIsInstance().firstOrNull() } + ?: return null + // An explicit primary constructor keeps its own (parameter-list) location. + if ((ktClass as? KtClass)?.primaryConstructor != null) return null + return tw.getLocation(ktClass.startOffset, ktClass.endOffset) + } + private fun extractVariable( v: IrVariable, callable: Label, diff --git a/java/ql/test-kotlin1/library-tests/controlflow/basic/bbStmts.expected b/java/ql/test-kotlin1/library-tests/controlflow/basic/bbStmts.expected index dcf1eae75f4..2f7da01dc65 100644 --- a/java/ql/test-kotlin1/library-tests/controlflow/basic/bbStmts.expected +++ b/java/ql/test-kotlin1/library-tests/controlflow/basic/bbStmts.expected @@ -1,12 +1,12 @@ -| Test.kt:3:8:80:1 | Entry | 0 | Test.kt:3:8:80:1 | Entry | -| Test.kt:3:8:80:1 | Entry | 1 | Test.kt:3:8:80:1 | { ... } | -| Test.kt:3:8:80:1 | Entry | 2 | Test.kt:3:1:80:1 | Before super(...) | -| Test.kt:3:8:80:1 | Entry | 3 | Test.kt:3:1:80:1 | super(...) | -| Test.kt:3:8:80:1 | Entry | 4 | Test.kt:3:1:80:1 | After super(...) | -| Test.kt:3:8:80:1 | Entry | 5 | Test.kt:3:8:80:1 | { ... } | -| Test.kt:3:8:80:1 | Entry | 6 | Test.kt:3:8:80:1 | After { ... } | -| Test.kt:3:8:80:1 | Entry | 7 | Test.kt:3:8:80:1 | Normal Exit | -| Test.kt:3:8:80:1 | Entry | 8 | Test.kt:3:8:80:1 | Exit | +| Test.kt:3:1:80:1 | Entry | 0 | Test.kt:3:1:80:1 | Entry | +| Test.kt:3:1:80:1 | Entry | 1 | Test.kt:3:8:80:1 | { ... } | +| Test.kt:3:1:80:1 | Entry | 2 | Test.kt:3:1:80:1 | Before super(...) | +| Test.kt:3:1:80:1 | Entry | 3 | Test.kt:3:1:80:1 | super(...) | +| Test.kt:3:1:80:1 | Entry | 4 | Test.kt:3:1:80:1 | After super(...) | +| Test.kt:3:1:80:1 | Entry | 5 | Test.kt:3:8:80:1 | { ... } | +| Test.kt:3:1:80:1 | Entry | 6 | Test.kt:3:8:80:1 | After { ... } | +| Test.kt:3:1:80:1 | Entry | 7 | Test.kt:3:1:80:1 | Normal Exit | +| Test.kt:3:1:80:1 | Entry | 8 | Test.kt:3:1:80:1 | Exit | | Test.kt:4:2:79:2 | Entry | 0 | Test.kt:4:2:79:2 | Entry | | Test.kt:4:2:79:2 | Entry | 1 | Test.kt:4:13:79:2 | { ... } | | Test.kt:4:2:79:2 | Entry | 2 | Test.kt:5:3:5:16 | var ...; | diff --git a/java/ql/test-kotlin1/library-tests/controlflow/basic/getASuccessor.expected b/java/ql/test-kotlin1/library-tests/controlflow/basic/getASuccessor.expected index f1561a33aa9..305cc1b6b2a 100644 --- a/java/ql/test-kotlin1/library-tests/controlflow/basic/getASuccessor.expected +++ b/java/ql/test-kotlin1/library-tests/controlflow/basic/getASuccessor.expected @@ -1,9 +1,9 @@ #select +| Test.kt:3:1:80:1 | Entry | Constructor | Test.kt:3:8:80:1 | { ... } | BlockStmt | +| Test.kt:3:1:80:1 | Normal Exit | Constructor | Test.kt:3:1:80:1 | Exit | Constructor | | Test.kt:3:1:80:1 | super(...) | SuperConstructorInvocationStmt | Test.kt:3:8:80:1 | { ... } | BlockStmt | -| Test.kt:3:8:80:1 | Entry | Constructor | Test.kt:3:8:80:1 | { ... } | BlockStmt | -| Test.kt:3:8:80:1 | Normal Exit | Constructor | Test.kt:3:8:80:1 | Exit | Constructor | +| Test.kt:3:8:80:1 | { ... } | BlockStmt | Test.kt:3:1:80:1 | Normal Exit | Constructor | | Test.kt:3:8:80:1 | { ... } | BlockStmt | Test.kt:3:1:80:1 | super(...) | SuperConstructorInvocationStmt | -| Test.kt:3:8:80:1 | { ... } | BlockStmt | Test.kt:3:8:80:1 | Normal Exit | Constructor | | Test.kt:4:2:79:2 | Entry | Method | Test.kt:4:13:79:2 | { ... } | BlockStmt | | Test.kt:4:2:79:2 | Normal Exit | Method | Test.kt:4:2:79:2 | Exit | Method | | Test.kt:4:13:79:2 | { ... } | BlockStmt | Test.kt:5:3:5:16 | var ...; | LocalVariableDeclStmt | diff --git a/java/ql/test-kotlin1/library-tests/exprs/exprs.expected b/java/ql/test-kotlin1/library-tests/exprs/exprs.expected index 86be385e40b..710666428ad 100644 --- a/java/ql/test-kotlin1/library-tests/exprs/exprs.expected +++ b/java/ql/test-kotlin1/library-tests/exprs/exprs.expected @@ -1474,11 +1474,11 @@ | exprs.kt:170:9:170:17 | r2.height | exprs.kt:165:1:172:1 | foo | VarAccess | | exprs.kt:170:9:170:21 | ...=... | exprs.kt:165:1:172:1 | foo | AssignExpr | | exprs.kt:170:21:170:21 | 3 | exprs.kt:165:1:172:1 | foo | IntegerLiteral | -| exprs.kt:174:1:176:1 | 0 | exprs.kt:174:6:176:1 | Direction | IntegerLiteral | -| exprs.kt:174:1:176:1 | Direction | exprs.kt:174:6:176:1 | Direction | TypeAccess | -| exprs.kt:174:1:176:1 | Enum | exprs.kt:174:6:176:1 | Direction | TypeAccess | -| exprs.kt:174:1:176:1 | new Enum(...) | exprs.kt:174:6:176:1 | Direction | ClassInstanceExpr | -| exprs.kt:174:1:176:1 | null | exprs.kt:174:6:176:1 | Direction | NullLiteral | +| exprs.kt:174:1:176:1 | 0 | exprs.kt:174:1:176:1 | Direction | IntegerLiteral | +| exprs.kt:174:1:176:1 | Direction | exprs.kt:174:1:176:1 | Direction | TypeAccess | +| exprs.kt:174:1:176:1 | Enum | exprs.kt:174:1:176:1 | Direction | TypeAccess | +| exprs.kt:174:1:176:1 | new Enum(...) | exprs.kt:174:1:176:1 | Direction | ClassInstanceExpr | +| exprs.kt:174:1:176:1 | null | exprs.kt:174:1:176:1 | Direction | NullLiteral | | exprs.kt:175:5:175:10 | ...=... | exprs.kt:0:0:0:0 | | KtInitializerAssignExpr | | exprs.kt:175:5:175:10 | Direction | exprs.kt:0:0:0:0 | | TypeAccess | | exprs.kt:175:5:175:10 | Direction | exprs.kt:0:0:0:0 | | TypeAccess | diff --git a/java/ql/test-kotlin1/library-tests/generic-inner-classes/test.expected b/java/ql/test-kotlin1/library-tests/generic-inner-classes/test.expected index 76c7467253e..8b8a4753c5d 100644 --- a/java/ql/test-kotlin1/library-tests/generic-inner-classes/test.expected +++ b/java/ql/test-kotlin1/library-tests/generic-inner-classes/test.expected @@ -33,12 +33,12 @@ paramTypes | file:///!unknown-binary-location/testuser/OuterNotGeneric$InnerGeneric.class:0:0:0:0 | InnerGeneric | String | constructors | KotlinUser.kt:3:1:18:1 | User | -| OuterGeneric.kt:3:8:21:1 | OuterGeneric | -| OuterGeneric.kt:5:16:9:3 | InnerNotGeneric | +| OuterGeneric.kt:3:1:21:1 | OuterGeneric | +| OuterGeneric.kt:5:3:9:3 | InnerNotGeneric | | OuterGeneric.kt:13:5:13:21 | InnerGeneric | | OuterGeneric.kt:15:5:15:25 | InnerGeneric | -| OuterNotGeneric.kt:3:8:11:1 | OuterNotGeneric | -| OuterNotGeneric.kt:5:16:9:3 | InnerGeneric | +| OuterNotGeneric.kt:3:1:11:1 | OuterNotGeneric | +| OuterNotGeneric.kt:5:3:9:3 | InnerGeneric | | file:///!unknown-binary-location/testuser/OuterGeneric$InnerGeneric.class:0:0:0:0 | InnerGeneric | | file:///!unknown-binary-location/testuser/OuterGeneric$InnerGeneric.class:0:0:0:0 | InnerGeneric | | file:///!unknown-binary-location/testuser/OuterGeneric$InnerNotGeneric.class:0:0:0:0 | InnerNotGeneric<> | diff --git a/java/ql/test-kotlin1/library-tests/generics/generics.expected b/java/ql/test-kotlin1/library-tests/generics/generics.expected index d693fac760f..83835bb7b99 100644 --- a/java/ql/test-kotlin1/library-tests/generics/generics.expected +++ b/java/ql/test-kotlin1/library-tests/generics/generics.expected @@ -29,7 +29,7 @@ parameterizedType function | generics.kt:3:1:5:1 | f0 | f0(int,java.lang.Object) | | generics.kt:7:1:9:1 | f1 | f1(int,java.lang.Object) | -| generics.kt:11:6:11:19 | C0 | C0() | +| generics.kt:11:1:11:19 | C0 | C0() | | generics.kt:13:15:13:24 | C1 | C1(java.lang.Object) | | generics.kt:13:16:13:23 | getT | getT() | | generics.kt:14:5:14:19 | f1 | f1(java.lang.Object) | @@ -40,7 +40,7 @@ function | generics.kt:36:1:40:1 | BoundedTest | BoundedTest() | | generics.kt:38:5:38:25 | m | m(java.lang.CharSequence,java.lang.CharSequence) | | generics.kt:42:1:54:1 | Outer | Outer() | -| generics.kt:43:11:47:5 | Inner1 | Inner1() | +| generics.kt:43:5:47:5 | Inner1 | Inner1() | | generics.kt:44:9:46:9 | fn1 | fn1(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object) | | generics.kt:49:5:53:5 | Nested1 | Nested1() | | generics.kt:50:9:52:9 | fn2 | fn2(java.lang.Object,java.lang.Object) | diff --git a/java/ql/test-kotlin1/library-tests/methods/constructors.expected b/java/ql/test-kotlin1/library-tests/methods/constructors.expected index abcab582ad7..327bc0825ba 100644 --- a/java/ql/test-kotlin1/library-tests/methods/constructors.expected +++ b/java/ql/test-kotlin1/library-tests/methods/constructors.expected @@ -6,7 +6,7 @@ | delegates.kt:8:32:11:5 | | delegates.kt:8:32:11:5 | new KMutableProperty1(...) { ... } | file://:0:0:0:0 | void | | delegates.kt:8:66:11:5 | | delegates.kt:8:66:11:5 | new Function3,String,String,Unit>(...) { ... } | file://:0:0:0:0 | void | | enumClass.kt:1:21:1:32 | EnumClass | enumClass.kt:1:1:4:1 | EnumClass | file://:0:0:0:0 | void | -| enumClass.kt:6:6:16:1 | EnumWithFunctions | enumClass.kt:6:1:16:1 | EnumWithFunctions | file://:0:0:0:0 | void | +| enumClass.kt:6:1:16:1 | EnumWithFunctions | enumClass.kt:6:1:16:1 | EnumWithFunctions | file://:0:0:0:0 | void | | enumClass.kt:8:3:11:4 | VAL | enumClass.kt:8:3:11:4 | VAL | file://:0:0:0:0 | void | | methods2.kt:7:1:10:1 | Class2 | methods2.kt:7:1:10:1 | Class2 | file://:0:0:0:0 | void | | methods3.kt:5:1:7:1 | Class3 | methods3.kt:5:1:7:1 | Class3 | file://:0:0:0:0 | void | diff --git a/java/ql/test-kotlin1/library-tests/methods/methods.expected b/java/ql/test-kotlin1/library-tests/methods/methods.expected index 971bb627436..06675d53e78 100644 --- a/java/ql/test-kotlin1/library-tests/methods/methods.expected +++ b/java/ql/test-kotlin1/library-tests/methods/methods.expected @@ -67,7 +67,7 @@ constructors | delegates.kt:8:32:11:5 | new KMutableProperty1(...) { ... } | delegates.kt:8:32:11:5 | | | | delegates.kt:8:66:11:5 | new Function3,String,String,Unit>(...) { ... } | delegates.kt:8:66:11:5 | | | | enumClass.kt:1:1:4:1 | EnumClass | enumClass.kt:1:21:1:32 | EnumClass | EnumClass(int) | -| enumClass.kt:6:1:16:1 | EnumWithFunctions | enumClass.kt:6:6:16:1 | EnumWithFunctions | EnumWithFunctions() | +| enumClass.kt:6:1:16:1 | EnumWithFunctions | enumClass.kt:6:1:16:1 | EnumWithFunctions | EnumWithFunctions() | | enumClass.kt:8:3:11:4 | VAL | enumClass.kt:8:3:11:4 | VAL | VAL() | | methods2.kt:7:1:10:1 | Class2 | methods2.kt:7:1:10:1 | Class2 | Class2() | | methods3.kt:5:1:7:1 | Class3 | methods3.kt:5:1:7:1 | Class3 | Class3() | diff --git a/java/ql/test-kotlin1/library-tests/modifiers/modifiers.expected b/java/ql/test-kotlin1/library-tests/modifiers/modifiers.expected index 37d6d8a7bf0..4fc6ff961db 100644 --- a/java/ql/test-kotlin1/library-tests/modifiers/modifiers.expected +++ b/java/ql/test-kotlin1/library-tests/modifiers/modifiers.expected @@ -1,5 +1,5 @@ | modifiers.kt:1:1:29:1 | X | Class | public | -| modifiers.kt:1:6:29:1 | X | Constructor | public | +| modifiers.kt:1:1:29:1 | X | Constructor | public | | modifiers.kt:2:13:2:17 | getA$private | Method | final | | modifiers.kt:2:13:2:17 | getA$private | Method | private | | modifiers.kt:2:13:2:21 | a | Field | final | @@ -22,7 +22,7 @@ | modifiers.kt:5:5:5:34 | d | Property | public | | modifiers.kt:7:5:9:5 | Nested | Class | final | | modifiers.kt:7:5:9:5 | Nested | Class | protected | -| modifiers.kt:7:15:9:5 | Nested | Constructor | public | +| modifiers.kt:7:5:9:5 | Nested | Constructor | public | | modifiers.kt:8:16:8:25 | getE | Method | final | | modifiers.kt:8:16:8:25 | getE | Method | public | | modifiers.kt:8:16:8:29 | e | Field | final | @@ -74,7 +74,7 @@ | modifiers.kt:32:5:32:32 | foo | Method | public | | modifiers.kt:35:1:41:1 | LateInit | Class | final | | modifiers.kt:35:1:41:1 | LateInit | Class | public | -| modifiers.kt:35:8:41:1 | LateInit | Constructor | public | +| modifiers.kt:35:1:41:1 | LateInit | Constructor | public | | modifiers.kt:36:22:36:40 | getTest0$private | Method | final | | modifiers.kt:36:22:36:40 | getTest0$private | Method | private | | modifiers.kt:36:22:36:40 | setTest0$private | Method | final |