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(...) |