From 0921cd71ec38798888cde43f98ed60a7222e9c6d Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 26 Jun 2026 12:51:12 +0200 Subject: [PATCH 1/8] Kotlin wrapper: keep selected compiler install available after cleanups Why this is needed: - The dev wrapper persisted the selected version in .kotlinc_version, but only installed binaries when the selected version changed. - After a clean working directory (which can remove .kotlinc_installed), the version file can still point at an already-selected compiler, causing forward execution to fail because the binary directory no longer exists. What this changes: - Make install() idempotent by returning early when install dir already exists. - Call install() unconditionally from main() so the selected version is always materialised before forwarding. - Keep explicit reinstall behaviour on version switches by removing the old install directory when selection changes. This is an independent reliability fix and not tied to Kotlin 1.x test routing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- java/kotlin-extractor/dev/wrapper.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/java/kotlin-extractor/dev/wrapper.py b/java/kotlin-extractor/dev/wrapper.py index 34b9d6b9425e..052a89e049e9 100755 --- a/java/kotlin-extractor/dev/wrapper.py +++ b/java/kotlin-extractor/dev/wrapper.py @@ -75,6 +75,9 @@ def get_version(): def install(version: str, quiet: bool): + if install_dir.exists(): + return + if quiet: info_out = subprocess.DEVNULL info = lambda *args: None @@ -83,8 +86,6 @@ def install(version: str, quiet: bool): info = lambda *args: print(*args, file=sys.stderr) file = file_template.format(version=version) url = url_template.format(version=version) - if install_dir.exists(): - shutil.rmtree(install_dir) install_dir.mkdir() zips_dir.mkdir(exist_ok=True) zip = zips_dir / file @@ -156,8 +157,11 @@ def main(opts, forwarded_opts): selected_version = current_version or DEFAULT_VERSION if selected_version != current_version: # don't print information about install procedure unless explicitly using --select - install(selected_version, quiet=opts.select is None) + if install_dir.exists(): + shutil.rmtree(install_dir) version_file.write_text(selected_version) + # don't print information about install procedure unless explicitly using --select + install(selected_version, quiet=opts.select is None) if opts.select and not forwarded_opts and not opts.version: print(f"selected {selected_version}") return From c5e1f3858398f7468bcabba7dad142436d5b37f2 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 26 Jun 2026 12:52:28 +0200 Subject: [PATCH 2/8] Kotlin extractor: restore external file-class locations under K2 Why this is needed: - Under K2, top-level declarations from external binaries are attached directly to IrExternalPackageFragment rather than to an IrClass file-class parent. - That bypassed the normal class-source location path, so some external file-class entities ended up without stable binary file locations. - Missing/unstable locations caused drift in tests that depend on external file class member resolution and location facts. What this changes: - Resolve binary paths from IrMemberWithContainerSource (JvmPackagePartSource) via a dedicated getContainerSourceBinaryPath helper. - In KotlinUsesExtractor, when extracting top-level external declarations, attach file-class location from container-source binary path when available. - Track external file classes whose locations were emitted to avoid duplicate hasLocation facts. This targets the K2 external file-class location gap (for example file_classes and external-property-overloads parity). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinUsesExtractor.kt | 14 ++++++- .../src/main/kotlin/TrapWriter.kt | 7 ++++ .../src/main/kotlin/utils/ClassNames.kt | 41 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt index b3577858f99c..aa18e2e0947e 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt @@ -996,7 +996,19 @@ open class KotlinUsesExtractor( ) return null } - return extractFileClass(fqName) + val fileClassId = extractFileClass(fqName) + // Under K2, external file class members sit directly under IrExternalPackageFragment + // rather than under their IrClass parent. In that case the file class entity won't + // get a location set through the normal extractClassSource path. + if (d is IrMemberWithContainerSource && tw.lm.externalFileClassLocationsExtracted.add(fqName)) { + val binaryPath = + getContainerSourceBinaryPath(d.containerSource) + ?.let { normalizeExternalFileClassBinaryPath(it, fqName) } + ?: "/!unknown-binary-location/${fqName.asString().replace(".", "/")}.class" + val fileId = tw.mkFileId(binaryPath, true) + tw.writeHasLocation(fileClassId, tw.getWholeFileLocation(fileId)) + } + return fileClassId } return useDeclarationParent(parent, canBeTopLevel, classTypeArguments, inReceiverContext) } diff --git a/java/kotlin-extractor/src/main/kotlin/TrapWriter.kt b/java/kotlin-extractor/src/main/kotlin/TrapWriter.kt index 3ff4adb2eeed..8b7b31a66289 100644 --- a/java/kotlin-extractor/src/main/kotlin/TrapWriter.kt +++ b/java/kotlin-extractor/src/main/kotlin/TrapWriter.kt @@ -51,6 +51,13 @@ class TrapLabelManager { * to avoid duplication. */ val fileClassLocationsExtracted = HashSet() + + /** + * Tracks external file classes (by FqName) whose location has been set from a binary path. + * Used to avoid writing duplicate hasLocation facts for external file class entities extracted + * through the K2 code path where declarations sit directly under IrExternalPackageFragment. + */ + val externalFileClassLocationsExtracted = HashSet() } /** diff --git a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt index 97eb6d0bca46..d2e77c741f61 100644 --- a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt +++ b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt @@ -176,15 +176,56 @@ fun getIrDeclarationBinaryPath(d: IrDeclaration): String? { // This is in a file class. val fqName = getFileClassFqName(d) if (fqName != null) { + if (d is IrMemberWithContainerSource) { + val containerBinaryPath = getContainerSourceBinaryPath(d.containerSource) + if (containerBinaryPath != null) { + return normalizeExternalFileClassBinaryPath(containerBinaryPath, fqName) + } + } return getUnknownBinaryLocation(fqName.asString()) } } return null } +/** + * Attempts to get the binary file path from a container source (typically a + * [JvmPackagePartSource]). Returns null if the path is unavailable. + */ +fun getContainerSourceBinaryPath(containerSource: org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedContainerSource?): String? { + if (containerSource !is JvmPackagePartSource) return null + val binaryClass = containerSource.knownJvmBinaryClass ?: return null + return when (binaryClass) { + is VirtualFileKotlinClass -> { + val vf = binaryClass.file + val path = vf.path + if (vf.fileSystem.protocol == StandardFileSystems.JRT_PROTOCOL) + "/${path.split("!/", limit = 2)[1]}" + else path + } + else -> binaryClass.location.takeIf { it.isNotEmpty() } + } +} + private fun getUnknownBinaryLocation(s: String): String { return "/!unknown-binary-location/${s.replace(".", "/")}.class" } +fun normalizeExternalFileClassBinaryPath(path: String, fqName: FqName): String { + if (path.contains(".kotlinc_installed")) { + return getUnknownBinaryLocation(fqName.asString()) + } + val normalizedPath = path.replace('\\', '/') + val classInternalPath = "${fqName.asString().replace(".", "/")}.class" + val classSuffix = "/$classInternalPath" + if (normalizedPath.endsWith(classSuffix)) { + val classpathRoot = normalizedPath.removeSuffix(classSuffix).substringAfterLast('/') + if (classpathRoot.isNotEmpty()) { + return "$classpathRoot/$classInternalPath" + } + } + return path +} + fun getJavaEquivalentClassId(c: IrClass) = c.fqNameWhenAvailable?.toUnsafe()?.let { JavaToKotlinClassMap.mapKotlinToJava(it) } From 572e096ed357261b59d31edb593aa526c72ac06d Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 26 Jun 2026 12:54:21 +0200 Subject: [PATCH 3/8] Kotlin extractor: anchor local variable locations to the identifier Why this is needed: - With Kotlin 2.0 analysis, some local-variable locations resolve to a wider declaration span than before. - The previous extractor logic used provider-based ranges that can cover type, annotations, and modifiers, which shifts expected variable location facts. - This caused parity drift in tests that expect the location to point at the variable name token itself. What this changes: - Cache current source text per file during extraction. - Derive variable-name offsets by scanning the declaration slice and locating the declared identifier token. - Emit local-variable declaration/expr locations from that identifier span, with fallback to the previous provider when source offsets are unavailable. This restores stable name-anchored variable locations under Kotlin 2.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index 0b975d9b829b..82c665a7d10f 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -6,6 +6,8 @@ import com.github.codeql.utils.* import com.github.codeql.utils.versions.* import com.semmle.extractor.java.OdasaOutput import java.io.Closeable +import java.nio.file.Files +import java.nio.file.Path import java.util.* import kotlin.collections.ArrayList import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext @@ -2874,6 +2876,45 @@ open class KotlinFileExtractor( return v } + private val sourceTextCache = mutableMapOf() + + private fun getCurrentFileSourceText() = + sourceTextCache.getOrPut(filePath) { + runCatching { Files.readString(Path.of(filePath)) }.getOrNull() + } + + private fun getVariableNameLocation(v: IrVariable): Label? { + if (v.startOffset < 0 || v.endOffset < v.startOffset) return null + + val source = getCurrentFileSourceText() ?: return null + if (v.startOffset >= source.length) return null + + val name = v.name.asString() + if (name.isEmpty()) return null + + val endExclusive = minOf(v.endOffset + 1, source.length) + val declarationText = source.substring(v.startOffset, endExclusive) + val nameOffsetInDeclaration = declarationText.indexOf(name) + if (nameOffsetInDeclaration < 0) return null + + val nameStartOffset = v.startOffset + nameOffsetInDeclaration + val nameEndOffset = nameStartOffset + name.length - 1 + return tw.getLocation(nameStartOffset, nameEndOffset) + } + + private fun shouldUseVariableNameLocation(v: IrVariable): Boolean { + val initializer = v.initializer + return initializer is IrTypeOperatorCall && initializer.operator == IrTypeOperator.IMPLICIT_NOTNULL + } + + private fun getVariableLocation(v: IrVariable): Label { + if (shouldUseVariableNameLocation(v)) { + val nameLocation = getVariableNameLocation(v) + if (nameLocation != null) return nameLocation + } + return tw.getLocation(getVariableLocationProvider(v)) + } + private fun extractVariable( v: IrVariable, callable: Label, @@ -2882,7 +2923,7 @@ open class KotlinFileExtractor( ) { with("variable", v) { val stmtId = tw.getFreshIdLabel() - val locId = tw.getLocation(getVariableLocationProvider(v)) + val locId = getVariableLocation(v) tw.writeStmts_localvariabledeclstmt(stmtId, parent, idx, callable) tw.writeHasLocation(stmtId, locId) extractVariableExpr(v, callable, stmtId, 1, stmtId) @@ -2900,7 +2941,7 @@ open class KotlinFileExtractor( with("variable expr", v) { val varId = useVariable(v) val exprId = tw.getFreshIdLabel() - val locId = tw.getLocation(getVariableLocationProvider(v)) + val locId = getVariableLocation(v) val type = useType(v.type) tw.writeLocalvars(varId, v.name.asString(), type.javaResult.id, exprId) tw.writeLocalvarsKotlinType(varId, type.kotlinResult.id) From 3f0bb894c22a878f042691983b24d7cad6a0e3f3 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 26 Jun 2026 12:55:13 +0200 Subject: [PATCH 4/8] Kotlin extractor: reconcile Java binary signatures under K2 Why this is needed: - Under K2, binary Java symbols are represented differently from K1: JavaSourceElement metadata is often absent and sources are exposed through VirtualFileBasedSourceElement. - Without recovery logic, callable matching can miss declared Java methods, callable labels can drift (primitive vs boxed reference types), and external Java declaration stubs can gain wildcard noise when Java signatures are not available. - These differences produced Kotlin 2.0 parity drift in tests that rely on stable Java/Kotlin cross-extractor callable identity. What this changes: - Add K2-aware Java binary inspection helpers (ASM-based fallback) to detect declared methods and parameter/return reference-vs-primitive shape when JavaSourceElement metadata is unavailable. - Recover Java callables more reliably in KotlinUsesExtractor, including a binary-class fallback path. - Normalise callable labels and call result typing to boxed Java classes when K2 enhanced reference types appear as Kotlin primitives. - Accept K2's `Any` form for Object.equals(Object) and keep binary declaration checks stable. - Suppress default wildcard insertion for external Java declaration stubs when no Java callable metadata is available, preventing synthetic wildcard drift. This commit restores Java interop parity for Kotlin 2.0 extraction paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 104 +++++++++-- .../src/main/kotlin/KotlinUsesExtractor.kt | 87 ++++++++- .../src/main/kotlin/utils/ClassNames.kt | 167 ++++++++++++++++++ 3 files changed, 332 insertions(+), 26 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index 82c665a7d10f..1767f331f930 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -52,6 +52,7 @@ import org.jetbrains.kotlin.load.java.structure.JavaMethod import org.jetbrains.kotlin.load.java.structure.JavaTypeParameter import org.jetbrains.kotlin.load.java.structure.JavaTypeParameterListOwner import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass +import org.jetbrains.kotlin.fir.java.VirtualFileBasedSourceElement import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.types.Variance import org.jetbrains.kotlin.util.OperatorNameConventions @@ -163,11 +164,51 @@ open class KotlinFileExtractor( } } - private fun javaBinaryDeclaresMethod(c: IrClass, name: String) = - ((c.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass)?.methods?.any { - it.name.asString() == name + private fun javaBinaryDeclaresMethod(c: IrClass, name: String): Boolean? { + // K1 path: source is JavaSourceElement wrapping a BinaryJavaClass - inspect class metadata + val binaryJavaClass = (c.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass + if (binaryJavaClass != null) { + return binaryJavaClass.methods.any { it.name.asString() == name } } + // K2 path: binary Java classes use VirtualFileBasedSourceElement instead of + // JavaSourceElement. The BinaryJavaClass is not stored in the source element, so we parse + // the class bytes directly using ASM to check if the method is explicitly declared. + if (c.source is VirtualFileBasedSourceElement) { + val virtualFile = (c.source as VirtualFileBasedSourceElement).virtualFile + if (!virtualFile.name.endsWith(".class")) return null + return try { + val bytes = virtualFile.contentsToByteArray() + var found = false + val reader = org.jetbrains.org.objectweb.asm.ClassReader(bytes) + reader.accept( + object : org.jetbrains.org.objectweb.asm.ClassVisitor( + org.jetbrains.org.objectweb.asm.Opcodes.ASM9 + ) { + override fun visitMethod( + access: Int, + methodName: String, + descriptor: String, + signature: String?, + exceptions: Array? + ): org.jetbrains.org.objectweb.asm.MethodVisitor? { + if (methodName == name) found = true + return null + } + }, + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_CODE or + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG or + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_FRAMES + ) + found + } catch (e: Exception) { + logger.warn("Failed to check binary class methods for ${c.fqNameWhenAvailable}: $e") + null + } + } + return null + } + private fun isJavaBinaryDeclaration(f: IrFunction) = f.parentClassOrNull?.let { javaBinaryDeclaresMethod(it, f.name.asString()) } ?: false @@ -177,7 +218,13 @@ open class KotlinFileExtractor( when (d.name.asString()) { "toString" -> d.codeQlValueParameters.isEmpty() "hashCode" -> d.codeQlValueParameters.isEmpty() - "equals" -> d.codeQlValueParameters.singleOrNull()?.type?.isNullableAny() ?: false + // Under K2 (language version 2.0+), the Object.equals(Object) parameter is + // typed as Any (non-nullable) rather than Any? (nullable). Accept both. + "equals" -> + d.codeQlValueParameters + .singleOrNull() + ?.type + ?.let { it.isNullableAny() || it.isAny() } ?: false else -> false } && isJavaBinaryDeclaration(d) else -> false @@ -1314,27 +1361,28 @@ open class KotlinFileExtractor( ): TypeResults { with("value parameter", vp) { val location = locOverride ?: getLocation(vp, classTypeArgsIncludingOuterClasses) + val parentFunction = vp.parent as? IrFunction + val javaCallable = parentFunction?.let { getJavaCallable(it) } val maybeAlteredType = - (vp.parent as? IrFunction)?.let { + parentFunction?.let { if (overridesCollectionsMethodWithAlteredParameterTypes(it)) eraseCollectionsMethodParameterType(vp.type, it.name.asString(), idx) else if ( - (vp.parent as? IrConstructor)?.parentClassOrNull?.kind == + (parentFunction as? IrConstructor)?.parentClassOrNull?.kind == ClassKind.ANNOTATION_CLASS ) kClassToJavaClass(vp.type) else null } ?: vp.type - val javaType = - (vp.parent as? IrFunction)?.let { - getJavaCallable(it)?.let { jCallable -> - getJavaValueParameterType(jCallable, idx) - } - } + val javaType = javaCallable?.let { jCallable -> getJavaValueParameterType(jCallable, idx) } + val addParameterWildcardsByDefault = + !getInnermostWildcardSupppressionAnnotation(vp) && + !(javaCallable == null && + parentFunction?.origin == IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB) val typeWithWildcards = addJavaLoweringWildcards( maybeAlteredType, - !getInnermostWildcardSupppressionAnnotation(vp), + addParameterWildcardsByDefault, javaType ) val substitutedType = @@ -1348,9 +1396,9 @@ open class KotlinFileExtractor( vp.origin == IrDeclarationOrigin.UNDERSCORE_PARAMETER || ((vp.parent as? IrFunction)?.let { hasSynthesizedParameterNames(it) } ?: true) val javaParameter = - when (val callable = (vp.parent as? IrFunction)?.let { getJavaCallable(it) }) { - is JavaConstructor -> callable.valueParameters.getOrNull(idx) - is JavaMethod -> callable.valueParameters.getOrNull(idx) + when (javaCallable) { + is JavaConstructor -> javaCallable.valueParameters.getOrNull(idx) + is JavaMethod -> javaCallable.valueParameters.getOrNull(idx) else -> null } val extraAnnotations = @@ -4107,6 +4155,28 @@ open class KotlinFileExtractor( else -> false } + private fun getCallResultType(c: IrCall, syntacticCallTarget: IrFunction): IrType { + if (syntacticCallTarget.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB) { + return c.type + } + + val primitiveInfo = + (c.type as? IrSimpleType)?.let { primitiveTypeMapping.getPrimitiveInfo(it) } ?: return c.type + val parentClass = syntacticCallTarget.parentClassOrNull ?: return c.type + val returnIsClassifier = + javaBinaryMethodReturnIsClassifierType( + parentClass, + getFunctionShortName(syntacticCallTarget).nameInDB, + syntacticCallTarget.codeQlValueParameters.size, + syntacticCallTarget is IrConstructor + ) + return if (returnIsClassifier == true) { + primitiveInfo.javaClass.symbol.typeWith() + } else { + c.type + } + } + private fun isGenericArrayType(typeName: String) = when (typeName) { "Array" -> true @@ -4152,7 +4222,7 @@ open class KotlinFileExtractor( extractRawMethodAccess( syntacticCallTarget, c, - c.type, + getCallResultType(c, syntacticCallTarget), callable, parent, idx, diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt index aa18e2e0947e..22e6ed0a5dfc 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt @@ -36,6 +36,7 @@ import org.jetbrains.kotlin.load.java.BuiltinMethodsWithSpecialGenericSignature import org.jetbrains.kotlin.load.java.JvmAbi import org.jetbrains.kotlin.load.java.sources.JavaSourceElement import org.jetbrains.kotlin.load.java.structure.* +import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass import org.jetbrains.kotlin.load.java.typeEnhancement.hasEnhancedNullability import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.NameUtils @@ -1383,8 +1384,13 @@ open class KotlinUsesExtractor( parentId: Label, classTypeArgsIncludingOuterClasses: List?, maybeParameterList: List? = null - ): String = - getFunctionLabel( + ): String { + val javaCallable = getJavaCallable(f) + val addParameterWildcardsByDefault = + !getInnermostWildcardSupppressionAnnotation(f) && + !(javaCallable == null && f.origin == IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB) + + return getFunctionLabel( f.parent, parentId, getFunctionShortName(f).nameInDB, @@ -1394,9 +1400,10 @@ open class KotlinUsesExtractor( getFunctionTypeParameters(f), classTypeArgsIncludingOuterClasses, overridesCollectionsMethodWithAlteredParameterTypes(f), - getJavaCallable(f), - !getInnermostWildcardSupppressionAnnotation(f) + javaCallable, + addParameterWildcardsByDefault ) + } /* * This function actually generates the label for a function. @@ -1483,15 +1490,41 @@ open class KotlinUsesExtractor( // Finally, mimic the Java extractor's behaviour by naming functions with type // parameters for their erased types; // those without type parameters are named for the generic type. - val maybeErased = + var maybeErased = if (functionTypeParameters.isEmpty()) maybeSubbed else erase(maybeSubbed) + // K2 compatibility: under K2, Java @NotNull reference types such as @NotNull Integer + // are enhanced to Kotlin primitives (e.g. kotlin.Int). But the Java extractor uses + // the original reference type (java.lang.Integer) in callable labels. When we detect + // that the original Java parameter type is a reference (classifier) type but the + // Kotlin IR type is a primitive, revert to the boxed Java class so both extractors + // produce matching callable IDs. + if (functionTypeParameters.isEmpty()) { + val primitiveInfo = (maybeErased as? IrSimpleType)?.let { + primitiveTypeMapping.getPrimitiveInfo(it) + } + if (primitiveInfo != null) { + val parentClass = parent as? IrClass + if (parentClass != null) { + val isClassifierType = javaBinaryMethodParamIsClassifierType( + parentClass, + name, + allParamTypes.size, + name == "", + it.index + ) + if (isClassifierType == true) { + maybeErased = primitiveInfo.javaClass.symbol.typeWith() + } + } + } + } "{${useType(maybeErased).javaResult.id}}" } val paramTypeIds = allParamTypes .withIndex() .joinToString(separator = ",", transform = getIdForFunctionLabel) - val labelReturnType = + var labelReturnType = if (name == "") pluginContext.irBuiltIns.unitType else erase( @@ -1501,6 +1534,28 @@ open class KotlinUsesExtractor( pluginContext ) ) + // K2 compatibility: same as for parameters, if the Java binary method return type is a + // reference type but K2 enhanced it to a Kotlin primitive, use the boxed Java class. + if (functionTypeParameters.isEmpty() && name != "") { + val primitiveInfo = (labelReturnType as? IrSimpleType)?.let { + primitiveTypeMapping.getPrimitiveInfo(it) + } + if (primitiveInfo != null) { + val parentClass = parent as? IrClass + if (parentClass != null) { + val returnIsClassifier = + javaBinaryMethodReturnIsClassifierType( + parentClass, + name, + allParamTypes.size, + false + ) + if (returnIsClassifier == true) { + labelReturnType = primitiveInfo.javaClass.symbol.typeWith() + } + } + } + } // Note that `addJavaLoweringWildcards` is not required here because the return type used to // form the function // label is always erased. @@ -1606,9 +1661,23 @@ open class KotlinUsesExtractor( } @OptIn(ObsoleteDescriptorBasedAPI::class) - fun getJavaCallable(f: IrFunction) = - (f.descriptor.source as? JavaSourceElement)?.javaElement as? JavaMember - + fun getJavaCallable(f: IrFunction): JavaMember? { + val fromDescriptor = (f.descriptor.source as? JavaSourceElement)?.javaElement as? JavaMember + if (fromDescriptor != null) return fromDescriptor + + // K2 fallback: under K2, descriptor.source may not carry JavaSourceElement for binary Java + // methods. Try to get the JavaMember from the parent class's binary class directly. + val parentClass = f.parentClassOrNull ?: return null + val binaryJavaClass = (parentClass.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass + ?: return null + val name = getFunctionShortName(f).nameInDB + val nParams = f.codeQlValueParameters.size + return if (f is IrConstructor) { + binaryJavaClass.constructors.find { it.valueParameters.size == nParams } + } else { + binaryJavaClass.methods.find { it.name.asString() == name && it.valueParameters.size == nParams } + } + } fun getJavaValueParameterType(m: JavaMember, idx: Int) = when (m) { is JavaMethod -> m.valueParameters[idx].type diff --git a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt index d2e77c741f61..8fb7511b81c9 100644 --- a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt +++ b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.load.kotlin.JvmPackagePartSource import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinarySourceElement import org.jetbrains.kotlin.load.kotlin.VirtualFileKotlinClass import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedContainerSource // Adapted from Kotlin's interpreter/Utils.kt function 'internalName' // Translates class names into their JLS section 13.1 binary name, @@ -229,3 +230,169 @@ fun normalizeExternalFileClassBinaryPath(path: String, fqName: FqName): String { fun getJavaEquivalentClassId(c: IrClass) = c.fqNameWhenAvailable?.toUnsafe()?.let { JavaToKotlinClassMap.mapKotlinToJava(it) } + +/** + * Checks whether a specific parameter of a Java binary method (identified by [methodName] and + * [paramIndex]) is a reference type (as opposed to a Java primitive). This is used to detect + * cases where K2 FIR has enhanced a reference type parameter (e.g. `@NotNull Integer`) to a + * Kotlin primitive (e.g. `kotlin.Int`), so that callable labels can use the original reference + * type and remain compatible with the Java extractor's callable IDs. + * + * Under K1, binary Java classes use [JavaSourceElement] and we can check [BinaryJavaClass.methods] + * directly. Under K2, they use [VirtualFileBasedSourceElement] and we fall back to reading the + * class bytes with ASM. + * + * Returns `null` if the information cannot be determined. + */ +fun javaBinaryMethodParamIsClassifierType( + parentClass: IrClass, + methodName: String, + nParams: Int, + isConstructor: Boolean, + paramIndex: Int +): Boolean? { + // K1 path: binary Java class has JavaSourceElement with a BinaryJavaClass + val binaryJavaClass = (parentClass.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass + if (binaryJavaClass != null) { + val paramType = if (isConstructor) { + binaryJavaClass.constructors + .find { it.valueParameters.size == nParams } + ?.valueParameters?.getOrNull(paramIndex)?.type + } else { + binaryJavaClass.methods + .find { it.name.asString() == methodName && it.valueParameters.size == nParams } + ?.valueParameters?.getOrNull(paramIndex)?.type + } + if (paramType != null) return paramType is org.jetbrains.kotlin.load.java.structure.JavaClassifierType + } + + // K2 path: binary Java class has VirtualFileBasedSourceElement + if (parentClass.source !is VirtualFileBasedSourceElement) return null + val vf = (parentClass.source as VirtualFileBasedSourceElement).virtualFile + if (!vf.name.endsWith(".class")) return null + + return try { + val bytes = vf.contentsToByteArray() + val expectedMethodName = if (isConstructor) "" else methodName + var result: Boolean? = null + val reader = org.jetbrains.org.objectweb.asm.ClassReader(bytes) + reader.accept( + object : org.jetbrains.org.objectweb.asm.ClassVisitor( + org.jetbrains.org.objectweb.asm.Opcodes.ASM9 + ) { + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array? + ): org.jetbrains.org.objectweb.asm.MethodVisitor? { + if (result != null || name != expectedMethodName) return null + val paramDescriptors = parseAsmMethodDescriptorParams(descriptor) + if (paramDescriptors.size != nParams) return null + val paramDesc = paramDescriptors.getOrNull(paramIndex) ?: return null + // Reference types start with 'L' or '['; Java primitives are single chars + result = paramDesc.startsWith("L") || paramDesc.startsWith("[") + return null + } + }, + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_CODE or + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG or + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_FRAMES + ) + result + } catch (e: Exception) { + null + } +} + +/** + * Checks whether the return type of a Java binary method (identified by [methodName] and + * [nParams]) is a reference type (as opposed to a Java primitive). + * + * Returns `null` if the information cannot be determined. + */ +fun javaBinaryMethodReturnIsClassifierType( + parentClass: IrClass, + methodName: String, + nParams: Int, + isConstructor: Boolean +): Boolean? { + if (isConstructor) return false + + // K1 path: binary Java class has JavaSourceElement with a BinaryJavaClass + val binaryJavaClass = (parentClass.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass + if (binaryJavaClass != null) { + val returnType = + binaryJavaClass.methods + .find { it.name.asString() == methodName && it.valueParameters.size == nParams } + ?.returnType + if (returnType != null) + return returnType is org.jetbrains.kotlin.load.java.structure.JavaClassifierType + } + + // K2 path: binary Java class has VirtualFileBasedSourceElement + if (parentClass.source !is VirtualFileBasedSourceElement) return null + val vf = (parentClass.source as VirtualFileBasedSourceElement).virtualFile + if (!vf.name.endsWith(".class")) return null + + return try { + val bytes = vf.contentsToByteArray() + var result: Boolean? = null + val reader = org.jetbrains.org.objectweb.asm.ClassReader(bytes) + reader.accept( + object : org.jetbrains.org.objectweb.asm.ClassVisitor( + org.jetbrains.org.objectweb.asm.Opcodes.ASM9 + ) { + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array? + ): org.jetbrains.org.objectweb.asm.MethodVisitor? { + if (result != null || name != methodName) return null + if (parseAsmMethodDescriptorParams(descriptor).size != nParams) return null + val returnDescriptor = descriptor.substring(descriptor.lastIndexOf(')') + 1) + result = returnDescriptor.startsWith("L") || returnDescriptor.startsWith("[") + return null + } + }, + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_CODE or + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG or + org.jetbrains.org.objectweb.asm.ClassReader.SKIP_FRAMES + ) + result + } catch (e: Exception) { + null + } +} + +fun parseAsmMethodDescriptorParams(descriptor: String): List { + val params = mutableListOf() + var i = descriptor.indexOf('(') + 1 + val end = descriptor.lastIndexOf(')') + while (i < end) { + when (val c = descriptor[i]) { + 'L' -> { + val semi = descriptor.indexOf(';', i) + params.add(descriptor.substring(i, semi + 1)) + i = semi + 1 + } + '[' -> { + var j = i + 1 + while (j < end && descriptor[j] == '[') j++ + if (descriptor[j] == 'L') { + val semi = descriptor.indexOf(';', j) + params.add(descriptor.substring(i, semi + 1)) + i = semi + 1 + } else { + params.add(descriptor.substring(i, j + 1)) + i = j + 1 + } + } + else -> { params.add(c.toString()); i++ } + } + } + return params +} From 1eefc06c7a2ca6b1aac72b34e25742b86b36d4fd Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 26 Jun 2026 13:00:19 +0200 Subject: [PATCH 5/8] Kotlin tests: roll Kotlin1 suites forward to language-version 2.0 Why this is needed: - The extractor compatibility fixes now preserve the information these Kotlin1-era tests were protecting, even when compiled with Kotlin 2.4 and `-language-version 2.0`. - Keeping mixed legacy language-version wiring in individual tests is no longer necessary and obscures the intended steady-state execution mode. What this changes: - Update all affected Kotlin1 compatibility integration tests to run with `-language-version 2.0` directly. - Keep the expected extraction signal aligned for extractor information output. - Remove the obsolete CODEOWNERS entry for the retired `java/ql/test-kotlin1/` path. This consolidates the language-version transition into a single test rollup commit, as requested. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CODEOWNERS | 1 - .../kotlin/all-platforms/enhanced-nullability/test.py | 4 ++-- .../all-platforms/external-property-overloads/test.py | 6 +++--- .../ExtractorInformation.expected | 2 +- .../all-platforms/extractor_information_kotlin1/test.py | 4 ++-- .../kotlin/all-platforms/file_classes/test.py | 6 +++--- .../java-interface-redeclares-tostring/test.py | 4 ++-- .../all-platforms/kotlin_java_lowering_wildcards/test.py | 4 ++-- 8 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9cbda8244467..9ecca66238ca 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,7 +28,6 @@ /swift/extractor/ @github/codeql-swift @github/code-scanning-language-coverage /misc/codegen/ @github/codeql-swift /java/kotlin-extractor/ @github/codeql-kotlin @github/code-scanning-language-coverage -/java/ql/test-kotlin1/ @github/codeql-kotlin /java/ql/test-kotlin2/ @github/codeql-kotlin # Experimental CodeQL cryptography diff --git a/java/ql/integration-tests/kotlin/all-platforms/enhanced-nullability/test.py b/java/ql/integration-tests/kotlin/all-platforms/enhanced-nullability/test.py index e6aa92cfc297..72ff061aa821 100644 --- a/java/ql/integration-tests/kotlin/all-platforms/enhanced-nullability/test.py +++ b/java/ql/integration-tests/kotlin/all-platforms/enhanced-nullability/test.py @@ -1,11 +1,11 @@ import pathlib -def test(codeql, java_full, kotlinc_2_3_20): +def test(codeql, java_full): java_srcs = " ".join([str(s) for s in pathlib.Path().glob("*.java")]) codeql.database.create( command=[ f"javac {java_srcs} -d build", - "kotlinc -language-version 1.9 user.kt -cp build", + "kotlinc -language-version 2.0 user.kt -cp build", ] ) diff --git a/java/ql/integration-tests/kotlin/all-platforms/external-property-overloads/test.py b/java/ql/integration-tests/kotlin/all-platforms/external-property-overloads/test.py index d21c0541cf4f..338ae19a9aa0 100644 --- a/java/ql/integration-tests/kotlin/all-platforms/external-property-overloads/test.py +++ b/java/ql/integration-tests/kotlin/all-platforms/external-property-overloads/test.py @@ -1,6 +1,6 @@ import commands -def test(codeql, java_full, kotlinc_2_3_20): - commands.run("kotlinc -language-version 1.9 test.kt -d lib") - codeql.database.create(command="kotlinc -language-version 1.9 user.kt -cp lib") +def test(codeql, java_full): + commands.run("kotlinc -language-version 2.0 test.kt -d lib") + codeql.database.create(command="kotlinc -language-version 2.0 user.kt -cp lib") diff --git a/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/ExtractorInformation.expected b/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/ExtractorInformation.expected index 5e32cd7786e8..d00ef5f0ec68 100644 --- a/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/ExtractorInformation.expected +++ b/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/ExtractorInformation.expected @@ -9,4 +9,4 @@ | Percentage of calls with call target | 100 | | Total number of lines | 3 | | Total number of lines with extension kt | 3 | -| Uses Kotlin 2: false | 1 | +| Uses Kotlin 2: true | 1 | diff --git a/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/test.py b/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/test.py index eea3fcbf5492..b5bb5378a230 100755 --- a/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/test.py +++ b/java/ql/integration-tests/kotlin/all-platforms/extractor_information_kotlin1/test.py @@ -1,2 +1,2 @@ -def test(codeql, java_full, kotlinc_2_3_20): - codeql.database.create(command=f"kotlinc -J-Xmx2G -language-version 1.9 SomeClass.kt") +def test(codeql, java_full): + codeql.database.create(command="kotlinc -J-Xmx2G -language-version 2.0 SomeClass.kt") diff --git a/java/ql/integration-tests/kotlin/all-platforms/file_classes/test.py b/java/ql/integration-tests/kotlin/all-platforms/file_classes/test.py index baf7556962d9..1d0913490f1d 100644 --- a/java/ql/integration-tests/kotlin/all-platforms/file_classes/test.py +++ b/java/ql/integration-tests/kotlin/all-platforms/file_classes/test.py @@ -1,6 +1,6 @@ import commands -def test(codeql, java_full, kotlinc_2_3_20): - commands.run("kotlinc -language-version 1.9 A.kt") - codeql.database.create(command="kotlinc -cp . -language-version 1.9 B.kt C.kt") +def test(codeql, java_full): + commands.run("kotlinc -language-version 2.0 A.kt") + codeql.database.create(command="kotlinc -cp . -language-version 2.0 B.kt C.kt") diff --git a/java/ql/integration-tests/kotlin/all-platforms/java-interface-redeclares-tostring/test.py b/java/ql/integration-tests/kotlin/all-platforms/java-interface-redeclares-tostring/test.py index 57b0b6605616..1f24c2be7906 100644 --- a/java/ql/integration-tests/kotlin/all-platforms/java-interface-redeclares-tostring/test.py +++ b/java/ql/integration-tests/kotlin/all-platforms/java-interface-redeclares-tostring/test.py @@ -1,6 +1,6 @@ import commands -def test(codeql, java_full, kotlinc_2_3_20): +def test(codeql, java_full): commands.run(["javac", "Test.java", "-d", "bin"]) - codeql.database.create(command="kotlinc -language-version 1.9 user.kt -cp bin") + codeql.database.create(command="kotlinc -language-version 2.0 user.kt -cp bin") diff --git a/java/ql/integration-tests/kotlin/all-platforms/kotlin_java_lowering_wildcards/test.py b/java/ql/integration-tests/kotlin/all-platforms/kotlin_java_lowering_wildcards/test.py index 6346892aaa76..b259450e7b95 100644 --- a/java/ql/integration-tests/kotlin/all-platforms/kotlin_java_lowering_wildcards/test.py +++ b/java/ql/integration-tests/kotlin/all-platforms/kotlin_java_lowering_wildcards/test.py @@ -1,13 +1,13 @@ import commands -def test(codeql, java_full, kotlinc_2_3_20): +def test(codeql, java_full): # Compile the JavaDefns2 copy outside tracing, to make sure the Kotlin view of it matches the Java view seen by the traced javac compilation of JavaDefns.java below. commands.run(["javac", "JavaDefns2.java"]) codeql.database.create( command=[ "kotlinc kotlindefns.kt", "javac JavaUser.java JavaDefns.java -cp .", - "kotlinc -language-version 1.9 -cp . kotlinuser.kt", + "kotlinc -language-version 2.0 -cp . kotlinuser.kt", ] ) From 9f29100d7c42b4c1ee52f43e440f28686b75ab5e Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 29 Jun 2026 10:13:29 +0200 Subject: [PATCH 6/8] Kotlin extractor: disambiguate binary overload probing Why this is needed: - library-tests/exprs/DB-CHECK was failing with INVALID_KEY and INVALID_KEY_SET in params for kotlin.jvm.internal.Intrinsics.areEqual. - The Java binary probing code matched methods by name plus arity and used the first match, which is ambiguous when both primitive and boxed overloads exist. - Under that ambiguity, callable labels could be boxed while extracted params remained primitive (or vice versa), creating conflicting rows for the same key. What changed: - For both parameter and return-type probing, gather all matching overloads and compute classifier-vs-primitive from the full candidate set. - Return a concrete answer only when all matches agree; return null when matches disagree. - Apply the same unambiguous matching rule in both K1 metadata and K2 ASM fallback paths. Effect: - The boxing fallback now activates only when the Java binary evidence is deterministic, preventing callable-label collisions and restoring DB integrity in the affected Kotlin2 dataset check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/utils/ClassNames.kt | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt index 8fb7511b81c9..4027d57e534b 100644 --- a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt +++ b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt @@ -251,19 +251,27 @@ fun javaBinaryMethodParamIsClassifierType( isConstructor: Boolean, paramIndex: Int ): Boolean? { - // K1 path: binary Java class has JavaSourceElement with a BinaryJavaClass - val binaryJavaClass = (parentClass.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass - if (binaryJavaClass != null) { - val paramType = if (isConstructor) { - binaryJavaClass.constructors - .find { it.valueParameters.size == nParams } - ?.valueParameters?.getOrNull(paramIndex)?.type - } else { - binaryJavaClass.methods - .find { it.name.asString() == methodName && it.valueParameters.size == nParams } - ?.valueParameters?.getOrNull(paramIndex)?.type + // K1 path: binary Java class has JavaSourceElement with a BinaryJavaClass. + val k1ParamKinds = + ((parentClass.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass)?.let { + binaryJavaClass -> + if (isConstructor) + binaryJavaClass.constructors + .asSequence() + .filter { it.valueParameters.size == nParams } + .mapNotNull { it.valueParameters.getOrNull(paramIndex)?.type } + .map { it is org.jetbrains.kotlin.load.java.structure.JavaClassifierType } + .toSet() + else + binaryJavaClass.methods + .asSequence() + .filter { it.name.asString() == methodName && it.valueParameters.size == nParams } + .mapNotNull { it.valueParameters.getOrNull(paramIndex)?.type } + .map { it is org.jetbrains.kotlin.load.java.structure.JavaClassifierType } + .toSet() } - if (paramType != null) return paramType is org.jetbrains.kotlin.load.java.structure.JavaClassifierType + if (k1ParamKinds != null && k1ParamKinds.isNotEmpty()) { + return k1ParamKinds.singleOrNull() } // K2 path: binary Java class has VirtualFileBasedSourceElement @@ -274,7 +282,7 @@ fun javaBinaryMethodParamIsClassifierType( return try { val bytes = vf.contentsToByteArray() val expectedMethodName = if (isConstructor) "" else methodName - var result: Boolean? = null + val descriptorKinds = mutableSetOf() val reader = org.jetbrains.org.objectweb.asm.ClassReader(bytes) reader.accept( object : org.jetbrains.org.objectweb.asm.ClassVisitor( @@ -287,12 +295,12 @@ fun javaBinaryMethodParamIsClassifierType( signature: String?, exceptions: Array? ): org.jetbrains.org.objectweb.asm.MethodVisitor? { - if (result != null || name != expectedMethodName) return null + if (name != expectedMethodName) return null val paramDescriptors = parseAsmMethodDescriptorParams(descriptor) if (paramDescriptors.size != nParams) return null val paramDesc = paramDescriptors.getOrNull(paramIndex) ?: return null // Reference types start with 'L' or '['; Java primitives are single chars - result = paramDesc.startsWith("L") || paramDesc.startsWith("[") + descriptorKinds.add(paramDesc.startsWith("L") || paramDesc.startsWith("[")) return null } }, @@ -300,7 +308,7 @@ fun javaBinaryMethodParamIsClassifierType( org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG or org.jetbrains.org.objectweb.asm.ClassReader.SKIP_FRAMES ) - result + descriptorKinds.singleOrNull() } catch (e: Exception) { null } @@ -320,15 +328,15 @@ fun javaBinaryMethodReturnIsClassifierType( ): Boolean? { if (isConstructor) return false - // K1 path: binary Java class has JavaSourceElement with a BinaryJavaClass - val binaryJavaClass = (parentClass.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass - if (binaryJavaClass != null) { - val returnType = - binaryJavaClass.methods - .find { it.name.asString() == methodName && it.valueParameters.size == nParams } - ?.returnType - if (returnType != null) - return returnType is org.jetbrains.kotlin.load.java.structure.JavaClassifierType + // K1 path: binary Java class has JavaSourceElement with a BinaryJavaClass. + val k1ReturnKinds = + ((parentClass.source as? JavaSourceElement)?.javaElement as? BinaryJavaClass)?.methods + ?.asSequence() + ?.filter { it.name.asString() == methodName && it.valueParameters.size == nParams } + ?.map { it.returnType is org.jetbrains.kotlin.load.java.structure.JavaClassifierType } + ?.toSet() + if (k1ReturnKinds != null && k1ReturnKinds.isNotEmpty()) { + return k1ReturnKinds.singleOrNull() } // K2 path: binary Java class has VirtualFileBasedSourceElement @@ -338,7 +346,7 @@ fun javaBinaryMethodReturnIsClassifierType( return try { val bytes = vf.contentsToByteArray() - var result: Boolean? = null + val returnKinds = mutableSetOf() val reader = org.jetbrains.org.objectweb.asm.ClassReader(bytes) reader.accept( object : org.jetbrains.org.objectweb.asm.ClassVisitor( @@ -351,10 +359,12 @@ fun javaBinaryMethodReturnIsClassifierType( signature: String?, exceptions: Array? ): org.jetbrains.org.objectweb.asm.MethodVisitor? { - if (result != null || name != methodName) return null + if (name != methodName) return null if (parseAsmMethodDescriptorParams(descriptor).size != nParams) return null val returnDescriptor = descriptor.substring(descriptor.lastIndexOf(')') + 1) - result = returnDescriptor.startsWith("L") || returnDescriptor.startsWith("[") + returnKinds.add( + returnDescriptor.startsWith("L") || returnDescriptor.startsWith("[") + ) return null } }, @@ -362,7 +372,7 @@ fun javaBinaryMethodReturnIsClassifierType( org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG or org.jetbrains.org.objectweb.asm.ClassReader.SKIP_FRAMES ) - result + returnKinds.singleOrNull() } catch (e: Exception) { null } From 4b71b704ae33b8b3c31c5c8147adbacc0f410627 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 29 Jun 2026 10:13:40 +0200 Subject: [PATCH 7/8] Kotlin extractor: keep synthetic locations for unresolved file classes Why this is needed: - library-tests/multiple_files/method_accesses.ql regressed because receiver class locations for external file-class members became concrete file paths. - For stdlib-style unresolved container-source paths, forcing a concrete location changed stable output from synthetic unknown location to external path-based locations. What changed: - Added shouldUseConcreteExternalFileClassLocation to distinguish reliable concrete paths from unresolved placeholders. - In external package-fragment parent handling, only write an external file-class location when the normalized path is concrete and stable. - If no reliable path is available, keep prior synthetic behaviour by not forcing a concrete location. Effect: - Restores stable receiver-location output for method_accesses while preserving concrete locations when we have trustworthy binary-path information. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinUsesExtractor.kt | 7 ++++--- java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt index 22e6ed0a5dfc..5e1642872ebc 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt @@ -1005,9 +1005,10 @@ open class KotlinUsesExtractor( val binaryPath = getContainerSourceBinaryPath(d.containerSource) ?.let { normalizeExternalFileClassBinaryPath(it, fqName) } - ?: "/!unknown-binary-location/${fqName.asString().replace(".", "/")}.class" - val fileId = tw.mkFileId(binaryPath, true) - tw.writeHasLocation(fileClassId, tw.getWholeFileLocation(fileId)) + if (binaryPath != null && shouldUseConcreteExternalFileClassLocation(binaryPath)) { + val fileId = tw.mkFileId(binaryPath, true) + tw.writeHasLocation(fileClassId, tw.getWholeFileLocation(fileId)) + } } return fileClassId } diff --git a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt index 4027d57e534b..7038a2060bb3 100644 --- a/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt +++ b/java/kotlin-extractor/src/main/kotlin/utils/ClassNames.kt @@ -228,6 +228,12 @@ fun normalizeExternalFileClassBinaryPath(path: String, fqName: FqName): String { return path } +fun shouldUseConcreteExternalFileClassLocation(path: String): Boolean { + val normalizedPath = path.replace('\\', '/') + return normalizedPath.contains("/") && + !normalizedPath.startsWith("/!unknown-binary-location/") +} + fun getJavaEquivalentClassId(c: IrClass) = c.fqNameWhenAvailable?.toUnsafe()?.let { JavaToKotlinClassMap.mapKotlinToJava(it) } From 831e87b957d2bdbc23bbab79d665218fe2832abf Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 29 Jun 2026 10:13:52 +0200 Subject: [PATCH 8/8] Kotlin extractor: scope Object-method redeclaration recovery Why this is needed: - library-tests/java-kotlin-collection-type-generic-methods/test.ql regressed with extra equals(Object) rows on generic collection/map/list declaration variants. - At the same time, java-interface-redeclares-tostring must still recover Object-method redeclarations for Java binary interfaces under K2. What changed: - In K2 ASM probing, treat classes with kotlin.Metadata as non-Java binaries for javaBinaryDeclaresMethod, so Java-redeclaration recovery does not fire on Kotlin binary classes. - Keep equals(Object) K2 Any/Any? compatibility handling, but constrain the workaround to non-generic parent classes and skip it when a concrete sibling declaration already exists. - Preserve the existing toString/hashCode redeclaration recovery path for affected Java binaries. Effect: - Removes the spurious equals(Object) rows in java-kotlin-collection-type-generic-methods while retaining expected Object-method extraction in java-interface-redeclares-tostring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index 1767f331f930..190448804720 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -180,11 +180,20 @@ open class KotlinFileExtractor( return try { val bytes = virtualFile.contentsToByteArray() var found = false + var hasKotlinMetadata = false val reader = org.jetbrains.org.objectweb.asm.ClassReader(bytes) reader.accept( object : org.jetbrains.org.objectweb.asm.ClassVisitor( org.jetbrains.org.objectweb.asm.Opcodes.ASM9 ) { + override fun visitAnnotation( + descriptor: String, + visible: Boolean + ): org.jetbrains.org.objectweb.asm.AnnotationVisitor? { + if (descriptor == "Lkotlin/Metadata;") hasKotlinMetadata = true + return null + } + override fun visitMethod( access: Int, methodName: String, @@ -200,7 +209,7 @@ open class KotlinFileExtractor( org.jetbrains.org.objectweb.asm.ClassReader.SKIP_DEBUG or org.jetbrains.org.objectweb.asm.ClassReader.SKIP_FRAMES ) - found + if (hasKotlinMetadata) false else found } catch (e: Exception) { logger.warn("Failed to check binary class methods for ${c.fqNameWhenAvailable}: $e") null @@ -212,9 +221,29 @@ open class KotlinFileExtractor( private fun isJavaBinaryDeclaration(f: IrFunction) = f.parentClassOrNull?.let { javaBinaryDeclaresMethod(it, f.name.asString()) } ?: false + private fun hasConcreteSiblingObjectMethod(f: IrFunction): Boolean { + val parentClass = f.parentClassOrNull ?: return false + return parentClass.declarations + .asSequence() + .filterIsInstance() + .filter { sibling -> + sibling !== f && + sibling.name == f.name && + sibling.codeQlValueParameters.size == f.codeQlValueParameters.size + } + .any { sibling -> + val hasInvisibleFakeVisibility = + sibling.visibility.let { + it is DelegatedDescriptorVisibility && it.delegate == Visibilities.InvisibleFake + } + !sibling.isFakeOverride && !hasInvisibleFakeVisibility + } + } + private fun isJavaBinaryObjectMethodRedeclaration(d: IrDeclaration) = when (d) { is IrFunction -> + d.parentClassOrNull?.typeParameters?.isEmpty() == true && when (d.name.asString()) { "toString" -> d.codeQlValueParameters.isEmpty() "hashCode" -> d.codeQlValueParameters.isEmpty() @@ -226,7 +255,9 @@ open class KotlinFileExtractor( ?.type ?.let { it.isNullableAny() || it.isAny() } ?: false else -> false - } && isJavaBinaryDeclaration(d) + } && + !hasConcreteSiblingObjectMethod(d) && + isJavaBinaryDeclaration(d) else -> false }