Files
codeql/java/kotlin-extractor/src/main/kotlin/KotlinUsesExtractor.kt

1684 lines
80 KiB
Kotlin

package com.github.codeql
import com.github.codeql.utils.*
import com.github.codeql.utils.versions.codeQlWithHasQuestionMark
import com.github.codeql.utils.versions.isRawType
import com.semmle.extractor.java.OdasaOutput
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.ir.*
import org.jetbrains.kotlin.backend.common.lower.parents
import org.jetbrains.kotlin.backend.common.lower.parentsWithSelf
import org.jetbrains.kotlin.backend.jvm.ir.propertyIfAccessor
import org.jetbrains.kotlin.codegen.JvmCodegenUtil
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.types.impl.*
import org.jetbrains.kotlin.ir.util.*
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.kotlin.getJvmModuleNameForDeserializedDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.util.OperatorNameConventions
open class KotlinUsesExtractor(
open val logger: Logger,
open val tw: TrapWriter,
val dependencyCollector: OdasaOutput.TrapFileManager?,
val externalClassExtractor: ExternalDeclExtractor,
val primitiveTypeMapping: PrimitiveTypeMapping,
val pluginContext: IrPluginContext,
val globalExtensionState: KotlinExtractorGlobalState
) {
val javaLangObject by lazy {
val result = pluginContext.referenceClass(FqName("java.lang.Object"))?.owner
result?.let { extractExternalClassLater(it) }
result
}
val javaLangObjectType by lazy {
javaLangObject?.typeWith()
}
private fun usePackage(pkg: String): Label<out DbPackage> {
return extractPackage(pkg)
}
fun extractPackage(pkg: String): Label<out DbPackage> {
val pkgLabel = "@\"package;$pkg\""
val id: Label<DbPackage> = tw.getLabelFor(pkgLabel, {
tw.writePackages(it, pkg)
})
return id
}
fun useFileClassType(f: IrFile) = TypeResults(
TypeResult(extractFileClass(f), "", ""),
TypeResult(fakeKotlinType(), "", "")
)
@OptIn(kotlin.ExperimentalStdlibApi::class) // Annotation required by kotlin versions < 1.5
fun extractFileClass(f: IrFile): Label<out DbClass> {
val fileName = f.fileEntry.name
val pkg = f.fqName.asString()
val defaultName = fileName.replaceFirst(Regex(""".*[/\\]"""), "").replaceFirst(Regex("""\.kt$"""), "").replaceFirstChar({ it.uppercase() }) + "Kt"
var jvmName = getJvmName(f) ?: defaultName
val qualClassName = if (pkg.isEmpty()) jvmName else "$pkg.$jvmName"
val label = "@\"class;$qualClassName\""
val id: Label<DbClass> = tw.getLabelFor(label, {
val fileId = tw.mkFileId(f.path, false)
val locId = tw.getWholeFileLocation(fileId)
val pkgId = extractPackage(pkg)
tw.writeClasses(it, jvmName, pkgId, it)
tw.writeFile_class(it)
tw.writeHasLocation(it, locId)
addModifiers(it, "public", "final")
})
return id
}
data class UseClassInstanceResult(val typeResult: TypeResult<DbClassorinterface>, val javaClass: IrClass)
fun useType(t: IrType, context: TypeContext = TypeContext.OTHER): TypeResults {
when(t) {
is IrSimpleType -> return useSimpleType(t, context)
else -> {
logger.error("Unrecognised IrType: " + t.javaClass)
return extractErrorType()
}
}
}
private fun extractJavaErrorType(): TypeResult<DbErrortype> {
val typeId = tw.getLabelFor<DbErrortype>("@\"errorType\"") {
tw.writeError_type(it)
}
return TypeResult(typeId, "<CodeQL error type>", "<CodeQL error type>")
}
private fun extractErrorType(): TypeResults {
val javaResult = extractJavaErrorType()
val kotlinTypeId = tw.getLabelFor<DbKt_nullable_type>("@\"errorKotlinType\"") {
tw.writeKt_nullable_types(it, javaResult.id)
}
return TypeResults(javaResult,
TypeResult(kotlinTypeId, "<CodeQL error type>", "<CodeQL error type>"))
}
fun getJavaEquivalentClass(c: IrClass) =
getJavaEquivalentClassId(c)?.let { pluginContext.referenceClass(it.asSingleFqName()) }?.owner
/**
* Gets a KotlinFileExtractor based on this one, except it attributes locations to the file that declares the given class.
*/
private fun withFileOfClass(cls: IrClass): KotlinFileExtractor {
val clsFile = cls.fileOrNull
if (this is KotlinFileExtractor && this.filePath == clsFile?.path) {
return this
}
if (clsFile == null || isExternalDeclaration(cls)) {
val filePath = getIrClassBinaryPath(cls)
val newTrapWriter = tw.makeFileTrapWriter(filePath, true)
val newLoggerTrapWriter = logger.tw.makeFileTrapWriter(filePath, false)
val newLogger = FileLogger(logger.loggerBase, newLoggerTrapWriter)
return KotlinFileExtractor(newLogger, newTrapWriter, filePath, dependencyCollector, externalClassExtractor, primitiveTypeMapping, pluginContext, globalExtensionState)
}
val newTrapWriter = tw.makeSourceFileTrapWriter(clsFile, true)
val newLoggerTrapWriter = logger.tw.makeSourceFileTrapWriter(clsFile, false)
val newLogger = FileLogger(logger.loggerBase, newLoggerTrapWriter)
return KotlinFileExtractor(newLogger, newTrapWriter, clsFile.path, dependencyCollector, externalClassExtractor, primitiveTypeMapping, pluginContext, globalExtensionState)
}
// The Kotlin compiler internal representation of Outer<T>.Inner<S>.InnerInner<R> is InnerInner<R, S, T>. This function returns just `R`.
fun removeOuterClassTypeArgs(c: IrClass, argsIncludingOuterClasses: List<IrTypeArgument>?): List<IrTypeArgument>? {
return argsIncludingOuterClasses?.let {
if (it.size > c.typeParameters.size)
it.take(c.typeParameters.size)
else
null
} ?: argsIncludingOuterClasses
}
private fun isStaticClass(c: IrClass) = c.visibility != DescriptorVisibilities.LOCAL && !c.isInner
// Gets nested inner classes starting at `c` and proceeding outwards to the innermost enclosing static class.
// For example, for (java syntax) `class A { static class B { class C { class D { } } } }`,
// `nonStaticParentsWithSelf(D)` = `[D, C, B]`.
private fun parentsWithTypeParametersInScope(c: IrClass): List<IrDeclarationParent> {
val parentsList = c.parentsWithSelf.toList()
val firstOuterClassIdx = parentsList.indexOfFirst { it is IrClass && isStaticClass(it) }
return if (firstOuterClassIdx == -1) parentsList else parentsList.subList(0, firstOuterClassIdx + 1)
}
// Gets the type parameter symbols that are in scope for class `c` in Kotlin order (i.e. for
// `class NotInScope<T> { static class OutermostInScope<A, B> { class QueryClass<C, D> { } } }`,
// `getTypeParametersInScope(QueryClass)` = `[C, D, A, B]`.
private fun getTypeParametersInScope(c: IrClass) =
parentsWithTypeParametersInScope(c).mapNotNull({ getTypeParameters(it) }).flatten()
// Returns a map from `c`'s type variables in scope to type arguments `argsIncludingOuterClasses`.
// Hack for the time being: the substituted types are always nullable, to prevent downstream code
// from replacing a generic parameter by a primitive. As and when we extract Kotlin types we will
// need to track this information in more detail.
private fun makeTypeGenericSubstitutionMap(c: IrClass, argsIncludingOuterClasses: List<IrTypeArgument>) =
getTypeParametersInScope(c).map({ it.symbol }).zip(argsIncludingOuterClasses.map { it.withQuestionMark(true) }).toMap()
fun makeGenericSubstitutionFunction(c: IrClass, argsIncludingOuterClasses: List<IrTypeArgument>) =
makeTypeGenericSubstitutionMap(c, argsIncludingOuterClasses).let {
{ x: IrType, useContext: TypeContext, pluginContext: IrPluginContext ->
x.substituteTypeAndArguments(
it,
useContext,
pluginContext
)
}
}
// The Kotlin compiler internal representation of Outer<A, B>.Inner<C, D>.InnerInner<E, F>.someFunction<G, H>.LocalClass<I, J> is LocalClass<I, J, G, H, E, F, C, D, A, B>. This function returns [A, B, C, D, E, F, G, H, I, J].
private fun orderTypeArgsLeftToRight(c: IrClass, argsIncludingOuterClasses: List<IrTypeArgument>?): List<IrTypeArgument>? {
if(argsIncludingOuterClasses.isNullOrEmpty())
return argsIncludingOuterClasses
val ret = ArrayList<IrTypeArgument>()
// Iterate over nested inner classes starting at `c`'s surrounding top-level or static nested class and ending at `c`, from the outermost inwards:
val truncatedParents = parentsWithTypeParametersInScope(c)
for(parent in truncatedParents.reversed()) {
val parentTypeParameters = getTypeParameters(parent)
val firstArgIdx = argsIncludingOuterClasses.size - (ret.size + parentTypeParameters.size)
ret.addAll(argsIncludingOuterClasses.subList(firstArgIdx, firstArgIdx + parentTypeParameters.size))
}
return ret
}
// `typeArgs` can be null to describe a raw generic type.
// For non-generic types it will be zero-length list.
fun useClassInstance(c: IrClass, typeArgs: List<IrTypeArgument>?, inReceiverContext: Boolean = false): UseClassInstanceResult {
if (c.isAnonymousObject) {
logger.error("Unexpected access to anonymous class instance")
}
val substituteClass = getJavaEquivalentClass(c)
val extractClass = substituteClass ?: c
// `KFunction1<T1,T2>` is substituted by `KFunction<T>`. The last type argument is the return type.
// Similarly Function23 and above get replaced by kotlin.jvm.functions.FunctionN with only one type arg, the result type.
// References to SomeGeneric<T1, T2, ...> where SomeGeneric is declared SomeGeneric<T1, T2, ...> are extracted
// as if they were references to the unbound type SomeGeneric.
val extractedTypeArgs = when {
extractClass.symbol.isKFunction() && typeArgs != null && typeArgs.isNotEmpty() -> listOf(typeArgs.last())
extractClass.fqNameWhenAvailable == FqName("kotlin.jvm.functions.FunctionN") && typeArgs != null && typeArgs.isNotEmpty() -> listOf(typeArgs.last())
typeArgs != null && isUnspecialised(c, typeArgs, logger) -> listOf()
else -> typeArgs
}
val classTypeResult = addClassLabel(extractClass, extractedTypeArgs, inReceiverContext)
// Extract both the Kotlin and equivalent Java classes, so that we have database entries
// for both even if all internal references to the Kotlin type are substituted.
if(c != extractClass) {
extractClassLaterIfExternal(c)
}
return UseClassInstanceResult(classTypeResult, extractClass)
}
private fun isArray(t: IrSimpleType) = t.isBoxedArray || t.isPrimitiveArray()
private fun extractClassLaterIfExternal(c: IrClass) {
if (isExternalDeclaration(c)) {
extractExternalClassLater(c)
}
}
private fun extractExternalEnclosingClassLater(d: IrDeclaration) {
when (val parent = d.parent) {
is IrClass -> extractExternalClassLater(parent)
is IrFunction -> extractExternalEnclosingClassLater(parent)
is IrFile -> logger.error("extractExternalEnclosingClassLater but no enclosing class.")
else -> logger.error("Unrecognised extractExternalEnclosingClassLater: " + d.javaClass)
}
}
private fun extractPropertyLaterIfExternalFileMember(p: IrProperty) {
if (isExternalFileClassMember(p)) {
extractExternalClassLater(p.parentAsClass)
dependencyCollector?.addDependency(p, externalClassExtractor.propertySignature)
externalClassExtractor.extractLater(p)
}
}
private fun extractFieldLaterIfExternalFileMember(f: IrField) {
if (isExternalFileClassMember(f)) {
extractExternalClassLater(f.parentAsClass)
dependencyCollector?.addDependency(f, externalClassExtractor.fieldSignature)
externalClassExtractor.extractLater(f)
}
}
private fun extractFunctionLaterIfExternalFileMember(f: IrFunction) {
if (isExternalFileClassMember(f)) {
extractExternalClassLater(f.parentAsClass)
(f as? IrSimpleFunction)?.correspondingPropertySymbol?.let {
extractPropertyLaterIfExternalFileMember(it.owner)
// No need to extract the function specifically, as the property's
// getters and setters are extracted alongside it
return
}
// Note we erase the parameter types before calling useType even though the signature should be the same
// in order to prevent an infinite loop through useTypeParameter -> useDeclarationParent -> useFunction
// -> extractFunctionLaterIfExternalFileMember, which would result for `fun <T> f(t: T) { ... }` for example.
val ext = f.extensionReceiverParameter
val parameters = if (ext != null) {
listOf(ext) + f.valueParameters
} else {
f.valueParameters
}
val paramSigs = parameters.map { useType(erase(it.type)).javaResult.signature }
val signature = paramSigs.joinToString(separator = ",", prefix = "(", postfix = ")")
dependencyCollector?.addDependency(f, signature)
externalClassExtractor.extractLater(f, signature)
}
}
fun extractExternalClassLater(c: IrClass) {
dependencyCollector?.addDependency(c)
externalClassExtractor.extractLater(c)
}
private fun tryReplaceAndroidSyntheticClass(c: IrClass): IrClass {
// The Android Kotlin Extensions Gradle plugin introduces synthetic functions, fields and classes. The most
// obvious signature is that they lack any supertype information even though they are not root classes.
// If possible, replace them by a real version of the same class.
if (c.superTypes.isNotEmpty() ||
c.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB ||
c.hasEqualFqName(FqName("java.lang.Object")))
return c
return globalExtensionState.syntheticToRealClassMap.getOrPut(c) {
val qualifiedName = c.fqNameWhenAvailable
if (qualifiedName == null) {
logger.warn("Failed to replace synthetic class ${c.name} because it has no fully qualified name")
return@getOrPut null
}
val result = pluginContext.referenceClass(qualifiedName)?.owner
if (result != null) {
logger.info("Replaced synthetic class ${c.name} with its real equivalent")
return@getOrPut result
}
// The above doesn't work for (some) generated nested classes, such as R$id, which should be R.id
val fqn = qualifiedName.asString()
if (fqn.indexOf('$') >= 0) {
val nested = pluginContext.referenceClass(FqName(fqn.replace('$', '.')))?.owner
if (nested != null) {
logger.info("Replaced synthetic nested class ${c.name} with its real equivalent")
return@getOrPut nested
}
}
logger.warn("Failed to replace synthetic class ${c.name}")
return@getOrPut null
} ?: c
}
private fun tryReplaceFunctionInSyntheticClass(f: IrFunction, getClassReplacement: (IrClass) -> IrClass): IrFunction {
val parentClass = f.parent as? IrClass ?: return f
val replacementClass = getClassReplacement(parentClass)
if (replacementClass === parentClass)
return f
return globalExtensionState.syntheticToRealFunctionMap.getOrPut(f) {
val result = replacementClass.declarations.findSubType<IrSimpleFunction> { replacementDecl ->
replacementDecl.name == f.name && replacementDecl.valueParameters.size == f.valueParameters.size && replacementDecl.valueParameters.zip(f.valueParameters).all {
erase(it.first.type) == erase(it.second.type)
}
}
if (result == null) {
logger.warn("Failed to replace synthetic class function ${f.name}")
} else {
logger.info("Replaced synthetic class function ${f.name} with its real equivalent")
}
result
} ?: f
}
fun tryReplaceSyntheticFunction(f: IrFunction): IrFunction {
val androidReplacement = tryReplaceFunctionInSyntheticClass(f) { tryReplaceAndroidSyntheticClass(it) }
return tryReplaceFunctionInSyntheticClass(androidReplacement) { tryReplaceParcelizeRawType(it)?.first ?: it }
}
fun tryReplaceAndroidSyntheticField(f: IrField): IrField {
val parentClass = f.parent as? IrClass ?: return f
val replacementClass = tryReplaceAndroidSyntheticClass(parentClass)
if (replacementClass === parentClass)
return f
return globalExtensionState.syntheticToRealFieldMap.getOrPut(f) {
val result = replacementClass.declarations.findSubType<IrField> { replacementDecl ->
replacementDecl.name == f.name
}
if (result == null) {
logger.warn("Failed to replace synthetic class field ${f.name}")
} else {
logger.info("Replaced synthetic class field ${f.name} with its real equivalent")
}
result
} ?: f
}
private fun tryReplaceType(cBeforeReplacement: IrClass, argsIncludingOuterClassesBeforeReplacement: List<IrTypeArgument>?): Pair<IrClass, List<IrTypeArgument>?> {
val c = tryReplaceAndroidSyntheticClass(cBeforeReplacement)
val p = tryReplaceParcelizeRawType(c)
return Pair(
p?.first ?: c,
p?.second ?: argsIncludingOuterClassesBeforeReplacement
)
}
// `typeArgs` can be null to describe a raw generic type.
// For non-generic types it will be zero-length list.
private fun addClassLabel(cBeforeReplacement: IrClass, argsIncludingOuterClassesBeforeReplacement: List<IrTypeArgument>?, inReceiverContext: Boolean = false): TypeResult<DbClassorinterface> {
val replaced = tryReplaceType(cBeforeReplacement, argsIncludingOuterClassesBeforeReplacement)
val replacedClass = replaced.first
val replacedArgsIncludingOuterClasses = replaced.second
val classLabelResult = getClassLabel(replacedClass, replacedArgsIncludingOuterClasses)
var instanceSeenBefore = true
val classLabel : Label<out DbClassorinterface> = tw.getLabelFor(classLabelResult.classLabel) {
instanceSeenBefore = false
extractClassLaterIfExternal(replacedClass)
}
if (replacedArgsIncludingOuterClasses == null || replacedArgsIncludingOuterClasses.isNotEmpty()) {
// If this is a generic type instantiation or a raw type then it has no
// source entity, so we need to extract it here
val extractorWithCSource by lazy { this.withFileOfClass(replacedClass) }
if (!instanceSeenBefore) {
extractorWithCSource.extractClassInstance(replacedClass, replacedArgsIncludingOuterClasses)
}
if (inReceiverContext && tw.lm.genericSpecialisationsExtracted.add(classLabelResult.classLabel)) {
val supertypeMode = if (replacedArgsIncludingOuterClasses == null) ExtractSupertypesMode.Raw else ExtractSupertypesMode.Specialised(replacedArgsIncludingOuterClasses)
extractorWithCSource.extractClassSupertypes(replacedClass, classLabel, supertypeMode, true)
extractorWithCSource.extractNonPrivateMemberPrototypes(replacedClass, replacedArgsIncludingOuterClasses, classLabel)
}
}
val fqName = replacedClass.fqNameWhenAvailable
val signature = if (fqName == null) {
logger.error("Unable to find signature/fqName for ${replacedClass.name}")
// TODO: Should we return null here instead?
"<no signature available>"
} else {
fqName.asString()
}
return TypeResult(
classLabel,
signature,
classLabelResult.shortName)
}
private fun tryReplaceParcelizeRawType(c: IrClass): Pair<IrClass, List<IrTypeArgument>?>? {
if (c.superTypes.isNotEmpty() ||
c.origin != IrDeclarationOrigin.DEFINED ||
c.hasEqualFqName(FqName("java.lang.Object"))) {
return null
}
val fqName = c.fqNameWhenAvailable
if (fqName == null) {
return null
}
fun tryGetPair(arity: Int): Pair<IrClass, List<IrTypeArgument>?>? {
val replaced = pluginContext.referenceClass(fqName)?.owner ?: return null
return Pair(replaced, List(arity) { makeTypeProjection(pluginContext.irBuiltIns.anyNType, Variance.INVARIANT) })
}
// The list of types handled here match https://github.com/JetBrains/kotlin/blob/d7c7d1efd2c0983c13b175e9e4b1cda979521159/plugins/parcelize/parcelize-compiler/src/org/jetbrains/kotlin/parcelize/ir/AndroidSymbols.kt
// Specifically, types are added for generic types created in AndroidSymbols.kt.
// This replacement is from a raw type to its matching parameterized type with `Object` type arguments.
return when (fqName.asString()) {
"java.util.ArrayList" -> tryGetPair(1)
"java.util.LinkedHashMap" -> tryGetPair(2)
"java.util.LinkedHashSet" -> tryGetPair(1)
"java.util.List" -> tryGetPair(1)
"java.util.TreeMap" -> tryGetPair(2)
"java.util.TreeSet" -> tryGetPair(1)
"java.lang.Class" -> tryGetPair(1)
else -> null
}
}
fun useAnonymousClass(c: IrClass) =
tw.lm.anonymousTypeMapping.getOrPut(c) {
TypeResults(
TypeResult(tw.getFreshIdLabel<DbClass>(), "", ""),
TypeResult(fakeKotlinType(), "TODO", "TODO")
)
}
fun getExistingAnonymousClassLabel(c: IrClass): Label<out DbType>? {
if (!c.isAnonymousObject){
return null
}
return tw.lm.anonymousTypeMapping[c]?.javaResult?.id
}
fun fakeKotlinType(): Label<out DbKt_type> {
val fakeKotlinPackageId: Label<DbPackage> = tw.getLabelFor("@\"FakeKotlinPackage\"", {
tw.writePackages(it, "fake.kotlin")
})
val fakeKotlinClassId: Label<DbClass> = tw.getLabelFor("@\"FakeKotlinClass\"", {
tw.writeClasses(it, "FakeKotlinClass", fakeKotlinPackageId, it)
})
val fakeKotlinTypeId: Label<DbKt_nullable_type> = tw.getLabelFor("@\"FakeKotlinType\"", {
tw.writeKt_nullable_types(it, fakeKotlinClassId)
})
return fakeKotlinTypeId
}
// `args` can be null to describe a raw generic type.
// For non-generic types it will be zero-length list.
fun useSimpleTypeClass(c: IrClass, args: List<IrTypeArgument>?, hasQuestionMark: Boolean): TypeResults {
if (c.isAnonymousObject) {
args?.let {
if (it.isNotEmpty() && !isUnspecialised(c, it, logger)) {
logger.error("Unexpected specialised instance of generic anonymous class")
}
}
return useAnonymousClass(c)
}
val classInstanceResult = useClassInstance(c, args)
val javaClassId = classInstanceResult.typeResult.id
val kotlinQualClassName = getUnquotedClassLabel(c, args).classLabel
val javaResult = classInstanceResult.typeResult
val kotlinResult = if (true) TypeResult(fakeKotlinType(), "TODO", "TODO") else
if (hasQuestionMark) {
val kotlinSignature = "$kotlinQualClassName?" // TODO: Is this right?
val kotlinLabel = "@\"kt_type;nullable;$kotlinQualClassName\""
val kotlinId: Label<DbKt_nullable_type> = tw.getLabelFor(kotlinLabel, {
tw.writeKt_nullable_types(it, javaClassId)
})
TypeResult(kotlinId, kotlinSignature, "TODO")
} else {
val kotlinSignature = kotlinQualClassName // TODO: Is this right?
val kotlinLabel = "@\"kt_type;notnull;$kotlinQualClassName\""
val kotlinId: Label<DbKt_notnull_type> = tw.getLabelFor(kotlinLabel, {
tw.writeKt_notnull_types(it, javaClassId)
})
TypeResult(kotlinId, kotlinSignature, "TODO")
}
return TypeResults(javaResult, kotlinResult)
}
// Given either a primitive array or a boxed array, returns primitive arrays unchanged,
// but returns boxed arrays with a nullable, invariant component type, with any nested arrays
// similarly transformed. For example, Array<out Array<in E>> would become Array<Array<E?>?>
// Array<*> will become Array<Any?>.
private fun getInvariantNullableArrayType(arrayType: IrSimpleType): IrSimpleType =
if (arrayType.isPrimitiveArray())
arrayType
else {
val componentType = arrayType.getArrayElementType(pluginContext.irBuiltIns)
val componentTypeBroadened = when (componentType) {
is IrSimpleType ->
if (isArray(componentType)) getInvariantNullableArrayType(componentType) else componentType
else -> componentType
}
val unchanged =
componentType == componentTypeBroadened &&
(arrayType.arguments[0] as? IrTypeProjection)?.variance == Variance.INVARIANT &&
componentType.isNullable()
if (unchanged)
arrayType
else
IrSimpleTypeImpl(
arrayType.classifier,
true,
listOf(makeTypeProjection(componentTypeBroadened, Variance.INVARIANT)),
listOf()
)
}
data class ArrayInfo(val elementTypeResults: TypeResults,
val componentTypeResults: TypeResults,
val dimensions: Int)
/**
* `t` is somewhere in a stack of array types, or possibly the
* element type of the innermost array. For example, in
* `Array<Array<Int>>`, we will be called with `t` being
* `Array<Array<Int>>`, then `Array<Int>`, then `Int`.
* `isPrimitiveArray` is true if we are immediately nested
* inside a primitive array.
*/
private fun useArrayType(t: IrType, isPrimitiveArray: Boolean): ArrayInfo {
if (!t.isBoxedArray && !t.isPrimitiveArray()) {
val nullableT = if (t.isPrimitiveType() && !isPrimitiveArray) t.makeNullable() else t
val typeResults = useType(nullableT)
return ArrayInfo(typeResults, typeResults, 0)
}
if (t !is IrSimpleType) {
logger.error("Unexpected non-simple array type: ${t.javaClass}")
return ArrayInfo(extractErrorType(), extractErrorType(), 0)
}
val arrayClass = t.classifier.owner
if (arrayClass !is IrClass) {
logger.error("Unexpected owner type for array type: ${arrayClass.javaClass}")
return ArrayInfo(extractErrorType(), extractErrorType(), 0)
}
// Because Java's arrays are covariant, Kotlin will render
// Array<in X> as Object[], Array<Array<in X>> as Object[][] etc.
val elementType = if ((t.arguments.singleOrNull() as? IrTypeProjection)?.variance == Variance.IN_VARIANCE) {
pluginContext.irBuiltIns.anyType
} else {
t.getArrayElementType(pluginContext.irBuiltIns)
}
val recInfo = useArrayType(elementType, t.isPrimitiveArray())
val javaShortName = recInfo.componentTypeResults.javaResult.shortName + "[]"
val kotlinShortName = recInfo.componentTypeResults.kotlinResult.shortName + "[]"
val elementTypeLabel = recInfo.elementTypeResults.javaResult.id
val componentTypeLabel = recInfo.componentTypeResults.javaResult.id
val dimensions = recInfo.dimensions + 1
val id = tw.getLabelFor<DbArray>("@\"array;$dimensions;{${elementTypeLabel}}\"") {
tw.writeArrays(
it,
javaShortName,
elementTypeLabel,
dimensions,
componentTypeLabel)
extractClassSupertypes(arrayClass, it, ExtractSupertypesMode.Specialised(t.arguments))
// array.length
val length = tw.getLabelFor<DbField>("@\"field;{$it};length\"")
val intTypeIds = useType(pluginContext.irBuiltIns.intType)
tw.writeFields(length, "length", intTypeIds.javaResult.id, it, length)
tw.writeFieldsKotlinType(length, intTypeIds.kotlinResult.id)
addModifiers(length, "public", "final")
// Note we will only emit one `clone()` method per Java array type, so we choose `Array<C?>` as its Kotlin
// return type, where C is the component type with any nested arrays themselves invariant and nullable.
val kotlinCloneReturnType = getInvariantNullableArrayType(t).makeNullable()
val kotlinCloneReturnTypeLabel = useType(kotlinCloneReturnType).kotlinResult.id
val clone = tw.getLabelFor<DbMethod>("@\"callable;{$it}.clone(){$it}\"")
tw.writeMethods(clone, "clone", "clone()", it, it, clone)
tw.writeMethodsKotlinType(clone, kotlinCloneReturnTypeLabel)
addModifiers(clone, "public")
}
val javaResult = TypeResult(
id,
recInfo.componentTypeResults.javaResult.signature + "[]",
javaShortName)
val kotlinResult = TypeResult(
fakeKotlinType(),
recInfo.componentTypeResults.kotlinResult.signature + "[]",
kotlinShortName)
val typeResults = TypeResults(javaResult, kotlinResult)
return ArrayInfo(recInfo.elementTypeResults, typeResults, dimensions)
}
enum class TypeContext {
RETURN, GENERIC_ARGUMENT, OTHER
}
private fun useSimpleType(s: IrSimpleType, context: TypeContext): TypeResults {
if (s.abbreviation != null) {
// TODO: Extract this information
}
// We use this when we don't actually have an IrClass for a class
// we want to refer to
// TODO: Eliminate the need for this if possible
fun makeClass(pkgName: String, className: String): Label<DbClass> {
val pkgId = extractPackage(pkgName)
val label = "@\"class;$pkgName.$className\""
val classId: Label<DbClass> = tw.getLabelFor(label, {
tw.writeClasses(it, className, pkgId, it)
})
return classId
}
fun primitiveType(kotlinClass: IrClass, primitiveName: String?,
otherIsPrimitive: Boolean,
javaClass: IrClass,
kotlinPackageName: String, kotlinClassName: String): TypeResults {
val javaResult = if ((context == TypeContext.RETURN || (context == TypeContext.OTHER && otherIsPrimitive)) && !s.isNullable() && primitiveName != null) {
val label: Label<DbPrimitive> = tw.getLabelFor("@\"type;$primitiveName\"", {
tw.writePrimitives(it, primitiveName)
})
TypeResult(label, primitiveName, primitiveName)
} else {
addClassLabel(javaClass, listOf())
}
val kotlinClassId = useClassInstance(kotlinClass, listOf()).typeResult.id
val kotlinResult = if (true) TypeResult(fakeKotlinType(), "TODO", "TODO") else
if (s.isNullable()) {
val kotlinSignature = "$kotlinPackageName.$kotlinClassName?" // TODO: Is this right?
val kotlinLabel = "@\"kt_type;nullable;$kotlinPackageName.$kotlinClassName\""
val kotlinId: Label<DbKt_nullable_type> = tw.getLabelFor(kotlinLabel, {
tw.writeKt_nullable_types(it, kotlinClassId)
})
TypeResult(kotlinId, kotlinSignature, "TODO")
} else {
val kotlinSignature = "$kotlinPackageName.$kotlinClassName" // TODO: Is this right?
val kotlinLabel = "@\"kt_type;notnull;$kotlinPackageName.$kotlinClassName\""
val kotlinId: Label<DbKt_notnull_type> = tw.getLabelFor(kotlinLabel, {
tw.writeKt_notnull_types(it, kotlinClassId)
})
TypeResult(kotlinId, kotlinSignature, "TODO")
}
return TypeResults(javaResult, kotlinResult)
}
val owner = s.classifier.owner
val primitiveInfo = primitiveTypeMapping.getPrimitiveInfo(s)
when {
primitiveInfo != null -> {
if (owner is IrClass) {
return primitiveType(
owner,
primitiveInfo.primitiveName, primitiveInfo.otherIsPrimitive,
primitiveInfo.javaClass,
primitiveInfo.kotlinPackageName, primitiveInfo.kotlinClassName
)
} else {
logger.error("Got primitive info for non-class (${owner.javaClass}) for ${s.render()}")
return extractErrorType()
}
}
(s.isBoxedArray && s.arguments.isNotEmpty()) || s.isPrimitiveArray() -> {
val arrayInfo = useArrayType(s, false)
return arrayInfo.componentTypeResults
}
owner is IrClass -> {
val args = if (s.isRawType()) null else s.arguments
return useSimpleTypeClass(owner, args, s.isNullable())
}
owner is IrTypeParameter -> {
val javaResult = useTypeParameter(owner)
val aClassId = makeClass("kotlin", "TypeParam") // TODO: Wrong
val kotlinResult = if (true) TypeResult(fakeKotlinType(), "TODO", "TODO") else
if (s.isNullable()) {
val kotlinSignature = "${javaResult.signature}?" // TODO: Wrong
val kotlinLabel = "@\"kt_type;nullable;type_param\"" // TODO: Wrong
val kotlinId: Label<DbKt_nullable_type> = tw.getLabelFor(kotlinLabel, {
tw.writeKt_nullable_types(it, aClassId)
})
TypeResult(kotlinId, kotlinSignature, "TODO")
} else {
val kotlinSignature = javaResult.signature // TODO: Wrong
val kotlinLabel = "@\"kt_type;notnull;type_param\"" // TODO: Wrong
val kotlinId: Label<DbKt_notnull_type> = tw.getLabelFor(kotlinLabel, {
tw.writeKt_notnull_types(it, aClassId)
})
TypeResult(kotlinId, kotlinSignature, "TODO")
}
return TypeResults(javaResult, kotlinResult)
}
else -> {
logger.error("Unrecognised IrSimpleType: " + s.javaClass + ": " + s.render())
return extractErrorType()
}
}
}
fun useDeclarationParent(
// The declaration parent according to Kotlin
dp: IrDeclarationParent,
// Whether the type of entity whose parent this is can be a
// top-level entity in the JVM's eyes. If so, then its parent may
// be a file; otherwise, if dp is a file foo.kt, then the parent
// is really the JVM class FooKt.
canBeTopLevel: Boolean,
classTypeArguments: List<IrTypeArgument>? = null,
inReceiverContext: Boolean = false):
Label<out DbElement>? =
when(dp) {
is IrFile ->
if(canBeTopLevel) {
usePackage(dp.fqName.asString())
} else {
extractFileClass(dp)
}
is IrClass ->
if (classTypeArguments != null && !dp.isAnonymousObject) {
useClassInstance(dp, classTypeArguments, inReceiverContext).typeResult.id
} else {
val replacedType = tryReplaceParcelizeRawType(dp)
if (replacedType == null) {
useClassSource(dp)
} else {
useClassInstance(replacedType.first, replacedType.second, inReceiverContext).typeResult.id
}
}
is IrFunction -> useFunction(dp)
is IrExternalPackageFragment -> {
// TODO
logger.error("Unhandled IrExternalPackageFragment")
null
}
else -> {
logger.error("Unrecognised IrDeclarationParent: " + dp.javaClass)
null
}
}
private val IrDeclaration.isAnonymousFunction get() = this is IrSimpleFunction && name == SpecialNames.NO_NAME_PROVIDED
data class FunctionNames(val nameInDB: String, val kotlinName: String)
@OptIn(ObsoleteDescriptorBasedAPI::class)
private fun getJvmModuleName(f: IrFunction) =
NameUtils.sanitizeAsJavaIdentifier(
getJvmModuleNameForDeserializedDescriptor(f.descriptor) ?: JvmCodegenUtil.getModuleName(pluginContext.moduleDescriptor)
)
fun getFunctionShortName(f: IrFunction) : FunctionNames {
if (f.origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA || f.isAnonymousFunction)
return FunctionNames(
OperatorNameConventions.INVOKE.asString(),
OperatorNameConventions.INVOKE.asString())
fun getSuffixIfInternal() =
if (f.visibility == DescriptorVisibilities.INTERNAL) {
"\$" + getJvmModuleName(f)
} else {
""
}
(f as? IrSimpleFunction)?.correspondingPropertySymbol?.let {
val propName = it.owner.name.asString()
val getter = it.owner.getter
val setter = it.owner.setter
if (it.owner.parentClassOrNull?.kind == ClassKind.ANNOTATION_CLASS) {
if (getter == null) {
logger.error("Expected to find a getter for a property inside an annotation class")
return FunctionNames(propName, propName)
} else {
val jvmName = getJvmName(getter)
return FunctionNames(jvmName ?: propName, propName)
}
}
val maybeFunctionName = when (f) {
getter -> JvmAbi.getterName(propName)
setter -> JvmAbi.setterName(propName)
else -> {
logger.error(
"Function has a corresponding property, but is neither the getter nor the setter"
)
null
}
}
maybeFunctionName?.let { defaultFunctionName ->
val suffix = if (f.visibility == DescriptorVisibilities.PRIVATE && f.origin == IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR) {
"\$private"
} else {
getSuffixIfInternal()
}
return FunctionNames(getJvmName(f) ?: "$defaultFunctionName$suffix", defaultFunctionName)
}
}
return FunctionNames(getJvmName(f) ?: "${f.name.asString()}${getSuffixIfInternal()}", f.name.asString())
}
// This excludes class type parameters that show up in (at least) constructors' typeParameters list.
fun getFunctionTypeParameters(f: IrFunction): List<IrTypeParameter> {
return if (f is IrConstructor) f.typeParameters else f.typeParameters.filter { it.parent == f }
}
private fun getTypeParameters(dp: IrDeclarationParent): List<IrTypeParameter> =
when(dp) {
is IrClass -> dp.typeParameters
is IrFunction -> getFunctionTypeParameters(dp)
else -> listOf()
}
private fun getEnclosingClass(it: IrDeclarationParent): IrClass? =
when(it) {
is IrClass -> it
is IrFunction -> getEnclosingClass(it.parent)
else -> null
}
val javaUtilCollection by lazy {
val result = pluginContext.referenceClass(FqName("java.util.Collection"))?.owner
result?.let { extractExternalClassLater(it) }
result
}
val wildcardCollectionType by lazy {
javaUtilCollection?.let {
it.symbol.typeWithArguments(listOf(IrStarProjectionImpl))
}
}
private fun makeCovariant(t: IrTypeArgument) =
t.typeOrNull?.let { makeTypeProjection(it, Variance.OUT_VARIANCE) } ?: t
private fun makeArgumentsCovariant(t: IrType) = (t as? IrSimpleType)?.let {
t.toBuilder().also { b -> b.arguments = b.arguments.map(this::makeCovariant) }.buildSimpleType()
} ?: t
fun eraseCollectionsMethodParameterType(t: IrType, collectionsMethodName: String, paramIdx: Int) =
when(collectionsMethodName) {
"contains", "remove", "containsKey", "containsValue", "get", "indexOf", "lastIndexOf" -> javaLangObjectType
"getOrDefault" -> if (paramIdx == 0) javaLangObjectType else null
"containsAll", "removeAll", "retainAll" -> wildcardCollectionType
// Kotlin defines these like addAll(Collection<E>); Java uses addAll(Collection<? extends E>)
"putAll", "addAll" -> makeArgumentsCovariant(t)
else -> null
} ?: t
private fun overridesFunctionDefinedOn(f: IrFunction, packageName: String, className: String) =
(f as? IrSimpleFunction)?.let {
it.overriddenSymbols.any { overridden ->
overridden.owner.parentClassOrNull?.let { defnClass ->
defnClass.name.asString() == className &&
defnClass.packageFqName?.asString() == packageName
} ?: false
}
} ?: false
@OptIn(ObsoleteDescriptorBasedAPI::class)
fun overridesCollectionsMethodWithAlteredParameterTypes(f: IrFunction) =
BuiltinMethodsWithSpecialGenericSignature.getOverriddenBuiltinFunctionWithErasedValueParametersInJava(f.descriptor) != null ||
(f.name.asString() == "putAll" && overridesFunctionDefinedOn(f, "kotlin.collections", "MutableMap")) ||
(f.name.asString() == "addAll" && overridesFunctionDefinedOn(f, "kotlin.collections", "MutableCollection")) ||
(f.name.asString() == "addAll" && overridesFunctionDefinedOn(f, "kotlin.collections", "MutableList"))
private val jvmWildcardAnnotation = FqName("kotlin.jvm.JvmWildcard")
private val jvmWildcardSuppressionAnnotaton = FqName("kotlin.jvm.JvmSuppressWildcards")
private fun arrayExtendsAdditionAllowed(t: IrSimpleType): Boolean =
// Note the array special case includes Array<*>, which does permit adding `? extends ...` (making `? extends Object[]` in that case)
// Surprisingly Array<in X> does permit this as well, though the contravariant array lowers to Object[] so this ends up `? extends Object[]` as well.
t.arguments[0].let {
when (it) {
is IrTypeProjection -> when (it.variance) {
Variance.INVARIANT -> false
Variance.IN_VARIANCE -> !(it.type.isAny() || it.type.isNullableAny())
Variance.OUT_VARIANCE -> extendsAdditionAllowed(it.type)
}
else -> true
}
}
private fun extendsAdditionAllowed(t: IrType) =
if (t.isBoxedArray) {
if (t is IrSimpleType) {
arrayExtendsAdditionAllowed(t)
} else {
logger.warn("Boxed array of unexpected kind ${t.javaClass}")
// Return false, for no particular reason
false
}
} else {
((t as? IrSimpleType)?.classOrNull?.owner?.isFinalClass) != true
}
private fun wildcardAdditionAllowed(v: Variance, t: IrType, addByDefault: Boolean) =
when {
t.hasAnnotation(jvmWildcardAnnotation) -> true
!addByDefault -> false
t.hasAnnotation(jvmWildcardSuppressionAnnotaton) -> false
v == Variance.IN_VARIANCE -> !(t.isNullableAny() || t.isAny())
v == Variance.OUT_VARIANCE -> extendsAdditionAllowed(t)
else -> false
}
private fun addJavaLoweringArgumentWildcards(p: IrTypeParameter, t: IrTypeArgument, addByDefault: Boolean, javaType: JavaType?): IrTypeArgument =
(t as? IrTypeProjection)?.let {
val newBase = addJavaLoweringWildcards(it.type, addByDefault, javaType)
val newVariance =
if (it.variance == Variance.INVARIANT &&
p.variance != Variance.INVARIANT &&
// The next line forbids inferring a wildcard type when we have a corresponding Java type with conflicting variance.
// For example, Java might declare f(Comparable<CharSequence> cs), in which case we shouldn't add a `? super ...`
// wildcard. Note if javaType is unknown (e.g. this is a Kotlin source element), we assume wildcards should be added.
(javaType?.let { jt -> jt is JavaWildcardType && jt.isExtends == (p.variance == Variance.OUT_VARIANCE) } != false) &&
wildcardAdditionAllowed(p.variance, it.type, addByDefault))
p.variance
else
it.variance
if (newBase !== it.type || newVariance != it.variance)
makeTypeProjection(newBase, newVariance)
else
null
} ?: t
private fun getJavaTypeArgument(jt: JavaType, idx: Int) =
when(jt) {
is JavaClassifierType -> jt.typeArguments.getOrNull(idx)
is JavaArrayType -> if (idx == 0) jt.componentType else null
else -> null
}
fun addJavaLoweringWildcards(t: IrType, addByDefault: Boolean, javaType: JavaType?): IrType =
(t as? IrSimpleType)?.let {
val typeParams = it.classOrNull?.owner?.typeParameters ?: return t
val newArgs = typeParams.zip(it.arguments).mapIndexed { idx, pair ->
addJavaLoweringArgumentWildcards(
pair.first,
pair.second,
addByDefault,
javaType?.let { jt -> getJavaTypeArgument(jt, idx) }
)
}
return if (newArgs.zip(it.arguments).all { pair -> pair.first === pair.second })
t
else
it.toBuilder().also { builder -> builder.arguments = newArgs }.buildSimpleType()
} ?: t
/*
* This is the normal getFunctionLabel function to use. If you want
* to refer to the function in its source class then
* classTypeArgsIncludingOuterClasses should be null. Otherwise, it
* is the list of type arguments that need to be applied to its
* enclosing classes to get the instantiation that this function is
* in.
*/
fun getFunctionLabel(f: IrFunction, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) : String {
return getFunctionLabel(f, null, classTypeArgsIncludingOuterClasses)
}
/*
* There are some pairs of classes (e.g. `kotlin.Throwable` and
* `java.lang.Throwable`) which are really just 2 different names
* for the same class. However, we extract them as separate
* classes. When extracting `kotlin.Throwable`'s methods, if we
* looked up the parent ID ourselves, we would get as ID for
* `java.lang.Throwable`, which isn't what we want. So we have to
* allow it to be passed in.
*/
@OptIn(ObsoleteDescriptorBasedAPI::class)
fun getFunctionLabel(f: IrFunction, maybeParentId: Label<out DbElement>?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) =
getFunctionLabel(
f.parent,
maybeParentId,
getFunctionShortName(f).nameInDB,
f.valueParameters,
getAdjustedReturnType(f),
f.extensionReceiverParameter,
getFunctionTypeParameters(f),
classTypeArgsIncludingOuterClasses,
overridesCollectionsMethodWithAlteredParameterTypes(f),
getJavaCallable(f),
!hasWildcardSuppressionAnnotation(f)
)
/*
* This function actually generates the label for a function.
* Sometimes, a function is only generated by kotlinc when writing a
* class file, so there is no corresponding `IrFunction` for it.
* This function therefore takes all the constituent parts of a
* function instead.
*/
fun getFunctionLabel(
// The parent of the function; normally f.parent.
parent: IrDeclarationParent,
// The ID of the function's parent, or null if we should work it out ourselves.
maybeParentId: Label<out DbElement>?,
// The name of the function; normally f.name.asString().
name: String,
// The value parameters that the functions takes; normally f.valueParameters.
parameters: List<IrValueParameter>,
// The return type of the function; normally f.returnType.
returnType: IrType,
// The extension receiver of the function, if any; normally f.extensionReceiverParameter.
extensionReceiverParameter: IrValueParameter?,
// The type parameters of the function. This does not include type parameters of enclosing classes.
functionTypeParameters: List<IrTypeParameter>,
// The type arguments of enclosing classes of the function.
classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?,
// If true, this method implements a Java Collections interface (Collection, Map or List) and may need
// parameter erasure to match the way this class will appear to an external consumer of the .class file.
overridesCollectionsMethod: Boolean,
// The Java signature of this callable, if known.
javaSignature: JavaMember?,
// If true, Java wildcards implied by Kotlin type parameter variance should be added by default to this function's value parameters' types.
// (Return-type wildcard addition is always off by default)
addParameterWildcardsByDefault: Boolean,
// The prefix used in the label. "callable", unless a property label is created, then it's "property".
prefix: String = "callable"
): String {
val parentId = maybeParentId ?: useDeclarationParent(parent, false, classTypeArgsIncludingOuterClasses, true)
val allParams = if (extensionReceiverParameter == null) {
parameters
} else {
listOf(extensionReceiverParameter) + parameters
}
val substitutionMap = classTypeArgsIncludingOuterClasses?.let { notNullArgs ->
if (notNullArgs.isEmpty()) {
null
} else {
val enclosingClass = getEnclosingClass(parent)
enclosingClass?.let { notNullClass -> makeTypeGenericSubstitutionMap(notNullClass, notNullArgs) }
}
}
val getIdForFunctionLabel = { it: IndexedValue<IrValueParameter> ->
// Kotlin rewrites certain Java collections types adding additional generic constraints-- for example,
// Collection.remove(Object) because Collection.remove(Collection::E) in the Kotlin universe.
// If this has happened, erase the type again to get the correct Java signature.
val maybeAmendedForCollections = if (overridesCollectionsMethod) eraseCollectionsMethodParameterType(it.value.type, name, it.index) else it.value.type
// Add any wildcard types that the Kotlin compiler would add in the Java lowering of this function:
val withAddedWildcards = addJavaLoweringWildcards(maybeAmendedForCollections, addParameterWildcardsByDefault, javaSignature?.let { sig -> getJavaValueParameterType(sig, it.index) })
// Now substitute any class type parameters in:
val maybeSubbed = withAddedWildcards.substituteTypeAndArguments(substitutionMap, TypeContext.OTHER, pluginContext)
// 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 = if (functionTypeParameters.isEmpty()) maybeSubbed else erase(maybeSubbed)
"{${useType(maybeErased).javaResult.id}}"
}
val paramTypeIds = allParams.withIndex().joinToString(separator = ",", transform = getIdForFunctionLabel)
val labelReturnType =
if (name == "<init>")
pluginContext.irBuiltIns.unitType
else
erase(returnType.substituteTypeAndArguments(substitutionMap, TypeContext.RETURN, pluginContext))
// Note that `addJavaLoweringWildcards` is not required here because the return type used to form the function
// label is always erased.
val returnTypeId = useType(labelReturnType, TypeContext.RETURN).javaResult.id
// This suffix is added to generic methods (and constructors) to match the Java extractor's behaviour.
// Comments in that extractor indicates it didn't want the label of the callable to clash with the raw
// method (and presumably that disambiguation is never needed when the method belongs to a parameterized
// instance of a generic class), but as of now I don't know when the raw method would be referred to.
val typeArgSuffix = if (functionTypeParameters.isNotEmpty() && classTypeArgsIncludingOuterClasses.isNullOrEmpty()) "<${functionTypeParameters.size}>" else "";
return "@\"$prefix;{$parentId}.$name($paramTypeIds){$returnTypeId}${typeArgSuffix}\""
}
val javaLangClass by lazy {
val result = pluginContext.referenceClass(FqName("java.lang.Class"))?.owner
result?.let { extractExternalClassLater(it) }
result
}
fun kClassToJavaClass(t: IrType): IrType {
when(t) {
is IrSimpleType -> {
if (t.classifier == pluginContext.irBuiltIns.kClassClass) {
javaLangClass?.let { jlc ->
return jlc.symbol.typeWithArguments(t.arguments)
}
} else {
t.classOrNull?.let { tCls ->
if (t.isArray() || t.isNullableArray()) {
(t.arguments.singleOrNull() as? IrTypeProjection)?.let { elementTypeArg ->
val elementType = elementTypeArg.type
val replacedElementType = kClassToJavaClass(elementType)
if (replacedElementType !== elementType) {
val newArg = makeTypeProjection(replacedElementType, elementTypeArg.variance)
return tCls.typeWithArguments(listOf(newArg)).codeQlWithHasQuestionMark(t.isNullableArray())
}
}
}
}
}
}
}
return t
}
fun isAnnotationClassField(f: IrField) =
f.correspondingPropertySymbol?.let {
isAnnotationClassProperty(it)
} ?: false
private fun isAnnotationClassProperty(p: IrPropertySymbol) =
p.owner.parentClassOrNull?.kind == ClassKind.ANNOTATION_CLASS
fun getAdjustedReturnType(f: IrFunction) : IrType {
// Replace annotation val accessor types as needed:
(f as? IrSimpleFunction)?.correspondingPropertySymbol?.let {
if (isAnnotationClassProperty(it) && f == it.owner.getter) {
val replaced = kClassToJavaClass(f.returnType)
if (replaced != f.returnType)
return replaced
}
}
// The return type of `java.util.concurrent.ConcurrentHashMap<K,V>.keySet/0` is defined as `Set<K>` in the stubs inside the Android SDK.
// This does not match the Java SDK return type: `ConcurrentHashMap.KeySetView<K,V>`, so it's adjusted here.
// This is a deliberate change in the Android SDK: https://github.com/AndroidSDKSources/android-sdk-sources-for-api-level-31/blob/2c56b25f619575bea12f9c5520ed2259620084ac/java/util/concurrent/ConcurrentHashMap.java#L1244-L1249
// The annotation on the source is not visible in the android.jar, so we can't make the change based on that.
// TODO: there are other instances of `dalvik.annotation.codegen.CovariantReturnType` in the Android SDK, we should handle those too if they cause DB inconsistencies
val parentClass = f.parentClassOrNull
if (parentClass == null ||
parentClass.fqNameWhenAvailable?.asString() != "java.util.concurrent.ConcurrentHashMap" ||
getFunctionShortName(f).nameInDB != "keySet" ||
f.valueParameters.isNotEmpty() ||
f.returnType.classFqName?.asString() != "kotlin.collections.MutableSet") {
return f.returnType
}
val otherKeySet = parentClass.declarations.findSubType<IrFunction> { it.name.asString() == "keySet" && it.valueParameters.size == 1 }
?: return f.returnType
return otherKeySet.returnType.codeQlWithHasQuestionMark(false)
}
@OptIn(ObsoleteDescriptorBasedAPI::class)
fun getJavaCallable(f: IrFunction) = (f.descriptor.source as? JavaSourceElement)?.javaElement as? JavaMember
fun getJavaValueParameterType(m: JavaMember, idx: Int) = when(m) {
is JavaMethod -> m.valueParameters[idx].type
is JavaConstructor -> m.valueParameters[idx].type
else -> null
}
fun hasWildcardSuppressionAnnotation(d: IrDeclaration) =
d.hasAnnotation(jvmWildcardSuppressionAnnotaton) ||
// Note not using `parentsWithSelf` as that only works if `d` is an IrDeclarationParent
d.parents.any { (it as? IrAnnotationContainer)?.hasAnnotation(jvmWildcardSuppressionAnnotaton) == true }
/**
* Class to hold labels for generated classes around local functions, lambdas, function references, and property references.
*/
open class GeneratedClassLabels(val type: TypeResults, val constructor: Label<DbConstructor>, val constructorBlock: Label<DbBlock>)
/**
* Class to hold labels generated for locally visible functions, such as
* - local functions,
* - lambdas, and
* - wrappers around function references.
*/
class LocallyVisibleFunctionLabels(type: TypeResults, constructor: Label<DbConstructor>, constructorBlock: Label<DbBlock>, val function: Label<DbMethod>)
: GeneratedClassLabels(type, constructor, constructorBlock)
/**
* Gets the labels for functions belonging to
* - local functions, and
* - lambdas.
*/
fun getLocallyVisibleFunctionLabels(f: IrFunction): LocallyVisibleFunctionLabels {
if (!f.isLocalFunction()){
logger.error("Extracting a non-local function as a local one")
}
var res = tw.lm.locallyVisibleFunctionLabelMapping[f]
if (res == null) {
val javaResult = TypeResult(tw.getFreshIdLabel<DbClass>(), "", "")
val kotlinResult = TypeResult(tw.getFreshIdLabel<DbKt_notnull_type>(), "", "")
tw.writeKt_notnull_types(kotlinResult.id, javaResult.id)
res = LocallyVisibleFunctionLabels(
TypeResults(javaResult, kotlinResult),
tw.getFreshIdLabel(),
tw.getFreshIdLabel(),
tw.getFreshIdLabel()
)
tw.lm.locallyVisibleFunctionLabelMapping[f] = res
}
return res
}
fun getExistingLocallyVisibleFunctionLabel(f: IrFunction): Label<DbMethod>? {
if (!f.isLocalFunction()){
return null
}
return tw.lm.locallyVisibleFunctionLabelMapping[f]?.function
}
// These are classes with Java equivalents, but whose methods don't all exist on those Java equivalents--
// for example, the numeric classes define arithmetic functions (Int.plus, Long.or and so on) that lower to
// primitive arithmetic on the JVM, but which we extract as calls to reflect the source syntax more closely.
private val expectedMissingEquivalents = setOf(
"kotlin.Boolean", "kotlin.Byte", "kotlin.Char", "kotlin.Double", "kotlin.Float", "kotlin.Int", "kotlin.Long", "kotlin.Number", "kotlin.Short"
)
private fun kotlinFunctionToJavaEquivalent(f: IrFunction, noReplace: Boolean): IrFunction =
if (noReplace)
f
else
f.parentClassOrNull?.let { parentClass ->
getJavaEquivalentClass(parentClass)?.let { javaClass ->
if (javaClass != parentClass) {
val jvmName = getJvmName(f) ?: f.name.asString()
// Look for an exact type match...
javaClass.declarations.findSubType<IrFunction> { decl ->
decl.name.asString() == jvmName &&
decl.valueParameters.size == f.valueParameters.size &&
// Note matching by classifier not the whole type so that generic arguments are allowed to differ,
// as they always will for method type parameters occurring in parameter types (e.g. <T> toArray(T[] array)
// Differing only by nullability would also be insignificant if it came up.
decl.valueParameters.zip(f.valueParameters).all { p -> p.first.type.classifierOrNull == p.second.type.classifierOrNull }
} ?:
// Or if there is none, look for the only viable overload
javaClass.declarations.singleOrNullSubType<IrFunction> { decl ->
decl.name.asString() == jvmName &&
decl.valueParameters.size == f.valueParameters.size
} ?:
// Or check property accessors:
(f.propertyIfAccessor as? IrProperty)?.let { kotlinProp ->
val javaProp = javaClass.declarations.findSubType<IrProperty> { decl ->
decl.name == kotlinProp.name
}
if (javaProp?.getter?.name == f.name)
javaProp.getter
else if (javaProp?.setter?.name == f.name)
javaProp.setter
else null
} ?: run {
val parentFqName = parentClass.fqNameWhenAvailable?.asString()
if (!expectedMissingEquivalents.contains(parentFqName)) {
logger.warn("Couldn't find a Java equivalent function to $parentFqName.${f.name} in ${javaClass.fqNameWhenAvailable}")
}
null
}
}
else
null
}
} ?: f
fun <T: DbCallable> useFunction(f: IrFunction, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>? = null, noReplace: Boolean = false): Label<out T> {
return useFunction(f, null, classTypeArgsIncludingOuterClasses, noReplace)
}
fun <T: DbCallable> useFunction(f: IrFunction, parentId: Label<out DbElement>?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?, noReplace: Boolean = false): Label<out T> {
if (f.isLocalFunction()) {
val ids = getLocallyVisibleFunctionLabels(f)
return ids.function.cast<T>()
}
val javaFun = kotlinFunctionToJavaEquivalent(f, noReplace)
val label = getFunctionLabel(javaFun, parentId, classTypeArgsIncludingOuterClasses)
val id: Label<T> = tw.getLabelFor(label)
if (isExternalDeclaration(javaFun)) {
extractFunctionLaterIfExternalFileMember(javaFun)
extractExternalEnclosingClassLater(javaFun)
}
return id
}
fun getTypeArgumentLabel(
arg: IrTypeArgument
): TypeResultWithoutSignature<DbReftype> {
fun extractBoundedWildcard(wildcardKind: Int, wildcardLabelStr: String, wildcardShortName: String, boundLabel: Label<out DbReftype>): Label<DbWildcard> =
tw.getLabelFor(wildcardLabelStr) { wildcardLabel ->
tw.writeWildcards(wildcardLabel, wildcardShortName, wildcardKind)
tw.writeHasLocation(wildcardLabel, tw.unknownLocation)
tw.getLabelFor<DbTypebound>("@\"bound;0;{$wildcardLabel}\"") {
tw.writeTypeBounds(it, boundLabel, 0, wildcardLabel)
}
}
// Note this function doesn't return a signature because type arguments are never incorporated into function signatures.
return when (arg) {
is IrStarProjection -> {
val anyTypeLabel = useType(pluginContext.irBuiltIns.anyType).javaResult.id.cast<DbReftype>()
TypeResultWithoutSignature(extractBoundedWildcard(1, "@\"wildcard;\"", "?", anyTypeLabel), Unit, "?")
}
is IrTypeProjection -> {
val boundResults = useType(arg.type, TypeContext.GENERIC_ARGUMENT)
val boundLabel = boundResults.javaResult.id.cast<DbReftype>()
return if(arg.variance == Variance.INVARIANT)
boundResults.javaResult.cast<DbReftype>().forgetSignature()
else {
val keyPrefix = if (arg.variance == Variance.IN_VARIANCE) "super" else "extends"
val wildcardKind = if (arg.variance == Variance.IN_VARIANCE) 2 else 1
val wildcardShortName = "? $keyPrefix ${boundResults.javaResult.shortName}"
TypeResultWithoutSignature(
extractBoundedWildcard(wildcardKind, "@\"wildcard;$keyPrefix{$boundLabel}\"", wildcardShortName, boundLabel),
Unit,
wildcardShortName)
}
}
else -> {
logger.error("Unexpected type argument.")
return extractJavaErrorType().forgetSignature()
}
}
}
data class ClassLabelResults(
val classLabel: String, val shortName: String
)
/**
* This returns the `X` in c's label `@"class;X"`.
*
* `argsIncludingOuterClasses` can be null to describe a raw generic type.
* For non-generic types it will be zero-length list.
*/
private fun getUnquotedClassLabel(c: IrClass, argsIncludingOuterClasses: List<IrTypeArgument>?): ClassLabelResults {
val pkg = c.packageFqName?.asString() ?: ""
val cls = c.name.asString()
val label = when (val parent = c.parent) {
is IrClass -> {
"${getUnquotedClassLabel(parent, listOf()).classLabel}\$$cls"
}
is IrFunction -> {
"{${useFunction<DbMethod>(parent)}}.$cls"
}
is IrField -> {
"{${useField(parent)}}.$cls"
}
else -> {
if (pkg.isEmpty()) cls else "$pkg.$cls"
}
}
val reorderedArgs = orderTypeArgsLeftToRight(c, argsIncludingOuterClasses)
val typeArgLabels = reorderedArgs?.map { getTypeArgumentLabel(it) }
val typeArgsShortName =
if (typeArgLabels == null)
"<>"
else if(typeArgLabels.isEmpty())
""
else
typeArgLabels.takeLast(c.typeParameters.size).joinToString(prefix = "<", postfix = ">", separator = ",") { it.shortName }
return ClassLabelResults(
label + (typeArgLabels?.joinToString(separator = "") { ";{${it.id}}" } ?: "<>"),
cls + typeArgsShortName
)
}
// `args` can be null to describe a raw generic type.
// For non-generic types it will be zero-length list.
fun getClassLabel(c: IrClass, argsIncludingOuterClasses: List<IrTypeArgument>?): ClassLabelResults {
if (c.isAnonymousObject) {
logger.error("Label generation should not be requested for an anonymous class")
}
val unquotedLabel = getUnquotedClassLabel(c, argsIncludingOuterClasses)
return ClassLabelResults(
"@\"class;${unquotedLabel.classLabel}\"",
unquotedLabel.shortName)
}
fun useClassSource(c: IrClass): Label<out DbClassorinterface> {
if (c.isAnonymousObject) {
return useAnonymousClass(c).javaResult.id.cast<DbClass>()
}
// For source classes, the label doesn't include any type arguments
val classTypeResult = addClassLabel(c, listOf())
return classTypeResult.id
}
fun getTypeParameterParentLabel(param: IrTypeParameter) =
param.parent.let {
when (it) {
is IrClass -> useClassSource(it)
is IrFunction -> useFunction(it, noReplace = true)
else -> { logger.error("Unexpected type parameter parent $it"); null }
}
}
fun getTypeParameterLabel(param: IrTypeParameter): String {
// Use this instead of `useDeclarationParent` so we can use useFunction with noReplace = true,
// ensuring that e.g. a method-scoped type variable declared on kotlin.String.transform <R> gets
// a different name to the corresponding java.lang.String.transform <R>, even though useFunction
// will usually replace references to one function with the other.
val parentLabel = getTypeParameterParentLabel(param)
return "@\"typevar;{$parentLabel};${param.name}\""
}
private fun useTypeParameter(param: IrTypeParameter) =
TypeResult(
tw.getLabelFor<DbTypevariable>(getTypeParameterLabel(param)),
useType(eraseTypeParameter(param)).javaResult.signature,
param.name.asString()
)
private fun extractModifier(m: String): Label<DbModifier> {
val modifierLabel = "@\"modifier;$m\""
val id: Label<DbModifier> = tw.getLabelFor(modifierLabel, {
tw.writeModifiers(it, m)
})
return id
}
fun addModifiers(modifiable: Label<out DbModifiable>, vararg modifiers: String) =
modifiers.forEach { tw.writeHasModifier(modifiable, extractModifier(it)) }
sealed class ExtractSupertypesMode {
object Unbound : ExtractSupertypesMode()
object Raw : ExtractSupertypesMode()
data class Specialised(val typeArgs : List<IrTypeArgument>) : ExtractSupertypesMode()
}
/**
* Extracts the supertypes of class `c`, either the unbound version, raw version or a specialisation to particular
* type arguments, depending on the value of `mode`. `id` is the label of this class or class instantiation.
*
* For example, for type `List` if `mode` `Specialised([String])` then we will extract the supertypes
* of `List<String>`, i.e. `Appendable<String>` etc, or if `mode` is `Unbound` we will extract `Appendable<E>`
* where `E` is the type variable declared as `List<E>`. Finally if `mode` is `Raw` we will extract the raw type
* `Appendable`, represented in QL as `Appendable<>`.
*
* Argument `inReceiverContext` will be passed onto the `useClassInstance` invocation for each supertype.
*/
fun extractClassSupertypes(c: IrClass, id: Label<out DbReftype>, mode: ExtractSupertypesMode = ExtractSupertypesMode.Unbound, inReceiverContext: Boolean = false) {
extractClassSupertypes(c.superTypes, c.typeParameters, id, mode, inReceiverContext)
}
fun extractClassSupertypes(superTypes: List<IrType>, typeParameters: List<IrTypeParameter>, id: Label<out DbReftype>, mode: ExtractSupertypesMode = ExtractSupertypesMode.Unbound, inReceiverContext: Boolean = false) {
// Note we only need to substitute type args here because it is illegal to directly extend a type variable.
// (For example, we can't have `class A<E> : E`, but can have `class A<E> : Comparable<E>`)
val subbedSupertypes = when(mode) {
is ExtractSupertypesMode.Specialised -> {
superTypes.map {
it.substituteTypeArguments(typeParameters, mode.typeArgs)
}
}
else -> superTypes
}
for(t in subbedSupertypes) {
when(t) {
is IrSimpleType -> {
val owner = t.classifier.owner
when (owner) {
is IrClass -> {
val typeArgs = if (t.arguments.isNotEmpty() && mode is ExtractSupertypesMode.Raw) null else t.arguments
val l = useClassInstance(owner, typeArgs, inReceiverContext).typeResult.id
tw.writeExtendsReftype(id, l)
}
else -> {
logger.error("Unexpected simple type supertype: " + t.javaClass + ": " + t.render())
}
}
} else -> {
logger.error("Unexpected supertype: " + t.javaClass + ": " + t.render())
}
}
}
}
fun useValueDeclaration(d: IrValueDeclaration): Label<out DbVariable>? =
when(d) {
is IrValueParameter -> useValueParameter(d, null)
is IrVariable -> useVariable(d)
else -> {
logger.error("Unrecognised IrValueDeclaration: " + d.javaClass)
null
}
}
/**
* Returns `t` with generic types replaced by raw types, and type parameters replaced by their first bound.
*
* Note that `Array<T>` is retained (with `T` itself erased) because these are expected to be lowered to Java
* arrays, which are not generic.
*/
private fun erase (t: IrType): IrType {
if (t is IrSimpleType) {
val classifier = t.classifier
val owner = classifier.owner
if(owner is IrTypeParameter) {
return eraseTypeParameter(owner)
}
if (owner is IrClass) {
if (t.isArray() || t.isNullableArray()) {
val elementType = t.getArrayElementType(pluginContext.irBuiltIns)
val erasedElementType = erase(elementType)
return owner.typeWith(erasedElementType).codeQlWithHasQuestionMark(t.isNullable())
}
return if (t.arguments.isNotEmpty())
t.addAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
else
t
}
}
return t
}
private fun eraseTypeParameter(t: IrTypeParameter) =
erase(t.superTypes[0])
/**
* Gets the label for `vp` in the context of function instance `parent`, or in that of its declaring function if
* `parent` is null.
*/
fun getValueParameterLabel(vp: IrValueParameter, parent: Label<out DbCallable>?): String {
val declarationParent = vp.parent
val parentId = parent ?: useDeclarationParent(declarationParent, false)
val idx = if (declarationParent is IrFunction && declarationParent.extensionReceiverParameter != null)
// For extension functions increase the index to match what the java extractor sees:
vp.index + 1
else
vp.index
if (idx < 0) {
// We're not extracting this and this@TYPE parameters of functions:
logger.error("Unexpected negative index for parameter")
}
return "@\"params;{$parentId};$idx\""
}
fun useValueParameter(vp: IrValueParameter, parent: Label<out DbCallable>?): Label<out DbParam> =
tw.getLabelFor(getValueParameterLabel(vp, parent))
private fun isDirectlyExposedCompanionObjectField(f: IrField) =
f.hasAnnotation(FqName("kotlin.jvm.JvmField")) ||
f.correspondingPropertySymbol?.owner?.let {
it.isConst || it.isLateinit
} ?: false
fun getFieldParent(f: IrField) =
f.parentClassOrNull?.let {
if (it.isCompanion && isDirectlyExposedCompanionObjectField(f))
it.parent
else
null
} ?: f.parent
// Gets a field's corresponding property's extension receiver type, if any
fun getExtensionReceiverType(f: IrField) =
f.correspondingPropertySymbol?.owner?.let {
(it.getter ?: it.setter)?.extensionReceiverParameter?.type
}
fun getFieldLabel(f: IrField): String {
val parentId = useDeclarationParent(getFieldParent(f), false)
// Distinguish backing fields of properties based on their extension receiver type;
// otherwise two extension properties declared in the same enclosing context will get
// clashing trap labels. These are always private, so we can just make up a label without
// worrying about their names as seen from Java.
val extensionPropertyDiscriminator = getExtensionReceiverType(f)?.let { "extension;${useType(it).javaResult.id}" } ?: ""
return "@\"field;{$parentId};${extensionPropertyDiscriminator}${f.name.asString()}\""
}
fun useField(f: IrField): Label<out DbField> =
tw.getLabelFor<DbField>(getFieldLabel(f)).also { extractFieldLaterIfExternalFileMember(f) }
fun getPropertyLabel(p: IrProperty): String? {
val parentId = useDeclarationParent(p.parent, false)
if (parentId == null) {
return null
} else {
return getPropertyLabel(p, parentId, null)
}
}
private fun getPropertyLabel(p: IrProperty, parentId: Label<out DbElement>, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?): String {
val getter = p.getter
val setter = p.setter
val func = getter ?: setter
val ext = func?.extensionReceiverParameter
return if (ext == null) {
"@\"property;{$parentId};${p.name.asString()}\""
} else {
val returnType = getter?.returnType ?: setter?.valueParameters?.singleOrNull()?.type ?: pluginContext.irBuiltIns.unitType
val typeParams = getFunctionTypeParameters(func)
getFunctionLabel(p.parent, parentId, p.name.asString(), listOf(), returnType, ext, typeParams, classTypeArgsIncludingOuterClasses, overridesCollectionsMethod = false, javaSignature = null, addParameterWildcardsByDefault = false, prefix = "property")
}
}
fun useProperty(p: IrProperty, parentId: Label<out DbElement>, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?): Label<out DbKt_property> =
tw.getLabelFor<DbKt_property>(getPropertyLabel(p, parentId, classTypeArgsIncludingOuterClasses)).also { extractPropertyLaterIfExternalFileMember(p) }
fun getEnumEntryLabel(ee: IrEnumEntry): String {
val parentId = useDeclarationParent(ee.parent, false)
return "@\"field;{$parentId};${ee.name.asString()}\""
}
fun useEnumEntry(ee: IrEnumEntry): Label<out DbField> =
tw.getLabelFor(getEnumEntryLabel(ee))
fun getTypeAliasLabel(ta: IrTypeAlias): String {
val parentId = useDeclarationParent(ta.parent, true)
return "@\"type_alias;{$parentId};${ta.name.asString()}\""
}
fun useTypeAlias(ta: IrTypeAlias): Label<out DbKt_type_alias> =
tw.getLabelFor(getTypeAliasLabel(ta))
fun useVariable(v: IrVariable): Label<out DbLocalvar> {
return tw.getVariableLabelFor<DbLocalvar>(v)
}
}