Kotlin: exclude leading parameter modifiers from val/var parameter location

A primary-constructor property parameter written with leading modifiers,
e.g. `public vararg val s: String`, is located differently by the two
frontend paths:

  - K1 (-language-version 1.9) starts the parameter location at the first
    modifier token (`public`), giving test.kt:50:5:50:31.
  - K2 (default) starts it at the `val`/`var` keyword, giving
    test.kt:50:19:50:31.

The K2 span is the more intuitive and consistent one: every other value
parameter is already located from its `val`/`var` keyword (or its name),
so including the leading modifier list here is an outlier that also makes
the property parameter's span inconsistent with the property it backs.

Converge K1 onto the K2 span. `getPsiBasedValueParameterLocation` finds
the enclosing `KtParameter` via PSI back-mapping (available under K1,
where `getKtFile` is non-null; it returns null under K2, which already
emits the desired offsets) and, only when modifiers precede the keyword,
re-anchors the location start at the `val`/`var` keyword while preserving
the parameter's own end offset.

The guard `keyword.startOffset <= vp.startOffset` is essential: when the
`val`/`var` keyword already is the parameter start (no leading modifiers),
the helper must not fire, otherwise it would rewrite the end offset to
`vp.endOffset` and diverge from the raw location for ordinary property
parameters (observed as orphaned `[Parameter]` rows in generics/
reflection/classes PrintAst during development).

After this change test-kotlin1 and test-kotlin2 vararg/args.expected are
byte-identical. All 3333 tests pass in both suites (K1 2.3.20 / lang 1.9
and K2 2.4.0 / lang 2.0).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Anders Fugmann
2026-07-13 00:00:26 +02:00
parent 32d00a0890
commit 5dc4830425
2 changed files with 34 additions and 1 deletions

View File

@@ -58,6 +58,7 @@ import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtConstructor
import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.psi.KtVariableDeclaration
@@ -1382,6 +1383,7 @@ open class KotlinFileExtractor(
val location =
locOverride
?: getGeneratedDataClassCopyParamLocation(vp, idx)
?: getPsiBasedValueParameterLocation(vp)
?: getLocation(vp, classTypeArgsIncludingOuterClasses)
val maybeAlteredType =
(vp.parent as? IrFunction)?.let {
@@ -3132,6 +3134,37 @@ open class KotlinFileExtractor(
return tw.getLocation(destructuring.startOffset, destructuring.endOffset)
}
/**
* Location for a primary-constructor `val`/`var` value parameter that excludes any
* leading modifiers, spanning from the `val`/`var` keyword to the parameter's end.
*
* For a property parameter declared with modifiers (for example
* `public vararg val s: String`), the K1 frontend's [IrValueParameter] offsets start at
* the first modifier (`public`), whereas K2 starts at the `val`/`var` keyword, consistent
* with how declarations exclude leading modifiers from their own span (annotations and
* modifiers carry their own locations). We converge K1 onto the K2 form.
*
* Returns null under K2 (where [getKtFile] is unavailable and the raw offsets already
* exclude the modifiers), and for any parameter without a `val`/`var` keyword (an ordinary
* value parameter), where the start already coincides with the parameter and there is no
* modifier span to strip.
*/
private fun getPsiBasedValueParameterLocation(vp: IrValueParameter): Label<DbLocation>? {
if (vp.startOffset < 0 || vp.endOffset < 0) return null
val file = currentIrFile ?: return null
val ktFile = getPsi2Ir()?.getKtFile(file) ?: return null
val ktParam = ktFile.findElementAt(vp.startOffset)
?.let { leaf -> generateSequence(leaf) { it.parent }.filterIsInstance<KtParameter>().firstOrNull() }
?: return null
val keyword = ktParam.valOrVarKeyword ?: return null
// Only converge when leading modifiers (e.g. `vararg`, visibility) precede the
// `val`/`var` keyword: there the raw K1 offset starts at the first modifier while
// K2 starts at the keyword. When the keyword is already the parameter start there
// is nothing to exclude, and rewriting the end offset would diverge from K2.
if (keyword.startOffset <= vp.startOffset) return null
return tw.getLocation(keyword.startOffset, vp.endOffset)
}
/**
* Returns the PSI-based location for a synthesised (`DEFAULT_PROPERTY_ACCESSOR`)
* getter or setter, matching the span the K2 frontend emits natively.

View File

@@ -5,7 +5,7 @@ varargsParams
| test.kt:20:24:20:37 | xs | file://:0:0:0:0 | int[] |
| test.kt:24:50:24:63 | xs | file://:0:0:0:0 | int[] |
| test.kt:28:37:28:50 | xs | file://:0:0:0:0 | int[] |
| test.kt:50:5:50:31 | s | file://:0:0:0:0 | String[] |
| test.kt:50:19:50:31 | s | file://:0:0:0:0 | String[] |
explicitVarargsArguments
| test.kt:12:50:12:51 | xs | test.kt:12:44:12:52 | this(...) |
| test.kt:38:25:38:29 | array | test.kt:38:5:38:30 | funWithOnlyVarArgs(...) |