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/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index 0b975d9b829b..b6318f4aaa37 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 @@ -50,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 @@ -161,23 +164,108 @@ 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 + 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, + 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 + ) + if (hasKotlinMetadata) false else 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 + 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 -> - when (d.name.asString()) { - "toString" -> d.codeQlValueParameters.isEmpty() - "hashCode" -> d.codeQlValueParameters.isEmpty() - "equals" -> d.codeQlValueParameters.singleOrNull()?.type?.isNullableAny() ?: false - else -> false - } && isJavaBinaryDeclaration(d) + d.parentClassOrNull?.let { parentClass -> + // K2 specific: Only suppress Object-method redeclarations when using K2 + // (VirtualFileBasedSourceElement source). K1 uses JavaSourceElement and + // always emits these methods from Java interop signatures (e.g. equals(Object) + // on Map/Collection implementations). Suppressing them under K1 causes + // missing row entries in Kotlin1 parity tests. + parentClass.source is VirtualFileBasedSourceElement && + parentClass.typeParameters.isEmpty() && + when (d.name.asString()) { + "toString" -> d.codeQlValueParameters.isEmpty() + "hashCode" -> d.codeQlValueParameters.isEmpty() + // 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 + } && + !hasConcreteSiblingObjectMethod(d) && + isJavaBinaryDeclaration(d) + } ?: false else -> false } @@ -1312,27 +1400,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 = @@ -1346,9 +1435,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 = @@ -2874,6 +2963,48 @@ 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 { + // K2 specific: IMPLICIT_NOTNULL is a K2-only IR operator. K1 doesn't use it. + // This anchors variable locations to the identifier name token for K2, + // while K1 continues to use the full declaration span via the location provider. + 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 +3013,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 +3031,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) @@ -4066,6 +4197,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 @@ -4111,7 +4264,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 b3577858f99c..5e1642872ebc 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 @@ -996,7 +997,20 @@ 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) } + if (binaryPath != null && shouldUseConcreteExternalFileClassLocation(binaryPath)) { + val fileId = tw.mkFileId(binaryPath, true) + tw.writeHasLocation(fileClassId, tw.getWholeFileLocation(fileId)) + } + } + return fileClassId } return useDeclarationParent(parent, canBeTopLevel, classTypeArguments, inReceiverContext) } @@ -1371,8 +1385,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, @@ -1382,9 +1401,10 @@ open class KotlinUsesExtractor( getFunctionTypeParameters(f), classTypeArgsIncludingOuterClasses, overridesCollectionsMethodWithAlteredParameterTypes(f), - getJavaCallable(f), - !getInnermostWildcardSupppressionAnnotation(f) + javaCallable, + addParameterWildcardsByDefault ) + } /* * This function actually generates the label for a function. @@ -1471,15 +1491,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( @@ -1489,6 +1535,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. @@ -1594,9 +1662,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/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..02824e46cde8 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, @@ -176,15 +177,238 @@ 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 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) } + +/** + * 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 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 (k1ParamKinds != null && k1ParamKinds.isNotEmpty()) { + return k1ParamKinds.singleOrNull() + } + + // K2 path: binary Java class has VirtualFileBasedSourceElement + val k2Source = parentClass.source as? VirtualFileBasedSourceElement ?: return null + val vf = k2Source.virtualFile + if (!vf.name.endsWith(".class")) return null + + return try { + val bytes = vf.contentsToByteArray() + val expectedMethodName = if (isConstructor) "" else methodName + val descriptorKinds = mutableSetOf() + 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 (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 + descriptorKinds.add(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 + ) + descriptorKinds.singleOrNull() + } 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 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 + val k2Source = parentClass.source as? VirtualFileBasedSourceElement ?: return null + val vf = k2Source.virtualFile + if (!vf.name.endsWith(".class")) return null + + return try { + val bytes = vf.contentsToByteArray() + val returnKinds = mutableSetOf() + 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 (name != methodName) return null + if (parseAsmMethodDescriptorParams(descriptor).size != nParams) return null + val returnDescriptor = descriptor.substring(descriptor.lastIndexOf(')') + 1) + returnKinds.add( + 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 + ) + returnKinds.singleOrNull() + } catch (e: Exception) { + null + } +} + +private 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 +} 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", ] )