Kotlin: converge default-accessor body spans onto the K2 signature span

A compiler-generated default property accessor (`DEFAULT_PROPERTY_ACCESSOR`)
has no source body, so the two frontends anchor its synthesised body
expressions (`field = value` / `return field`, plus their `<set-?>`, field and
`this` sub-accesses) differently:

  - K1 anchors them at the whole `KtProperty`, whose end offset runs through the
    initialiser (`var topLevelInt: Int = 0` -> `60:1:60:24`,
    `var curValue = 0` -> `26:13:26:24` .. `26:28`).
  - K2 has no PSI for these accessors and falls back to the raw signature span,
    which stops at the type (`60:1:60:20`) or, when the type is inferred, at the
    name (`26:13:26:24`).

The initialiser is not part of the accessor body (it runs in the field
initialiser / `<clinit>`, not in the setter), so K2's narrower signature span is
the more intuitive, information-preserving choice: it keeps the accessor's
synthetic expressions on the property signature and leaves the initialiser to
the genuine `KtInitializerAssignExpr` / `<clinit>` rows.

We converge K1 onto the K2 span with a scoped offset remap set only while
extracting the accessor body (mirroring the existing delegated-property-accessor
remap). The remap is keyed off the accessor's corresponding property and matches
the property's full IR range exactly, so:

  - it only rewrites the synthetic body expressions that carry that range;
  - the real field-initialiser rows (which share the same source text but are
    extracted in the initialiser context) are untouched and keep their
    initialiser-inclusive spans; and
  - it is a no-op under K2 (no PSI, so `getEnclosingKtProperty` returns null) and
    for any property without an initialiser past its signature.

Tradeoff: this converges K1 onto a span that K2 produces natively but that K1
cannot recover without the PSI, so the direction is fixed (K1 -> K2) rather than
chosen freely. That is acceptable here because the K2 span is the more correct
one on the merits (the initialiser does not belong to the accessor body).

Expected updates (K1 only; K2 already emitted these):
  - library-tests/exprs/exprs.expected (setCurValue, setTopLevelInt)
  - library-tests/methods/exprs.expected (clinit.kt setTopLevelInt)

Both suites relearned; all tests pass. Divergence 820 -> 798 rows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86bfc022-3ecc-4746-ba23-3b76c4e4c3e4
This commit is contained in:
Anders Fugmann
2026-07-14 19:53:44 +02:00
parent fe38154431
commit 8267d331a4
3 changed files with 53 additions and 14 deletions

View File

@@ -2580,9 +2580,13 @@ open class KotlinFileExtractor(
f
)
val remap =
if (f.origin == IrDeclarationOrigin.DELEGATED_PROPERTY_ACCESSOR)
getDelegateExpressionOffsetRemap(f)
else null
when (f.origin) {
IrDeclarationOrigin.DELEGATED_PROPERTY_ACCESSOR ->
getDelegateExpressionOffsetRemap(f)
IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR ->
getDefaultAccessorBodyOffsetRemap(f)
else -> null
}
val previousRemap = tw.scopedOffsetRemap
if (remap != null) tw.scopedOffsetRemap = remap
try {
@@ -3581,6 +3585,41 @@ open class KotlinFileExtractor(
)
}
/**
* Maps the property-declaration range (which the K1 frontend records for the synthesised
* body expressions of a compiler-generated default `get`/`set` accessor, running through the
* property initialiser) onto the property *signature* range (the `val`/`var` keyword through
* the type, or through the name when the type is inferred), which K2 records natively.
*
* A `DEFAULT_PROPERTY_ACCESSOR` has no source body, so the K1 frontend anchors its synthesised
* `field = value` / `return field` expressions (and their `<set-?>`/field/`this` sub-accesses)
* at the whole [KtProperty], whose end offset runs through the initialiser
* (`var topLevelInt: Int = 0` -> `60:1:60:24`). K2 has no PSI for these accessors and falls
* back to the raw signature span, which stops at the type (`60:1:60:20`) or, when the type is
* inferred, at the name (`var curValue = 0` -> `26:13:26:24`). The initialiser is not part of
* the accessor body, so K2's narrower signature span is the more intuitive one; we converge K1
* onto it via a scoped offset remap set only while extracting the accessor body. The remap
* matches the property range exactly, so no unrelated location is affected.
*
* Returns null under K2 (no PSI, [getEnclosingKtProperty] returns null) and when the property
* has no initialiser past the signature to exclude.
*/
private fun getDefaultAccessorBodyOffsetRemap(
f: IrFunction
): Pair<Pair<Int, Int>, Pair<Int, Int>>? {
val property = (f as? IrSimpleFunction)?.correspondingPropertySymbol?.owner ?: return null
if (property.startOffset < 0 || property.endOffset < 0) return null
val ktProperty = getEnclosingKtProperty(property) ?: return null
val declEnd = ktProperty.typeReference?.endOffset
?: ktProperty.nameIdentifier?.endOffset
?: return null
if (property.endOffset <= declEnd) return null
return Pair(
Pair(property.startOffset, property.endOffset),
Pair(property.startOffset, declEnd)
)
}
/**
* Returns the [KtProperty] enclosing the PSI element that back-maps from IR element [e],
* or null when no PSI is available (as under K2, where [getKtFile]/[findPsiElement] return

View File

@@ -116,18 +116,18 @@
| delegatedProperties.kt:25:64:31:9 | <Stmt> | delegatedProperties.kt:25:9:31:9 | resourceDelegate | StmtExpr |
| delegatedProperties.kt:25:64:31:9 | ReadWriteProperty<Object,Integer> | delegatedProperties.kt:25:9:31:9 | resourceDelegate | TypeAccess |
| delegatedProperties.kt:25:64:31:9 | new (...) | delegatedProperties.kt:25:9:31:9 | resourceDelegate | ClassInstanceExpr |
| delegatedProperties.kt:26:13:26:24 | ...=... | delegatedProperties.kt:26:13:26:24 | setCurValue | AssignExpr |
| delegatedProperties.kt:26:13:26:24 | <set-?> | delegatedProperties.kt:26:13:26:24 | setCurValue | VarAccess |
| delegatedProperties.kt:26:13:26:24 | Unit | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:26:13:26:24 | int | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:26:13:26:24 | int | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:26:13:26:24 | this | delegatedProperties.kt:26:13:26:24 | getCurValue | ThisAccess |
| delegatedProperties.kt:26:13:26:24 | this | delegatedProperties.kt:26:13:26:24 | setCurValue | ThisAccess |
| delegatedProperties.kt:26:13:26:24 | this.curValue | delegatedProperties.kt:26:13:26:24 | getCurValue | VarAccess |
| delegatedProperties.kt:26:13:26:24 | this.curValue | delegatedProperties.kt:26:13:26:24 | setCurValue | VarAccess |
| delegatedProperties.kt:26:13:26:28 | ...=... | delegatedProperties.kt:25:64:31:9 | | KtInitializerAssignExpr |
| delegatedProperties.kt:26:13:26:28 | ...=... | delegatedProperties.kt:26:13:26:24 | setCurValue | AssignExpr |
| delegatedProperties.kt:26:13:26:28 | <set-?> | delegatedProperties.kt:26:13:26:24 | setCurValue | VarAccess |
| delegatedProperties.kt:26:13:26:28 | curValue | delegatedProperties.kt:25:64:31:9 | | VarAccess |
| delegatedProperties.kt:26:13:26:28 | int | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:26:13:26:28 | this.curValue | delegatedProperties.kt:26:13:26:24 | setCurValue | VarAccess |
| delegatedProperties.kt:26:28:26:28 | 0 | delegatedProperties.kt:25:64:31:9 | | IntegerLiteral |
| delegatedProperties.kt:27:22:27:88 | int | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:27:35:27:47 | Object | file://:0:0:0:0 | <none> | TypeAccess |
@@ -281,18 +281,18 @@
| delegatedProperties.kt:54:51:54:68 | KProperty<?> | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:56:16:56:33 | ResourceDelegate | delegatedProperties.kt:54:14:57:5 | provideDelegate | TypeAccess |
| delegatedProperties.kt:56:16:56:33 | new ResourceDelegate(...) | delegatedProperties.kt:54:14:57:5 | provideDelegate | ClassInstanceExpr |
| delegatedProperties.kt:60:1:60:20 | ...=... | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | AssignExpr |
| delegatedProperties.kt:60:1:60:20 | <set-?> | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | VarAccess |
| delegatedProperties.kt:60:1:60:20 | DelegatedPropertiesKt | delegatedProperties.kt:60:1:60:20 | getTopLevelInt | TypeAccess |
| delegatedProperties.kt:60:1:60:20 | DelegatedPropertiesKt | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | TypeAccess |
| delegatedProperties.kt:60:1:60:20 | DelegatedPropertiesKt.topLevelInt | delegatedProperties.kt:60:1:60:20 | getTopLevelInt | VarAccess |
| delegatedProperties.kt:60:1:60:20 | DelegatedPropertiesKt.topLevelInt | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | VarAccess |
| delegatedProperties.kt:60:1:60:20 | Unit | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:60:1:60:20 | int | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:60:1:60:20 | int | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:60:1:60:24 | ...=... | delegatedProperties.kt:0:0:0:0 | <clinit> | KtInitializerAssignExpr |
| delegatedProperties.kt:60:1:60:24 | ...=... | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | AssignExpr |
| delegatedProperties.kt:60:1:60:24 | <set-?> | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | VarAccess |
| delegatedProperties.kt:60:1:60:24 | DelegatedPropertiesKt | delegatedProperties.kt:0:0:0:0 | <clinit> | TypeAccess |
| delegatedProperties.kt:60:1:60:24 | DelegatedPropertiesKt | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | TypeAccess |
| delegatedProperties.kt:60:1:60:24 | DelegatedPropertiesKt.topLevelInt | delegatedProperties.kt:0:0:0:0 | <clinit> | VarAccess |
| delegatedProperties.kt:60:1:60:24 | DelegatedPropertiesKt.topLevelInt | delegatedProperties.kt:60:1:60:20 | setTopLevelInt | VarAccess |
| delegatedProperties.kt:60:1:60:24 | int | file://:0:0:0:0 | <none> | TypeAccess |
| delegatedProperties.kt:60:24:60:24 | 0 | delegatedProperties.kt:0:0:0:0 | <clinit> | IntegerLiteral |
| delegatedProperties.kt:62:25:62:48 | ...=... | delegatedProperties.kt:62:24:62:49 | ClassWithDelegate | KtInitializerAssignExpr |

View File

@@ -1,14 +1,14 @@
| clinit.kt:3:1:3:20 | ...=... | AssignExpr |
| clinit.kt:3:1:3:20 | <set-?> | VarAccess |
| clinit.kt:3:1:3:20 | ClinitKt | TypeAccess |
| clinit.kt:3:1:3:20 | ClinitKt | TypeAccess |
| clinit.kt:3:1:3:20 | ClinitKt.topLevelInt | VarAccess |
| clinit.kt:3:1:3:20 | ClinitKt.topLevelInt | VarAccess |
| clinit.kt:3:1:3:20 | Unit | TypeAccess |
| clinit.kt:3:1:3:20 | int | TypeAccess |
| clinit.kt:3:1:3:20 | int | TypeAccess |
| clinit.kt:3:1:3:24 | ...=... | AssignExpr |
| clinit.kt:3:1:3:24 | ...=... | KtInitializerAssignExpr |
| clinit.kt:3:1:3:24 | <set-?> | VarAccess |
| clinit.kt:3:1:3:24 | ClinitKt | TypeAccess |
| clinit.kt:3:1:3:24 | ClinitKt | TypeAccess |
| clinit.kt:3:1:3:24 | ClinitKt.topLevelInt | VarAccess |
| clinit.kt:3:1:3:24 | ClinitKt.topLevelInt | VarAccess |
| clinit.kt:3:1:3:24 | int | TypeAccess |
| clinit.kt:3:24:3:24 | 0 | IntegerLiteral |