From 5dc4830425492c6a628ef2d301a622a24b15218d Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 13 Jul 2026 00:00:26 +0200 Subject: [PATCH] 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> --- .../src/main/kotlin/KotlinFileExtractor.kt | 33 +++++++++++++++++++ .../library-tests/vararg/args.expected | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index ea8bb574ffe..211e760ad71 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -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? { + 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().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. diff --git a/java/ql/test-kotlin1/library-tests/vararg/args.expected b/java/ql/test-kotlin1/library-tests/vararg/args.expected index 6cbc936dfa1..5ea9c22aeea 100644 --- a/java/ql/test-kotlin1/library-tests/vararg/args.expected +++ b/java/ql/test-kotlin1/library-tests/vararg/args.expected @@ -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(...) |