Support reification of arrays containing reified parameters
#KT-10555 Fixed
This commit is contained in:
@@ -24,6 +24,7 @@ import com.intellij.psi.tree.IElementType;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.containers.Stack;
|
||||
import kotlin.Pair;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
@@ -2450,8 +2451,8 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
|
||||
KotlinType type = TypeUtils.uncaptureTypeForInlineMapping(entry.getValue());
|
||||
|
||||
TypeParameterDescriptor parameterDescriptor = TypeUtils.getTypeParameterDescriptorOrNull(type);
|
||||
if (parameterDescriptor == null) {
|
||||
Pair<TypeParameterDescriptor, ReificationArgument> typeParameterAndReificationArgument = extractReificationArgument(type);
|
||||
if (typeParameterAndReificationArgument == null) {
|
||||
// type is not generic
|
||||
BothSignatureWriter signatureWriter = new BothSignatureWriter(BothSignatureWriter.Mode.TYPE);
|
||||
Type asmType = typeMapper.mapTypeParameter(type, signatureWriter);
|
||||
@@ -2464,9 +2465,9 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
key.isReified());
|
||||
}
|
||||
else {
|
||||
mappings.addParameterMappingToNewParameter(
|
||||
mappings.addParameterMappingForFurtherReification(
|
||||
key.getName().getIdentifier(), type,
|
||||
parameterDescriptor.getName().getIdentifier(), key.isReified());
|
||||
typeParameterAndReificationArgument.getSecond(), key.isReified());
|
||||
}
|
||||
}
|
||||
return getOrCreateCallGenerator(
|
||||
@@ -2474,6 +2475,24 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
private static Pair<TypeParameterDescriptor, ReificationArgument> extractReificationArgument(@NotNull KotlinType type) {
|
||||
int arrayDepth = 0;
|
||||
boolean isNullable = type.isMarkedNullable();
|
||||
while (KotlinBuiltIns.isArray(type)) {
|
||||
arrayDepth++;
|
||||
type = type.getArguments().get(0).getType();
|
||||
}
|
||||
|
||||
TypeParameterDescriptor parameterDescriptor = TypeUtils.getTypeParameterDescriptorOrNull(type);
|
||||
if (parameterDescriptor == null) return null;
|
||||
|
||||
return new Pair<TypeParameterDescriptor, ReificationArgument>(
|
||||
parameterDescriptor,
|
||||
new ReificationArgument(parameterDescriptor.getName().asString(), isNullable, arrayDepth));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public StackValue generateReceiverValue(@Nullable ReceiverValue receiverValue, boolean isSuper) {
|
||||
if (receiverValue instanceof ImplicitClassReceiver) {
|
||||
@@ -3746,15 +3765,15 @@ The "returned" value of try expression with no finally is either the last expres
|
||||
public void putReifiedOperationMarkerIfTypeIsReifiedParameter(
|
||||
@NotNull KotlinType type, @NotNull ReifiedTypeInliner.OperationKind operationKind
|
||||
) {
|
||||
TypeParameterDescriptor typeParameterDescriptor = TypeUtils.getTypeParameterDescriptorOrNull(type);
|
||||
if (typeParameterDescriptor != null && typeParameterDescriptor.isReified()) {
|
||||
Pair<TypeParameterDescriptor, ReificationArgument> typeParameterAndReificationArgument = extractReificationArgument(type);
|
||||
if (typeParameterAndReificationArgument != null && typeParameterAndReificationArgument.getFirst().isReified()) {
|
||||
TypeParameterDescriptor typeParameterDescriptor = typeParameterAndReificationArgument.getFirst();
|
||||
if (typeParameterDescriptor.getContainingDeclaration() != context.getContextDescriptor()) {
|
||||
parentCodegen.getReifiedTypeParametersUsages().
|
||||
addUsedReifiedParameter(typeParameterDescriptor.getName().asString());
|
||||
}
|
||||
v.iconst(operationKind.getId());
|
||||
boolean putNullableFlag = operationKind.isTypeNullabilityAware() && type.isMarkedNullable();
|
||||
v.visitLdcInsn(typeParameterDescriptor.getName().asString() + (putNullableFlag ? "?" : ""));
|
||||
v.visitLdcInsn(typeParameterAndReificationArgument.getSecond().asString());
|
||||
v.invokestatic(
|
||||
IntrinsicMethods.INTRINSICS_CLASS_NAME, ReifiedTypeInliner.REIFIED_OPERATION_MARKER_METHOD_NAME,
|
||||
Type.getMethodDescriptor(Type.VOID_TYPE, Type.INT_TYPE, Type.getType(String.class)), false
|
||||
|
||||
@@ -22,14 +22,39 @@ import org.jetbrains.kotlin.codegen.generateIsCheck
|
||||
import org.jetbrains.kotlin.codegen.intrinsics.IntrinsicMethods
|
||||
import org.jetbrains.kotlin.codegen.optimization.common.intConstant
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.types.TypeUtils
|
||||
import org.jetbrains.kotlin.types.Variance
|
||||
import org.jetbrains.kotlin.types.typeUtil.builtIns
|
||||
import org.jetbrains.kotlin.types.typeUtil.makeNullableIfNeeded
|
||||
import org.jetbrains.org.objectweb.asm.MethodVisitor
|
||||
import org.jetbrains.org.objectweb.asm.Opcodes
|
||||
import org.jetbrains.org.objectweb.asm.Type
|
||||
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
|
||||
import org.jetbrains.org.objectweb.asm.tree.*
|
||||
|
||||
private class ParameterNameAndNullability(val name: String, val nullable: Boolean)
|
||||
class ReificationArgument(
|
||||
val parameterName: String, val nullable: Boolean, val arrayDepth: Int
|
||||
) {
|
||||
fun asString() = "[".repeat(arrayDepth) + parameterName + (if (nullable) "?" else "")
|
||||
fun combine(replacement: ReificationArgument) =
|
||||
ReificationArgument(
|
||||
replacement.parameterName,
|
||||
this.nullable || (replacement.nullable && this.arrayDepth == 0),
|
||||
this.arrayDepth + replacement.arrayDepth)
|
||||
|
||||
fun reify(replacementAsmType: Type, kotlinType: KotlinType) =
|
||||
Pair(Type.getType("[".repeat(arrayDepth) + replacementAsmType), kotlinType.arrayOf(arrayDepth).makeNullableIfNeeded(nullable))
|
||||
|
||||
private fun KotlinType.arrayOf(arrayDepth: Int): KotlinType {
|
||||
val builtins = this.builtIns
|
||||
var currentType = this
|
||||
|
||||
repeat(arrayDepth) {
|
||||
currentType = builtins.getArrayType(Variance.INVARIANT, currentType)
|
||||
}
|
||||
|
||||
return currentType
|
||||
}
|
||||
}
|
||||
|
||||
class ReifiedTypeInliner(private val parametersMapping: TypeParameterMappings?) {
|
||||
|
||||
@@ -37,7 +62,6 @@ class ReifiedTypeInliner(private val parametersMapping: TypeParameterMappings?)
|
||||
NEW_ARRAY, AS, SAFE_AS, IS, JAVA_CLASS;
|
||||
|
||||
val id: Int get() = ordinal
|
||||
val isTypeNullabilityAware: Boolean get() = this == AS || this == IS
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -98,20 +122,15 @@ class ReifiedTypeInliner(private val parametersMapping: TypeParameterMappings?)
|
||||
*/
|
||||
private fun processReifyMarker(insn: MethodInsnNode, instructions: InsnList): String? {
|
||||
val operationKind = insn.operationKind ?: return null
|
||||
val parameter = insn.parameterNameAndNullability ?: return null
|
||||
val mapping = parametersMapping?.get(parameter.name) ?: return null
|
||||
val kotlinType =
|
||||
if (operationKind.isTypeNullabilityAware && parameter.nullable)
|
||||
TypeUtils.makeNullable(mapping.type)
|
||||
else
|
||||
mapping.type
|
||||
val reificationArgument = insn.reificationArgument ?: return null
|
||||
val mapping = parametersMapping?.get(reificationArgument.parameterName) ?: return null
|
||||
|
||||
|
||||
val asmType = mapping.asmType
|
||||
if (asmType != null) {
|
||||
if (mapping.asmType != null) {
|
||||
// process* methods return false if marker should be reified further
|
||||
// or it's invalid (may be emitted explicitly in code)
|
||||
// they return true if instruction is reified and marker can be deleted
|
||||
val (asmType, kotlinType) = reificationArgument.reify(mapping.asmType, mapping.type)
|
||||
|
||||
if (when (operationKind) {
|
||||
OperationKind.NEW_ARRAY -> processNewArray(insn, asmType)
|
||||
OperationKind.AS -> processAs(insn, instructions, kotlinType, asmType, safe = false)
|
||||
@@ -126,9 +145,9 @@ class ReifiedTypeInliner(private val parametersMapping: TypeParameterMappings?)
|
||||
|
||||
return null
|
||||
} else {
|
||||
val nullableSuffix = if (operationKind.isTypeNullabilityAware && kotlinType.isMarkedNullable) "?" else ""
|
||||
instructions.set(insn.previous!!, LdcInsnNode(mapping.newName + nullableSuffix))
|
||||
return mapping.newName
|
||||
val newReificationArgument = reificationArgument.combine(mapping.reificationArgument!!)
|
||||
instructions.set(insn.previous!!, LdcInsnNode(newReificationArgument.asString()))
|
||||
return mapping.reificationArgument.parameterName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,17 +217,20 @@ class ReifiedTypeInliner(private val parametersMapping: TypeParameterMappings?)
|
||||
|
||||
}
|
||||
|
||||
private val MethodInsnNode.parameterNameAndNullability: ParameterNameAndNullability?
|
||||
private val MethodInsnNode.reificationArgument: ReificationArgument?
|
||||
get() {
|
||||
val prev = previous!!
|
||||
|
||||
val parameterNameWithFlag = when (prev.opcode) {
|
||||
val reificationArgumentRaw = when (prev.opcode) {
|
||||
Opcodes.LDC -> (prev as LdcInsnNode).cst as String
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val parameterName = if (parameterNameWithFlag.endsWith("?")) parameterNameWithFlag.dropLast(1) else parameterNameWithFlag
|
||||
return ParameterNameAndNullability(parameterName, parameterName !== parameterNameWithFlag)
|
||||
val arrayDepth = reificationArgumentRaw.indexOfFirst { it != '[' }
|
||||
val parameterName = reificationArgumentRaw.substring(arrayDepth).removeSuffix("?")
|
||||
val nullable = reificationArgumentRaw.endsWith('?')
|
||||
|
||||
return ReificationArgument(parameterName, nullable, arrayDepth)
|
||||
}
|
||||
|
||||
private val MethodInsnNode.operationKind: ReifiedTypeInliner.OperationKind? get() =
|
||||
@@ -219,12 +241,12 @@ private val MethodInsnNode.operationKind: ReifiedTypeInliner.OperationKind? get(
|
||||
class TypeParameterMappings() {
|
||||
private val mappingsByName = hashMapOf<String, TypeParameterMapping>()
|
||||
|
||||
public fun addParameterMappingToType(name: String, type: KotlinType, asmType: Type, signature: String, isReified: Boolean) {
|
||||
mappingsByName[name] = TypeParameterMapping(name, type, asmType, newName = null, signature = signature, isReified = isReified)
|
||||
fun addParameterMappingToType(name: String, type: KotlinType, asmType: Type, signature: String, isReified: Boolean) {
|
||||
mappingsByName[name] = TypeParameterMapping(name, type, asmType, reificationArgument = null, signature = signature, isReified = isReified)
|
||||
}
|
||||
|
||||
public fun addParameterMappingToNewParameter(name: String, type: KotlinType, newName: String, isReified: Boolean) {
|
||||
mappingsByName[name] = TypeParameterMapping(name, type, asmType = null, newName = newName, signature = null, isReified = isReified)
|
||||
fun addParameterMappingForFurtherReification(name: String, type: KotlinType, reificationArgument: ReificationArgument, isReified: Boolean) {
|
||||
mappingsByName[name] = TypeParameterMapping(name, type, asmType = null, reificationArgument = reificationArgument, signature = null, isReified = isReified)
|
||||
}
|
||||
|
||||
operator fun get(name: String): TypeParameterMapping? {
|
||||
@@ -239,7 +261,11 @@ class TypeParameterMappings() {
|
||||
}
|
||||
|
||||
class TypeParameterMapping(
|
||||
val name: String, val type: KotlinType, val asmType: Type?, val newName: String?, val signature: String?, val isReified: Boolean
|
||||
val name: String, val type: KotlinType,
|
||||
val asmType: Type?,
|
||||
val reificationArgument: ReificationArgument?,
|
||||
val signature: String?,
|
||||
val isReified: Boolean
|
||||
)
|
||||
|
||||
class ReifiedTypeParametersUsages {
|
||||
|
||||
@@ -52,7 +52,7 @@ class TypeRemapper private constructor(private val typeMapping: MutableMap<Strin
|
||||
}
|
||||
|
||||
fun registerTypeParameter(mapping: TypeParameterMapping) {
|
||||
typeParametersMapping[mapping.name] = TypeParameter(mapping.name, mapping.newName, mapping.isReified, mapping.signature)
|
||||
typeParametersMapping[mapping.name] = TypeParameter(mapping.name, mapping.reificationArgument?.parameterName, mapping.isReified, mapping.signature)
|
||||
}
|
||||
|
||||
fun mapTypeParameter(name: String): TypeParameter? {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
inline fun <reified T> foo(x: Any?) = Pair(x is T, x is T?)
|
||||
|
||||
fun box(): String {
|
||||
val x1 = foo<Array<String>>(arrayOf(""))
|
||||
if (x1.toString() != "(true, true)") return "fail 1"
|
||||
|
||||
val x2 = foo<Array<String>?>(arrayOf(""))
|
||||
if (x2.toString() != "(true, true)") return "fail 2"
|
||||
|
||||
val x3 = foo<Array<String>>(null)
|
||||
if (x3.toString() != "(false, true)") return "fail 3"
|
||||
|
||||
val x4 = foo<Array<String>?>(null)
|
||||
if (x4.toString() != "(true, true)") return "fail 4"
|
||||
|
||||
val x5 = foo<Array<Double>?>(arrayOf(""))
|
||||
if (x5.toString() != "(false, false)") return "fail 5"
|
||||
|
||||
val x6 = foo<Array<Double>?>(null)
|
||||
if (x6.toString() != "(true, true)") return "fail 6"
|
||||
return "OK"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
inline fun <reified T> foo(x: Any?) = Pair(x is T, x is T?)
|
||||
inline fun <reified F> bar(y: Any?) = foo<Array<F>>(y)
|
||||
inline fun <reified F> barNullable(y: Any?) = foo<Array<F>?>(y)
|
||||
|
||||
fun box(): String {
|
||||
val x1 = bar<String>(arrayOf(""))
|
||||
if (x1.toString() != "(true, true)") return "fail 1"
|
||||
|
||||
val x3 = bar<String>(null)
|
||||
if (x3.toString() != "(false, true)") return "fail 3"
|
||||
|
||||
val x4 = bar<String?>(null)
|
||||
if (x4.toString() != "(false, true)") return "fail 4"
|
||||
|
||||
val x5 = bar<Double?>(arrayOf(""))
|
||||
if (x5.toString() != "(false, false)") return "fail 5"
|
||||
|
||||
val x6 = bar<Double?>(null)
|
||||
if (x6.toString() != "(false, true)") return "fail 6"
|
||||
|
||||
// barNullable
|
||||
|
||||
val x7 = barNullable<String>(arrayOf(""))
|
||||
if (x7.toString() != "(true, true)") return "fail 7"
|
||||
|
||||
val x9 = barNullable<String>(null)
|
||||
if (x9.toString() != "(true, true)") return "fail 9"
|
||||
|
||||
val x10 = barNullable<Double?>(arrayOf(""))
|
||||
if (x10.toString() != "(false, false)") return "fail 11"
|
||||
|
||||
val x12 = barNullable<Double?>(null)
|
||||
if (x12.toString() != "(true, true)") return "fail 12"
|
||||
return "OK"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
inline fun <reified T : Any> jClass() = T::class.java
|
||||
inline fun <reified T : Any> jClassArray() = jClass<Array<T>>()
|
||||
|
||||
fun box(): String {
|
||||
if (jClass<Array<String>>().simpleName != "String[]") return "fail 1"
|
||||
if (jClass<IntArray>().simpleName != "int[]") return "fail 2"
|
||||
|
||||
if (jClassArray<String>().simpleName != "String[]") return "fail 3"
|
||||
if (jClassArray<Array<String>>().simpleName != "String[][]") return "fail 4"
|
||||
if (jClassArray<IntArray>().simpleName != "int[][]") return "fail 5"
|
||||
|
||||
return "OK"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
inline fun <reified T> jaggedArray(x: (Int, Int) -> T): Array<Array<T>> = Array(1) { i ->
|
||||
Array(1) { j -> x(i, j) }
|
||||
}
|
||||
|
||||
fun box(): String {
|
||||
val x1: Array<Array<String>> = jaggedArray<String>() { x, y -> "$x-$y" }
|
||||
if (x1[0][0] != "0-0") return "fail 1"
|
||||
|
||||
val x2: Array<Array<Array<String>>> = jaggedArray() { x, y -> arrayOf("$x-$y") }
|
||||
if (x2[0][0][0] != "0-0") return "fail 2"
|
||||
|
||||
val x3: Array<Array<IntArray>> = jaggedArray() { x, y -> intArrayOf(x + y + 1) }
|
||||
if (x3[0][0][0] != 1) return "fail 3"
|
||||
return "OK"
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
inline fun <reified T> jaggedArrayOfNulls(): Array<Array<T>?> = arrayOfNulls<Array<T>>(1)
|
||||
|
||||
fun box(): String {
|
||||
val x1 = jaggedArrayOfNulls<String>().javaClass.simpleName
|
||||
if (x1 != "String[][]") return "fail1: $x1"
|
||||
|
||||
val x2 = jaggedArrayOfNulls<Array<String>>().javaClass.simpleName
|
||||
if (x2 != "String[][][]") return "fail2: $x2"
|
||||
|
||||
val x3 = jaggedArrayOfNulls<IntArray>().javaClass.simpleName
|
||||
if (x3 != "int[][][]") return "fail3: $x3"
|
||||
return "OK"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
inline fun <reified T> jaggedArray(x: (Int, Int, Int) -> T): Array<Array<Array<T>>> = Array(1) { i ->
|
||||
Array(1) {
|
||||
j -> Array(1) { k -> x(i, j, k) }
|
||||
}
|
||||
}
|
||||
|
||||
fun box(): String {
|
||||
val x1: Array<Array<Array<String>>> = jaggedArray<String>() { x, y, z -> "$x-$y-$z" }
|
||||
if (x1[0][0][0] != "0-0-0") return "fail 1"
|
||||
|
||||
val x2: Array<Array<Array<Array<String>>>> = jaggedArray() { x, y, z -> arrayOf("$x-$y-$z") }
|
||||
if (x2[0][0][0][0] != "0-0-0") return "fail 2"
|
||||
|
||||
val x3: Array<Array<Array<IntArray>>> = jaggedArray() { x, y, z -> intArrayOf(x + y + z + 1) }
|
||||
if (x3[0][0][0][0] != 1) return "fail 3"
|
||||
return "OK"
|
||||
}
|
||||
+45
@@ -4674,6 +4674,51 @@ public class BlackBoxWithStdlibCodegenTestGenerated extends AbstractBlackBoxCode
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/reified/varargs.kt");
|
||||
doTestWithStdlib(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("compiler/testData/codegen/boxWithStdlib/reified/arraysReification")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class ArraysReification extends AbstractBlackBoxCodegenTest {
|
||||
public void testAllFilesPresentInArraysReification() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/boxWithStdlib/reified/arraysReification"), Pattern.compile("^(.+)\\.kt$"), true);
|
||||
}
|
||||
|
||||
@TestMetadata("instanceOf.kt")
|
||||
public void testInstanceOf() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/reified/arraysReification/instanceOf.kt");
|
||||
doTestWithStdlib(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("instanceOfArrays.kt")
|
||||
public void testInstanceOfArrays() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/reified/arraysReification/instanceOfArrays.kt");
|
||||
doTestWithStdlib(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("jClass.kt")
|
||||
public void testJClass() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/reified/arraysReification/jClass.kt");
|
||||
doTestWithStdlib(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("jaggedArray.kt")
|
||||
public void testJaggedArray() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/reified/arraysReification/jaggedArray.kt");
|
||||
doTestWithStdlib(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("jaggedArrayOfNulls.kt")
|
||||
public void testJaggedArrayOfNulls() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/reified/arraysReification/jaggedArrayOfNulls.kt");
|
||||
doTestWithStdlib(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("jaggedDeep.kt")
|
||||
public void testJaggedDeep() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/boxWithStdlib/reified/arraysReification/jaggedDeep.kt");
|
||||
doTestWithStdlib(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("compiler/testData/codegen/boxWithStdlib/storeStackBeforeInline")
|
||||
|
||||
@@ -42,6 +42,7 @@ val KotlinType.builtIns: KotlinBuiltIns
|
||||
get() = constructor.builtIns
|
||||
|
||||
fun KotlinType.makeNullable() = TypeUtils.makeNullable(this)
|
||||
fun KotlinType.makeNullableIfNeeded(nullable: Boolean) = TypeUtils.makeNullableIfNeeded(this, nullable)
|
||||
fun KotlinType.makeNotNullable() = TypeUtils.makeNotNullable(this)
|
||||
|
||||
fun KotlinType.immediateSupertypes(): Collection<KotlinType> = TypeUtils.getImmediateSupertypes(this)
|
||||
|
||||
Reference in New Issue
Block a user