From 5626a5d8974ba7ccdbd2cd19d254eeac035cb672 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Sun, 12 Jul 2026 15:18:10 +0200 Subject: [PATCH] Kotlin: locate generated data-class `copy` params at their property A data class's generated `copy(...)` has one value parameter per primary-constructor property. The K1 frontend records each such parameter (and its type accesses) at the source location of the corresponding property; the K2 frontend leaves them with undefined offsets, which the extractor emits as a `0:0:0:0` location. This divergence is purely a K2 information regression: the richer K1 location is unambiguously better (it points at the real property in source, enabling location-based queries), so we converge K2 onto K1 rather than the other way around. Because K2 exposes no PSI back-mapping, the location cannot be recomputed from source; instead we recover it from the IR. For a value parameter of a `GENERATED_DATA_CLASS_MEMBER` function whose own offsets are undefined, we look up the primary-constructor parameter at the same index and reuse its location. Guards keep the change surgical: - `vp.startOffset >= 0` bails out, so K1 (which already has real offsets) is untouched. - the origin must be `GENERATED_DATA_CLASS_MEMBER`. - the primary-ctor parameter name must match and carry real offsets, which restricts the remap to `copy`-style parameters and excludes members such as `equals(other)`. Relearned both suites: only data-class `copy` parameter rows change (K2 now matches K1). data-classes/PrintAst.expected becomes byte -identical across suites; the residual diffs in methods/{exprs, parameters}.expected are pre-existing, unrelated divergences. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 34 ++++++++++++++++++- .../data-classes/PrintAst.expected | 10 +++--- .../library-tests/methods/exprs.expected | 4 +-- .../library-tests/methods/parameters.expected | 4 +-- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index 762132add39..e3870ef17f3 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -1329,6 +1329,35 @@ open class KotlinFileExtractor( private fun hasSynthesizedParameterNames(f: IrFunction) = f.descriptor.hasSynthesizedParameterNames() + /** + * Returns the location of the primary-constructor parameter corresponding to a generated + * data-class `copy` value parameter, or null if [vp] is not such a parameter. + * + * A data class's generated `copy(...)` has one value parameter per primary-constructor + * property, defaulting to the current value. K1 records these parameters at the source + * location of the corresponding property; K2 leaves them with undefined offsets (a + * `0:0:0:0` location). To keep the richer K1 information under both frontends, we recover + * the property location from the primary constructor via the IR. + * + * Guards ensure this only affects the K2 case (K1 parameters already carry real offsets) + * and only `copy`-style parameters (matched by name against the primary-constructor + * parameter at the same index), so members such as `equals(other)` are left untouched. + */ + private fun getGeneratedDataClassCopyParamLocation( + vp: IrValueParameter, + idx: Int + ): Label? { + if (vp.startOffset >= 0) return null + val fn = vp.parent as? IrFunction ?: return null + if (fn.origin != IrDeclarationOrigin.GENERATED_DATA_CLASS_MEMBER) return null + val ctorParam = + fn.parentClassOrNull?.primaryConstructor?.codeQlValueParameters?.getOrNull(idx) + ?: return null + if (ctorParam.name.asString() != vp.name.asString() || ctorParam.startOffset < 0) + return null + return tw.getLocation(ctorParam) + } + private fun extractValueParameter( vp: IrValueParameter, parent: Label, @@ -1340,7 +1369,10 @@ open class KotlinFileExtractor( locOverride: Label? = null ): TypeResults { with("value parameter", vp) { - val location = locOverride ?: getLocation(vp, classTypeArgsIncludingOuterClasses) + val location = + locOverride + ?: getGeneratedDataClassCopyParamLocation(vp, idx) + ?: getLocation(vp, classTypeArgsIncludingOuterClasses) val maybeAlteredType = (vp.parent as? IrFunction)?.let { if (overridesCollectionsMethodWithAlteredParameterTypes(it)) diff --git a/java/ql/test-kotlin2/library-tests/data-classes/PrintAst.expected b/java/ql/test-kotlin2/library-tests/data-classes/PrintAst.expected index cce5f37dab9..98999cf1869 100644 --- a/java/ql/test-kotlin2/library-tests/data-classes/PrintAst.expected +++ b/java/ql/test-kotlin2/library-tests/data-classes/PrintAst.expected @@ -17,11 +17,11 @@ dc.kt: # 0| 3: [Method] copy # 0| 3: [TypeAccess] ProtoMapValue #-----| 4: (Parameters) -# 0| 0: [Parameter] bytes -# 0| 0: [TypeAccess] byte[] -# 0| 1: [Parameter] strs -# 0| 0: [TypeAccess] String[] -# 0| 0: [TypeAccess] String +# 1| 0: [Parameter] bytes +# 1| 0: [TypeAccess] byte[] +# 1| 1: [Parameter] strs +# 1| 0: [TypeAccess] String[] +# 1| 0: [TypeAccess] String # 0| 5: [BlockStmt] { ... } # 0| 0: [ReturnStmt] return ... # 0| 0: [ClassInstanceExpr] new ProtoMapValue(...) diff --git a/java/ql/test-kotlin2/library-tests/methods/exprs.expected b/java/ql/test-kotlin2/library-tests/methods/exprs.expected index fbe0ff41944..01b032301a6 100644 --- a/java/ql/test-kotlin2/library-tests/methods/exprs.expected +++ b/java/ql/test-kotlin2/library-tests/methods/exprs.expected @@ -48,7 +48,6 @@ | dataClass.kt:0:0:0:0 | String | TypeAccess | | dataClass.kt:0:0:0:0 | String | TypeAccess | | dataClass.kt:0:0:0:0 | String | TypeAccess | -| dataClass.kt:0:0:0:0 | String | TypeAccess | | dataClass.kt:0:0:0:0 | boolean | TypeAccess | | dataClass.kt:0:0:0:0 | copy(...) | MethodCall | | dataClass.kt:0:0:0:0 | false | BooleanLiteral | @@ -60,7 +59,6 @@ | dataClass.kt:0:0:0:0 | int | TypeAccess | | dataClass.kt:0:0:0:0 | int | TypeAccess | | dataClass.kt:0:0:0:0 | int | TypeAccess | -| dataClass.kt:0:0:0:0 | int | TypeAccess | | dataClass.kt:0:0:0:0 | new DataClass(...) | ClassInstanceExpr | | dataClass.kt:0:0:0:0 | other | VarAccess | | dataClass.kt:0:0:0:0 | other | VarAccess | @@ -114,6 +112,7 @@ | dataClass.kt:1:22:1:31 | int | TypeAccess | | dataClass.kt:1:22:1:31 | int | TypeAccess | | dataClass.kt:1:22:1:31 | int | TypeAccess | +| dataClass.kt:1:22:1:31 | int | TypeAccess | | dataClass.kt:1:22:1:31 | this | ThisAccess | | dataClass.kt:1:22:1:31 | this.x | VarAccess | | dataClass.kt:1:22:1:31 | x | VarAccess | @@ -125,6 +124,7 @@ | dataClass.kt:1:34:1:46 | String | TypeAccess | | dataClass.kt:1:34:1:46 | String | TypeAccess | | dataClass.kt:1:34:1:46 | String | TypeAccess | +| dataClass.kt:1:34:1:46 | String | TypeAccess | | dataClass.kt:1:34:1:46 | Unit | TypeAccess | | dataClass.kt:1:34:1:46 | this | ThisAccess | | dataClass.kt:1:34:1:46 | this | ThisAccess | diff --git a/java/ql/test-kotlin2/library-tests/methods/parameters.expected b/java/ql/test-kotlin2/library-tests/methods/parameters.expected index b26345a228d..87eb45458cf 100644 --- a/java/ql/test-kotlin2/library-tests/methods/parameters.expected +++ b/java/ql/test-kotlin2/library-tests/methods/parameters.expected @@ -1,6 +1,6 @@ | clinit.kt:3:1:3:20 | setTopLevelInt | clinit.kt:3:1:3:20 | | 0 | -| dataClass.kt:0:0:0:0 | copy | dataClass.kt:0:0:0:0 | x | 0 | -| dataClass.kt:0:0:0:0 | copy | dataClass.kt:0:0:0:0 | y | 1 | +| dataClass.kt:0:0:0:0 | copy | dataClass.kt:1:22:1:31 | x | 0 | +| dataClass.kt:0:0:0:0 | copy | dataClass.kt:1:34:1:46 | y | 1 | | dataClass.kt:0:0:0:0 | copy$default | dataClass.kt:0:0:0:0 | p0 | 0 | | dataClass.kt:0:0:0:0 | copy$default | dataClass.kt:0:0:0:0 | p1 | 1 | | dataClass.kt:0:0:0:0 | copy$default | dataClass.kt:0:0:0:0 | p2 | 2 |