Skip to content

Commit a0d5d41

Browse files
committed
Kotlin: extract methods defined on collections types with their Java signatures
Collection, List and Map all define various methods which are either made more generic in Kotlin (e.g. `remove(Object) -> remove(E)`, `containsAll(Collection<?>) -> containsAll(Collection<E>)`), or are made invariant (e.g. `addAll(Collection<? extends E>) -> addAll(Collection<E>)`). This substitutes the types back to their Java signatures, thereby avoiding differing trap labels and duplicated methods for these types and their descendents.
1 parent 5d4473b commit a0d5d41

File tree

6 files changed

+593
-15
lines changed

6 files changed

+593
-15
lines changed

java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -491,11 +491,17 @@ open class KotlinFileExtractor(
491491
private fun extractValueParameter(vp: IrValueParameter, parent: Label<out DbCallable>, idx: Int, typeSubstitution: TypeSubstitution?, parentSourceDeclaration: Label<out DbCallable>, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?, extractTypeAccess: Boolean, locOverride: Label<DbLocation>? = null): TypeResults {
492492
with("value parameter", vp) {
493493
val location = locOverride ?: getLocation(vp, classTypeArgsIncludingOuterClasses)
494+
val maybeErasedType = (vp.parent as? IrFunction)?.let {
495+
if (overridesCollectionsMethodWithAlteredParameterTypes(it))
496+
eraseCollectionsMethodParameterType(vp.type, it.name.asString(), idx)
497+
else
498+
null
499+
} ?: vp.type
494500
val id = useValueParameter(vp, parent)
495501
if (extractTypeAccess) {
496-
extractTypeAccessRecursive(typeSubstitution?.let { it(vp.type, TypeContext.OTHER, pluginContext) } ?: vp.type, location, id, -1)
502+
extractTypeAccessRecursive(typeSubstitution?.let { it(maybeErasedType, TypeContext.OTHER, pluginContext) } ?: maybeErasedType, location, id, -1)
497503
}
498-
return extractValueParameter(id, vp.type, vp.name.asString(), location, parent, idx, typeSubstitution, useValueParameter(vp, parentSourceDeclaration), vp.isVararg)
504+
return extractValueParameter(id, maybeErasedType, vp.name.asString(), location, parent, idx, typeSubstitution, useValueParameter(vp, parentSourceDeclaration), vp.isVararg)
499505
}
500506
}
501507

@@ -524,7 +530,8 @@ open class KotlinFileExtractor(
524530
pluginContext.irBuiltIns.unitType,
525531
extensionReceiverParameter = null,
526532
functionTypeParameters = listOf(),
527-
classTypeArgsIncludingOuterClasses = listOf()
533+
classTypeArgsIncludingOuterClasses = listOf(),
534+
overridesCollectionsMethod = false
528535
)
529536
val clinitId = tw.getLabelFor<DbMethod>(clinitLabel)
530537
val returnType = useType(pluginContext.irBuiltIns.unitType, TypeContext.RETURN)
@@ -1391,12 +1398,6 @@ open class KotlinFileExtractor(
13911398
result
13921399
}
13931400

1394-
val javaLangObject by lazy {
1395-
val result = pluginContext.referenceClass(FqName("java.lang.Object"))?.owner
1396-
result?.let { extractExternalClassLater(it) }
1397-
result
1398-
}
1399-
14001401
val objectCloneMethod by lazy {
14011402
val result = javaLangObject?.declarations?.find {
14021403
it is IrFunction && it.name.asString() == "clone"

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

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@ import com.github.codeql.utils.versions.isRawType
55
import com.semmle.extractor.java.OdasaOutput
66
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
77
import org.jetbrains.kotlin.backend.common.ir.allOverridden
8+
import org.jetbrains.kotlin.backend.common.ir.isOverridableOrOverrides
89
import org.jetbrains.kotlin.backend.common.lower.parentsWithSelf
910
import org.jetbrains.kotlin.backend.jvm.ir.propertyIfAccessor
1011
import org.jetbrains.kotlin.builtins.StandardNames
1112
import org.jetbrains.kotlin.descriptors.*
13+
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
1214
import org.jetbrains.kotlin.ir.declarations.*
1315
import org.jetbrains.kotlin.ir.expressions.*
16+
import org.jetbrains.kotlin.ir.interpreter.getLastOverridden
1417
import org.jetbrains.kotlin.ir.symbols.*
1518
import org.jetbrains.kotlin.ir.types.*
1619
import org.jetbrains.kotlin.ir.types.impl.*
1720
import org.jetbrains.kotlin.ir.util.*
21+
import org.jetbrains.kotlin.load.java.BuiltinMethodsWithSpecialGenericSignature
1822
import org.jetbrains.kotlin.load.java.JvmAbi
1923
import org.jetbrains.kotlin.name.FqName
2024
import org.jetbrains.kotlin.name.Name
@@ -31,6 +35,17 @@ open class KotlinUsesExtractor(
3135
val pluginContext: IrPluginContext,
3236
val globalExtensionState: KotlinExtractorGlobalState
3337
) {
38+
39+
val javaLangObject by lazy {
40+
val result = pluginContext.referenceClass(FqName("java.lang.Object"))?.owner
41+
result?.let { extractExternalClassLater(it) }
42+
result
43+
}
44+
45+
val javaLangObjectType by lazy {
46+
javaLangObject?.typeWith()
47+
}
48+
3449
fun usePackage(pkg: String): Label<out DbPackage> {
3550
return extractPackage(pkg)
3651
}
@@ -789,6 +804,52 @@ open class KotlinUsesExtractor(
789804
else -> null
790805
}
791806

807+
val javaUtilCollection by lazy {
808+
val result = pluginContext.referenceClass(FqName("java.util.Collection"))?.owner
809+
result?.let { extractExternalClassLater(it) }
810+
result
811+
}
812+
813+
val wildcardCollectionType by lazy {
814+
javaUtilCollection?.let {
815+
it.symbol.typeWithArguments(listOf(IrStarProjectionImpl))
816+
}
817+
}
818+
819+
private fun makeCovariant(t: IrTypeArgument) =
820+
t.typeOrNull?.let { makeTypeProjection(it, Variance.OUT_VARIANCE) } ?: t
821+
822+
private fun makeArgumentsCovariant(t: IrType) = (t as? IrSimpleType)?.let {
823+
t.toBuilder().also { b -> b.arguments = b.arguments.map(this::makeCovariant) }.buildSimpleType()
824+
} ?: t
825+
826+
fun eraseCollectionsMethodParameterType(t: IrType, collectionsMethodName: String, paramIdx: Int) =
827+
when(collectionsMethodName) {
828+
"contains", "remove", "containsKey", "containsValue", "get", "indexOf", "lastIndexOf" -> javaLangObjectType
829+
"getOrDefault" -> if (paramIdx == 0) javaLangObjectType else null
830+
"containsAll", "removeAll", "retainAll" -> wildcardCollectionType
831+
// Kotlin defines these like addAll(Collection<E>); Java uses addAll(Collection<? extends E>)
832+
"putAll", "addAll" -> makeArgumentsCovariant(t)
833+
else -> null
834+
} ?: t
835+
836+
private fun overridesFunctionDefinedOn(f: IrFunction, packageName: String, className: String) =
837+
(f as? IrSimpleFunction)?.let {
838+
it.overriddenSymbols.any { overridden ->
839+
overridden.owner.parentClassOrNull?.let { defnClass ->
840+
defnClass.name.asString() == className &&
841+
defnClass.packageFqName?.asString() == packageName
842+
} ?: false
843+
}
844+
} ?: false
845+
846+
@OptIn(ObsoleteDescriptorBasedAPI::class)
847+
fun overridesCollectionsMethodWithAlteredParameterTypes(f: IrFunction) =
848+
BuiltinMethodsWithSpecialGenericSignature.getOverriddenBuiltinFunctionWithErasedValueParametersInJava(f.descriptor) != null ||
849+
(f.name.asString() == "putAll" && overridesFunctionDefinedOn(f, "kotlin.collections", "MutableMap")) ||
850+
(f.name.asString() == "addAll" && overridesFunctionDefinedOn(f, "kotlin.collections", "MutableCollection")) ||
851+
(f.name.asString() == "addAll" && overridesFunctionDefinedOn(f, "kotlin.collections", "MutableList"))
852+
792853
/*
793854
* This is the normal getFunctionLabel function to use. If you want
794855
* to refer to the function in its source class then
@@ -810,8 +871,19 @@ open class KotlinUsesExtractor(
810871
* `java.lang.Throwable`, which isn't what we want. So we have to
811872
* allow it to be passed in.
812873
*/
874+
@OptIn(ObsoleteDescriptorBasedAPI::class)
813875
fun getFunctionLabel(f: IrFunction, maybeParentId: Label<out DbElement>?, classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?) =
814-
getFunctionLabel(f.parent, maybeParentId, getFunctionShortName(f).nameInDB, f.valueParameters, f.returnType, f.extensionReceiverParameter, getFunctionTypeParameters(f), classTypeArgsIncludingOuterClasses)
876+
getFunctionLabel(
877+
f.parent,
878+
maybeParentId,
879+
getFunctionShortName(f).nameInDB,
880+
f.valueParameters,
881+
f.returnType,
882+
f.extensionReceiverParameter,
883+
getFunctionTypeParameters(f),
884+
classTypeArgsIncludingOuterClasses,
885+
overridesCollectionsMethodWithAlteredParameterTypes(f)
886+
)
815887

816888
/*
817889
* This function actually generates the label for a function.
@@ -837,6 +909,9 @@ open class KotlinUsesExtractor(
837909
functionTypeParameters: List<IrTypeParameter>,
838910
// The type arguments of enclosing classes of the function.
839911
classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?,
912+
// If true, this method implements a Java Collections interface (Collection, Map or List) and may need
913+
// parameter erasure to match the way this class will appear to an external consumer of the .class file.
914+
overridesCollectionsMethod: Boolean,
840915
// The prefix used in the label. "callable", unless a property label is created, then it's "property".
841916
prefix: String = "callable"
842917
): String {
@@ -855,14 +930,19 @@ open class KotlinUsesExtractor(
855930
enclosingClass?.let { notNullClass -> makeTypeGenericSubstitutionMap(notNullClass, notNullArgs) }
856931
}
857932
}
858-
val getIdForFunctionLabel = { it: IrValueParameter ->
859-
// Mimic the Java extractor's behaviour: functions with type parameters are named for their erased types;
933+
val getIdForFunctionLabel = { it: IndexedValue<IrValueParameter> ->
934+
// Kotlin rewrites certain Java collections types adding additional generic constraints-- for example,
935+
// Collection.remove(Object) because Collection.remove(Collection::E) in the Kotlin universe.
936+
// If this has happened, erase the type again to get the correct Java signature.
937+
val maybeAmendedForCollections = if (overridesCollectionsMethod) eraseCollectionsMethodParameterType(it.value.type, name, it.index) else it.value.type
938+
// Now substitute any class type parameters in:
939+
val maybeSubbed = maybeAmendedForCollections.substituteTypeAndArguments(substitutionMap, TypeContext.OTHER, pluginContext)
940+
// Finally, mimic the Java extractor's behaviour by naming functions with type parameters for their erased types;
860941
// those without type parameters are named for the generic type.
861-
val maybeSubbed = it.type.substituteTypeAndArguments(substitutionMap, TypeContext.OTHER, pluginContext)
862942
val maybeErased = if (functionTypeParameters.isEmpty()) maybeSubbed else erase(maybeSubbed)
863943
"{${useType(maybeErased).javaResult.id}}"
864944
}
865-
val paramTypeIds = allParams.joinToString(separator = ",", transform = getIdForFunctionLabel)
945+
val paramTypeIds = allParams.withIndex().joinToString(separator = ",", transform = getIdForFunctionLabel)
866946
val labelReturnType =
867947
if (name == "<init>")
868948
pluginContext.irBuiltIns.unitType
@@ -1299,7 +1379,7 @@ open class KotlinUsesExtractor(
12991379
val returnType = getter?.returnType ?: setter?.valueParameters?.singleOrNull()?.type ?: pluginContext.irBuiltIns.unitType
13001380
val typeParams = getFunctionTypeParameters(func)
13011381

1302-
getFunctionLabel(p.parent, parentId, p.name.asString(), listOf(), returnType, ext, typeParams, classTypeArgsIncludingOuterClasses, "property")
1382+
getFunctionLabel(p.parent, parentId, p.name.asString(), listOf(), returnType, ext, typeParams, classTypeArgsIncludingOuterClasses, overridesCollectionsMethod = false, prefix = "property")
13031383
}
13041384
}
13051385

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import java.util.Map;
2+
import java.util.AbstractMap;
3+
import java.util.Collection;
4+
import java.util.AbstractCollection;
5+
import java.util.List;
6+
import java.util.AbstractList;
7+
8+
public class Test {
9+
10+
public static void test(
11+
Map<String, String> p1,
12+
AbstractMap<String, String> p2,
13+
Collection<String> p3,
14+
AbstractCollection<String> p4,
15+
List<String> p5,
16+
AbstractList<String> p6) {
17+
18+
// Use a method of each to ensure method prototypes are extracted:
19+
p1.remove("Hello");
20+
p2.remove("Hello");
21+
p3.remove("Hello");
22+
p4.remove("Hello");
23+
p5.remove("Hello");
24+
p6.remove("Hello");
25+
26+
}
27+
28+
}

0 commit comments

Comments
 (0)