Kotlin: converge delegated property accessor locations onto K2 property span

For local and member delegated properties, the synthesised accessor
(`<get-prop>`/`<set-prop>`) and its generated wrapper class/constructor
were anchored by the K1 frontend at the delegate expression rather than
at the property declaration. K2 (raw IR) anchors them at the property
`val`/`var` keyword through the end of the delegate, i.e. the whole
`KtProperty` span.

Adopt the K2 span for both frontends so the extractor emits identical
locations regardless of the supplied language version. Example:

  <get-prop1>  6:24:9:9  ->  6:9:9:9

Add `getPsiBasedDelegatedAccessorLocation`, which walks from the
accessor's PSI up to the enclosing `KtProperty` and returns its span.
It returns null when there is no PSI (K2) or no enclosing property, so
K2 keeps its native locations and non-delegated callables are
unaffected. Wire it into `extractFunction`'s location chain and into
`extractGeneratedClass` (guarded on `DELEGATED_PROPERTY_ACCESSOR`) so the
generated class/constructor sorts before the method, matching K2.

Updates test-kotlin1 expected only (test-kotlin2 unchanged).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Anders Fugmann
2026-07-11 13:15:35 +02:00
parent 240103e7b3
commit b2f94da372
7 changed files with 708 additions and 661 deletions

View File

@@ -2463,6 +2463,7 @@ open class KotlinFileExtractor(
val locId =
overriddenAttributes?.sourceLoc
?: (if (f.origin == IrDeclarationOrigin.DELEGATED_PROPERTY_ACCESSOR) getPsiBasedDelegatedAccessorLocation(f) else null)
?: (if (usesK2) getPsiBasedLocation(f) else null)
?: (if (f.symbol is IrConstructorSymbol && typeSubstitution == null) getPsiBasedImplicitConstructorLocation(f) else null)
?: getLocation(f, classTypeArgsIncludingOuterClasses)
@@ -3076,6 +3077,42 @@ open class KotlinFileExtractor(
return tw.getLocation(ktClass.startOffset, ktClass.endOffset)
}
/**
* Returns the PSI-based location for the synthesised getter/setter of a
* *delegated* property (origin `DELEGATED_PROPERTY_ACCESSOR`), spanning the
* enclosing property declaration from its `val`/`var` keyword through the end of
* the delegate expression.
*
* For a delegated property such as `val prop1: Int by lazy { ... }` the compiler
* synthesises a getter (and, for `var`, a setter) whose body forwards to the
* delegate's `getValue`/`setValue`. The K2 frontend records these accessors with
* raw IR offsets spanning the whole property declaration (the `val`/`var` keyword
* through the delegate expression). The K1 frontend's raw offsets (and its
* [findPsiElement]-based location) instead cover only the delegate expression, so
* the two frontends disagree on the accessor's start column.
*
* K1 retains the PSI, so we recover the K2-native span from the enclosing
* [KtProperty]: the [findPsiElement] mapping resolves the accessor to a node
* within the property's delegate, from which we walk up to the [KtProperty].
* [KtProperty.valOrVarKeyword] then gives the start (excluding any leading
* modifiers, matching the default-accessor and property spans) and
* [KtProperty.endOffset] gives the end (past the delegate expression).
*
* This is deliberately not gated on [usesK2]: it must apply to the K1 frontends
* (which retain PSI) to converge them onto the K2-native span, and it returns null
* under K2 (where [findPsiElement] finds no PSI and the raw offsets already carry
* this span). It applies uniformly to both getter and setter, and to both
* member-level and local delegated properties.
*/
private fun getPsiBasedDelegatedAccessorLocation(f: IrFunction): Label<DbLocation>? {
val file = currentIrFile ?: return null
val psiElement = getPsi2Ir()?.findPsiElement(f, file) ?: return null
val ktProperty = generateSequence(psiElement) { it.parent }
.filterIsInstance<KtProperty>().firstOrNull()
?: return null
return tw.getLocation(ktProperty.valOrVarKeyword.startOffset, ktProperty.endOffset)
}
private fun extractVariable(
v: IrVariable,
callable: Label<out DbCallable>,
@@ -9372,11 +9409,21 @@ open class KotlinFileExtractor(
with("generated class", localFunction) {
val ids = getLocallyVisibleFunctionLabels(localFunction)
// For a delegated property's synthesised accessor the generated wrapper class
// (and its constructor and super-call) must share the accessor's declaration
// span so that, like the K2 frontend, the constructor still sorts before the
// getter/setter rather than after it (the raw K1 offset would place the class
// at the delegate expression, past the accessor's own start column).
val classLocId =
(if (localFunction.origin == IrDeclarationOrigin.DELEGATED_PROPERTY_ACCESSOR)
getPsiBasedDelegatedAccessorLocation(localFunction) else null)
?: tw.getLocation(localFunction)
val id =
extractGeneratedClass(
ids,
superTypes,
tw.getLocation(localFunction),
classLocId,
localFunction,
localFunction.parent,
compilerGeneratedKindOverride = compilerGeneratedKindOverride