From a1ae9f7649f8ffb7d85ae6a6f5bd8985c51043a2 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Sun, 12 Jul 2026 20:34:56 +0200 Subject: [PATCH] [C14b] Kotlin: name/locate destructuring container `` in both frontends A destructuring declaration `val (a, b) = subject` introduces a temporary that holds the subject; each component is then read from it via `componentN()`. The two frontends represent that temporary differently: - K1 names it `tmp_container` and locates it at the subject expression only (e.g. `7:26:7:26`, pointing at `p`). - K2 names it `` and locates it across the whole destructuring declaration (e.g. `7:9:7:26`, `val (first, _) = p`). Decision: adopt the K2 representation from both frontends. The `` name is uniform and frontend-independent (K1's `tmp` counter cannot be reproduced under K2, consistent with C14a), and the full-declaration span is more informative than a bare pointer at the subject. Implementation: `isDestructuringContainerVariable` recognises the temporary by its frontend name (`` under K2, `tmp_container` under K1). `extractVariableExpr` then emits the name `` and, under K1 only, a PSI-based location spanning the enclosing `KtDestructuringDeclaration` (`getPsiBasedDestructuringContainerLocation`); under K2 the IR offsets are already correct so the helper returns null and the existing location is kept. Full dual-suite relearn: all 3333 tests pass. The only changed row is in query-tests/UnderscoreIdentifier, where the K1 container converges from `7:26:7:26 | tmp0_container` to `7:9:7:26 | `, matching K2. With this and C15, query-tests/UnderscoreIdentifier is now byte-identical across both suites. The for-loop destructuring shape (`for ((v, i) in ...)`, where K1 omits the container entirely) is a separate AST-shape difference tracked as C14c. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 49 ++++++++++++++++++- .../UnderscoreIdentifier/test.expected | 2 +- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index 0b42b779a43..518613bcd83 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -57,6 +57,7 @@ import org.jetbrains.kotlin.util.OperatorNameConventions 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.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtVariableDeclaration @@ -3065,6 +3066,44 @@ open class KotlinFileExtractor( return tw.getLocation(declStart, declaration.endOffset) } + // Matches the K1 frontend's synthetic name for the temporary holding the subject of a + // destructuring declaration (`val (a, b) = subject`), which is `tmp_container`. K2 names + // the same temporary with the special name ``. + private val destructuringContainerK1NameRegex = Regex("tmp\\d+_container") + + /** + * A destructuring declaration `val (a, b) = subject` introduces a temporary holding the + * subject, from which each component is read via `componentN()`. K1 names this temporary + * `tmp_container`; K2 names it ``. The uniform K2 name identifies the construct + * and is frontend-independent, so we emit `` from both frontends (see also the + * location convergence in [getPsiBasedDestructuringContainerLocation]). + */ + private fun isDestructuringContainerVariable(v: IrVariable): Boolean = + v.name.asString() == "" || + destructuringContainerK1NameRegex.matches(v.name.asString()) + + /** + * Location for the destructuring-container temporary spanning the whole + * [KtDestructuringDeclaration] (`val (a, b) = subject`). + * + * Under K2 the temporary already carries the full declaration span. Under K1 its IR offsets + * point only at the subject expression, so we recover the declaration span from the PSI: look + * up the leaf at the temporary's start offset and walk up to the enclosing + * [KtDestructuringDeclaration]. Returns null under K2 (no PSI back-mapping), where the IR + * offsets are already correct. + */ + private fun getPsiBasedDestructuringContainerLocation(v: IrVariable): Label? { + val startOffset = v.startOffset + if (startOffset < 0) return null + val file = currentIrFile ?: return null + val ktFile = getPsi2Ir()?.getKtFile(file) ?: return null + val element = ktFile.findElementAt(startOffset) ?: return null + val destructuring = generateSequence(element) { it.parent } + .filterIsInstance() + .firstOrNull() ?: return null + return tw.getLocation(destructuring.startOffset, destructuring.endOffset) + } + /** * Returns the PSI-based location for a property declaration, spanning from * the `val`/`var` keyword to the end of the full property declaration (including @@ -3495,15 +3534,21 @@ open class KotlinFileExtractor( with("variable expr", v) { val varId = useVariable(v) val exprId = tw.getFreshIdLabel() - val locId = getPsiBasedLocation(v) ?: tw.getLocation(getVariableLocationProvider(v)) + val isDestructContainer = isDestructuringContainerVariable(v) + val locId = + (if (isDestructContainer) getPsiBasedDestructuringContainerLocation(v) else null) + ?: getPsiBasedLocation(v) + ?: tw.getLocation(getVariableLocationProvider(v)) val type = useType(v.type) // K2 names a source `_` (unused) local/catch variable with the synthetic // SpecialNames.UNDERSCORE_FOR_UNUSED_VAR (``), whereas K1 keeps the // source spelling `_`. Emit `_` so both frontends produce the same, source-faithful - // name (D15). + // name (D15). The desugar temporaries for increment/decrement (``) and + // destructuring (``) are likewise normalised onto the uniform K2 names. val varName = when { currentDesugarTemp?.first === v -> currentDesugarTemp!!.second + isDestructContainer -> "" v.name == SpecialNames.UNDERSCORE_FOR_UNUSED_VAR -> "_" else -> v.name.asString() } diff --git a/java/ql/test-kotlin1/query-tests/UnderscoreIdentifier/test.expected b/java/ql/test-kotlin1/query-tests/UnderscoreIdentifier/test.expected index dc118062b3d..69b63cb3759 100644 --- a/java/ql/test-kotlin1/query-tests/UnderscoreIdentifier/test.expected +++ b/java/ql/test-kotlin1/query-tests/UnderscoreIdentifier/test.expected @@ -2,6 +2,6 @@ | Test.kt:4:28:4:32 | index | | Test.kt:4:35:4:35 | p1 | | Test.kt:6:9:6:26 | Pair p | +| Test.kt:7:9:7:26 | Pair | | Test.kt:7:14:7:18 | int first | -| Test.kt:7:26:7:26 | Pair tmp0_container | | Test.kt:8:14:8:25 | Exception _ |