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<V> {}` 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>
This commit is contained in:
Anders Fugmann
2026-07-11 10:12:41 +02:00
parent e37d89b849
commit 240103e7b3
9 changed files with 66 additions and 28 deletions

View File

@@ -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<V> {}`), 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<DbLocation>? {
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<KtClassOrObject>().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<out DbCallable>,

View File

@@ -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 ...; |

View File

@@ -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 |

View File

@@ -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<Direction> | 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<Direction> | 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 | <clinit> | KtInitializerAssignExpr |
| exprs.kt:175:5:175:10 | Direction | exprs.kt:0:0:0:0 | <clinit> | TypeAccess |
| exprs.kt:175:5:175:10 | Direction | exprs.kt:0:0:0:0 | <clinit> | TypeAccess |

View File

@@ -33,12 +33,12 @@ paramTypes
| file:///!unknown-binary-location/testuser/OuterNotGeneric$InnerGeneric.class:0:0:0:0 | InnerGeneric<String> | 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<String> |
| file:///!unknown-binary-location/testuser/OuterGeneric$InnerGeneric.class:0:0:0:0 | InnerGeneric<String> |
| file:///!unknown-binary-location/testuser/OuterGeneric$InnerNotGeneric.class:0:0:0:0 | InnerNotGeneric<> |

View File

@@ -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) |

View File

@@ -6,7 +6,7 @@
| delegates.kt:8:32:11:5 | | delegates.kt:8:32:11:5 | new KMutableProperty1<MyClass,String>(...) { ... } | file://:0:0:0:0 | void |
| delegates.kt:8:66:11:5 | | delegates.kt:8:66:11:5 | new Function3<KProperty<?>,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 |

View File

@@ -67,7 +67,7 @@ constructors
| delegates.kt:8:32:11:5 | new KMutableProperty1<MyClass,String>(...) { ... } | delegates.kt:8:32:11:5 | | |
| delegates.kt:8:66:11:5 | new Function3<KProperty<?>,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() |

View File

@@ -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 |