JVM: Rewrite companion object accessor generation
Old version handled only private companions correctly. Some situations require multiple companion object accessors (including accessors for protected companion objects from supertypes) to be generated in the corresponding class.
This commit is contained in:
@@ -1851,10 +1851,9 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
}
|
||||
if (descriptor instanceof TypeAliasDescriptor) {
|
||||
ClassDescriptor classDescriptor = ((TypeAliasDescriptor) descriptor).getClassDescriptor();
|
||||
if (classDescriptor == null) {
|
||||
throw new IllegalStateException("Type alias " + descriptor + " static member reference should be rejected by type checker, " +
|
||||
"since there is no class corresponding to this type alias.");
|
||||
}
|
||||
assert classDescriptor != null :
|
||||
"Type alias " + descriptor + " static member reference should be rejected by type checker, " +
|
||||
"since there is no class corresponding to this type alias.";
|
||||
descriptor = classDescriptor;
|
||||
}
|
||||
|
||||
@@ -1863,8 +1862,8 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
if (shouldGenerateSingletonAsThisOrOuterFromContext(classDescriptor)) {
|
||||
return generateThisOrOuterFromContext(classDescriptor, false, false);
|
||||
}
|
||||
if (isCompanionObject(classDescriptor) && !couldUseDirectAccessToCompanionObject(classDescriptor, context)) {
|
||||
return generateAccessorCallForCompanionObject(classDescriptor);
|
||||
if (isCompanionObject(classDescriptor)) {
|
||||
return generateCompanionObjectInstance(classDescriptor);
|
||||
}
|
||||
if (isObject(classDescriptor)) {
|
||||
return StackValue.singleton(classDescriptor, typeMapper);
|
||||
@@ -1874,7 +1873,7 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
}
|
||||
ClassDescriptor companionObjectDescriptor = classDescriptor.getCompanionObjectDescriptor();
|
||||
if (companionObjectDescriptor != null) {
|
||||
return StackValue.singleton(companionObjectDescriptor, typeMapper);
|
||||
return generateCompanionObjectInstance(companionObjectDescriptor);
|
||||
}
|
||||
return StackValue.none();
|
||||
}
|
||||
@@ -1886,6 +1885,41 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
throw new UnsupportedOperationException("don't know how to generate reference " + descriptor);
|
||||
}
|
||||
|
||||
private StackValue generateCompanionObjectInstance(ClassDescriptor companionObjectDescriptor) {
|
||||
assert isCompanionObject(companionObjectDescriptor) :
|
||||
"Companion object expected: " + companionObjectDescriptor;
|
||||
|
||||
CodegenContext accessorContext = getContextForCompanionObjectAccessorIfRequiredOrNull(companionObjectDescriptor, context);
|
||||
if (accessorContext == null) {
|
||||
return StackValue.singleton(companionObjectDescriptor, typeMapper);
|
||||
}
|
||||
|
||||
// Should use accessor
|
||||
DeclarationDescriptor accessorContextDescriptor = accessorContext.getContextDescriptor();
|
||||
assert accessorContextDescriptor instanceof ClassDescriptor :
|
||||
"Expected to put accessor for companion object " + companionObjectDescriptor +
|
||||
" into a class, but got " + accessorContextDescriptor;
|
||||
|
||||
accessorContext.getOrCreateAccessorForCompanionObject(companionObjectDescriptor);
|
||||
|
||||
Type hostClassType = typeMapper.mapClass((ClassDescriptor) accessorContextDescriptor);
|
||||
Type companionObjectType = typeMapper.mapClass(companionObjectDescriptor);
|
||||
|
||||
// TODO we actually have corresponding accessor descriptor, it might be a better idea to use general method call generation.
|
||||
return StackValue.operation(
|
||||
companionObjectType,
|
||||
companionObjectDescriptor.getDefaultType(),
|
||||
v1 -> {
|
||||
v1.invokestatic(
|
||||
hostClassType.getInternalName(),
|
||||
getCompanionObjectAccessorName(companionObjectDescriptor),
|
||||
Type.getMethodDescriptor(companionObjectType),
|
||||
/* itf */ false
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean shouldGenerateSingletonAsThisOrOuterFromContext(ClassDescriptor classDescriptor) {
|
||||
if (!isPossiblyUninitializedSingleton(classDescriptor)) return false;
|
||||
if (!isInsideSingleton(classDescriptor)) return false;
|
||||
@@ -2479,6 +2513,23 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static CodegenContext getCompanionObjectContextSubclassOf(ClassDescriptor descriptor, CodegenContext context) {
|
||||
for (CodegenContext current = context; current != null; current = current.getParentContext()) {
|
||||
if (!(current instanceof ClassContext)) continue;
|
||||
|
||||
ClassContext classContext = (ClassContext) current;
|
||||
ClassDescriptor companionObject = classContext.getContextDescriptor().getCompanionObjectDescriptor();
|
||||
if (companionObject == null) continue;
|
||||
|
||||
if (DescriptorUtils.isSubclass(companionObject, descriptor)) {
|
||||
return classContext.getCompanionObjectContext();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Callable resolveToCallable(@NotNull FunctionDescriptor fd, boolean superCall, @NotNull ResolvedCall resolvedCall) {
|
||||
IntrinsicMethod intrinsic = state.getIntrinsics().getIntrinsic(fd);
|
||||
@@ -2799,11 +2850,8 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
else if (isPossiblyUninitializedSingleton(receiverDescriptor) && isInsideSingleton(receiverDescriptor)) {
|
||||
return generateThisOrOuterFromContext(receiverDescriptor, false, false);
|
||||
}
|
||||
else if (couldUseDirectAccessToCompanionObject(receiverDescriptor, context)) {
|
||||
return StackValue.singleton(receiverDescriptor, typeMapper);
|
||||
}
|
||||
else {
|
||||
return generateAccessorCallForCompanionObject(receiverDescriptor);
|
||||
return generateCompanionObjectInstance(receiverDescriptor);
|
||||
}
|
||||
}
|
||||
else if (receiverDescriptor instanceof ScriptDescriptor) {
|
||||
@@ -2814,36 +2862,53 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private StackValue generateAccessorCallForCompanionObject(@NotNull ClassDescriptor companionObjectDescriptor) {
|
||||
DeclarationDescriptor hostClassDescriptor = companionObjectDescriptor.getContainingDeclaration();
|
||||
assert hostClassDescriptor instanceof ClassDescriptor :
|
||||
"Containing declaration of the companion object " + companionObjectDescriptor +
|
||||
": expected a class, actual: " + hostClassDescriptor;
|
||||
private static CodegenContext getContextForCompanionObjectAccessorIfRequiredOrNull(
|
||||
@NotNull ClassDescriptor companionObjectDescriptor,
|
||||
@NotNull CodegenContext contextBeforeInline
|
||||
) {
|
||||
if (isDebuggerContext(contextBeforeInline)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CodegenContext hostClassContext = context.findParentContextWithDescriptor(hostClassDescriptor);
|
||||
assert hostClassContext != null :
|
||||
"Host class context for " + hostClassDescriptor + " not found in context hierarchy for " + context;
|
||||
int accessFlag = AsmUtil.getVisibilityAccessFlag(companionObjectDescriptor);
|
||||
|
||||
hostClassContext.markCompanionObjectDescriptorWithAccessorRequired(companionObjectDescriptor);
|
||||
CodegenContext context = contextBeforeInline.getFirstCrossInlineOrNonInlineContext();
|
||||
boolean isInlineMethodContext = context.isInlineMethodContext();
|
||||
DeclarationDescriptor contextDescriptor = context.getContextDescriptor();
|
||||
|
||||
Type hostClassType = typeMapper.mapClass((ClassifierDescriptor) hostClassDescriptor);
|
||||
Type companionObjectType = typeMapper.mapClass(companionObjectDescriptor);
|
||||
ClassDescriptor containingClassOfCompanionObject = DescriptorUtils.getContainingClass(companionObjectDescriptor);
|
||||
|
||||
// TODO given that we actually have corresponding AccessorForCompanionObjectInstanceFieldDescriptor,
|
||||
// it might be a better idea to use general method call generation
|
||||
return StackValue.operation(
|
||||
companionObjectType,
|
||||
companionObjectDescriptor.getDefaultType(),
|
||||
v -> {
|
||||
v.invokestatic(
|
||||
hostClassType.getInternalName(),
|
||||
getCompanionObjectAccessorName(companionObjectDescriptor),
|
||||
Type.getMethodDescriptor(companionObjectType),
|
||||
/* itf */ false
|
||||
);
|
||||
return null;
|
||||
});
|
||||
if ((accessFlag & ACC_PRIVATE) != 0) {
|
||||
// Private companion object
|
||||
if (!isInlineMethodContext && contextDescriptor.getContainingDeclaration() == containingClassOfCompanionObject) {
|
||||
return null;
|
||||
}
|
||||
return context.findParentContextWithDescriptor(containingClassOfCompanionObject);
|
||||
} else if ((accessFlag & ACC_PROTECTED) != 0) {
|
||||
// Protected companion object
|
||||
if (!isInlineMethodContext && canAccessProtectedMembers(contextDescriptor, containingClassOfCompanionObject)) {
|
||||
return null;
|
||||
}
|
||||
CodegenContext accessorContext = getParentContextSubclassOf(containingClassOfCompanionObject, context);
|
||||
if (accessorContext == null) {
|
||||
accessorContext = getCompanionObjectContextSubclassOf(containingClassOfCompanionObject, context);
|
||||
}
|
||||
assert accessorContext != null :
|
||||
"Class context for accessor to a protected companion object " + companionObjectDescriptor + " not found:\n" +
|
||||
JvmCodegenUtil.dumpContextHierarchy(context);
|
||||
return accessorContext;
|
||||
} else {
|
||||
// Non-protected && non-private companion object
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean canAccessProtectedMembers(DeclarationDescriptor contextDescriptor, ClassDescriptor classDescriptor) {
|
||||
DeclarationDescriptor containingDeclaration = contextDescriptor.getContainingDeclaration();
|
||||
return containingDeclaration == classDescriptor ||
|
||||
JvmCodegenUtil.isInSamePackage(contextDescriptor, classDescriptor) ||
|
||||
containingDeclaration instanceof ClassDescriptor &&
|
||||
DescriptorUtils.isSubclass((ClassDescriptor) containingDeclaration, classDescriptor);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -151,29 +151,6 @@ public class JvmCodegenUtil {
|
||||
return propertyDescriptor.isConst() || hasJvmFieldAnnotation(propertyDescriptor);
|
||||
}
|
||||
|
||||
public static boolean couldUseDirectAccessToCompanionObject(
|
||||
@NotNull ClassDescriptor companionObjectDescriptor,
|
||||
@NotNull MethodContext contextBeforeInline
|
||||
) {
|
||||
if (!Visibilities.isPrivate(companionObjectDescriptor.getVisibility())) {
|
||||
// Non-private companion object can be directly accessed anywhere it's allowed by the front-end.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isDebuggerContext(contextBeforeInline)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
CodegenContext context = contextBeforeInline.getFirstCrossInlineOrNonInlineContext();
|
||||
if (context.isInlineMethodContext()) {
|
||||
// Inline method can be called from a nested class.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Private companion object is directly accessible only from the corresponding class
|
||||
return context.getContextDescriptor().getContainingDeclaration() == companionObjectDescriptor.getContainingDeclaration();
|
||||
}
|
||||
|
||||
public static String getCompanionObjectAccessorName(@NotNull ClassDescriptor companionObjectDescriptor) {
|
||||
return "access$" + companionObjectDescriptor.getName();
|
||||
}
|
||||
@@ -422,4 +399,23 @@ public class JvmCodegenUtil {
|
||||
return ((DeserializedClassDescriptor) descriptor).getMetadataVersion().isAtLeast(1, 1, 16);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isInSamePackage(DeclarationDescriptor descriptor1, DeclarationDescriptor descriptor2) {
|
||||
PackageFragmentDescriptor package1 = DescriptorUtils.getParentOfType(descriptor1, PackageFragmentDescriptor.class, false);
|
||||
PackageFragmentDescriptor package2 = DescriptorUtils.getParentOfType(descriptor2, PackageFragmentDescriptor.class, false);
|
||||
|
||||
return package1 != null && package2 != null &&
|
||||
package1.getFqName().equals(package2.getFqName());
|
||||
}
|
||||
|
||||
// Used mainly for debugging purposes.
|
||||
@SuppressWarnings("unused")
|
||||
public static String dumpContextHierarchy(CodegenContext context) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
int i = 0;
|
||||
for (CodegenContext current = context; current != null; current = current.getParentContext(), ++i) {
|
||||
result.append(i).append(": ").append(current).append('\n');
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jetbrains.kotlin.psi.*;
|
||||
import org.jetbrains.kotlin.psi.synthetics.SyntheticClassOrObjectDescriptor;
|
||||
import org.jetbrains.kotlin.resolve.BindingContext;
|
||||
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils;
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils;
|
||||
import org.jetbrains.kotlin.resolve.InlineClassesUtilsKt;
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
|
||||
import org.jetbrains.kotlin.resolve.constants.ConstantValue;
|
||||
@@ -52,10 +53,7 @@ import org.jetbrains.org.objectweb.asm.Type;
|
||||
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter;
|
||||
import org.jetbrains.org.objectweb.asm.commons.Method;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jetbrains.kotlin.codegen.AsmUtil.calculateInnerClassAccessFlags;
|
||||
import static org.jetbrains.kotlin.codegen.AsmUtil.isPrimitive;
|
||||
@@ -739,13 +737,26 @@ public abstract class MemberCodegen<T extends KtPureElement/* TODO: & KtDeclarat
|
||||
}
|
||||
}
|
||||
|
||||
AccessorForCompanionObjectInstanceFieldDescriptor accessorForCompanionObjectInstanceFieldDescriptor =
|
||||
context.getAccessorForCompanionObjectDescriptorIfRequired();
|
||||
if (accessorForCompanionObjectInstanceFieldDescriptor != null) {
|
||||
generateSyntheticAccessorForCompanionObject(accessorForCompanionObjectInstanceFieldDescriptor);
|
||||
Collection<AccessorForCompanionObjectInstanceFieldDescriptor> accessorsForCompanionObjects =
|
||||
context.getRequiredAccessorsForCompanionObjects();
|
||||
if (!accessorsForCompanionObjects.isEmpty()) {
|
||||
List<AccessorForCompanionObjectInstanceFieldDescriptor> sortedAccessorsForCompanionObjects =
|
||||
new ArrayList<>(accessorsForCompanionObjects);
|
||||
sortedAccessorsForCompanionObjects.sort(BY_COMPANION_FQN);
|
||||
|
||||
for (AccessorForCompanionObjectInstanceFieldDescriptor accessor : sortedAccessorsForCompanionObjects) {
|
||||
generateSyntheticAccessorForCompanionObject(accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Comparator<AccessorForCompanionObjectInstanceFieldDescriptor> BY_COMPANION_FQN =
|
||||
(o1, o2) -> {
|
||||
String companionFQN1 = DescriptorUtils.getFqName(o1.getCompanionObjectDescriptor()).asString();
|
||||
String companionFQN2 = DescriptorUtils.getFqName(o2.getCompanionObjectDescriptor()).asString();
|
||||
return companionFQN1.compareTo(companionFQN2);
|
||||
};
|
||||
|
||||
private void generateSyntheticAccessorForCompanionObject(@NotNull AccessorForCompanionObjectInstanceFieldDescriptor accessor) {
|
||||
ClassDescriptor companionObjectDescriptor = accessor.getCompanionObjectDescriptor();
|
||||
DeclarationDescriptor hostClassDescriptor = companionObjectDescriptor.getContainingDeclaration();
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.jetbrains.org.objectweb.asm.Type;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jetbrains.kotlin.codegen.AsmUtil.getVisibilityAccessFlag;
|
||||
import static org.jetbrains.kotlin.codegen.JvmCodegenUtil.isInSamePackage;
|
||||
import static org.jetbrains.kotlin.codegen.JvmCodegenUtil.isNonDefaultInterfaceMember;
|
||||
import static org.jetbrains.kotlin.resolve.inline.InlineOnlyKt.isInlineOnlyPrivateInBytecode;
|
||||
import static org.jetbrains.kotlin.resolve.jvm.annotations.JvmAnnotationUtilKt.hasJvmDefaultAnnotation;
|
||||
@@ -33,6 +34,7 @@ import static org.jetbrains.kotlin.resolve.jvm.annotations.JvmAnnotationUtilKt.i
|
||||
import static org.jetbrains.org.objectweb.asm.Opcodes.ACC_PRIVATE;
|
||||
import static org.jetbrains.org.objectweb.asm.Opcodes.ACC_PROTECTED;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public abstract class CodegenContext<T extends DeclarationDescriptor> {
|
||||
private final T contextDescriptor;
|
||||
private final OwnerKind contextKind;
|
||||
@@ -45,7 +47,8 @@ public abstract class CodegenContext<T extends DeclarationDescriptor> {
|
||||
private Map<DeclarationDescriptor, CodegenContext> childContexts;
|
||||
private Map<AccessorKey, AccessorForCallableDescriptor<?>> accessors;
|
||||
private Map<AccessorKey, AccessorForPropertyDescriptorFactory> propertyAccessorFactories;
|
||||
private AccessorForCompanionObjectInstanceFieldDescriptor accessorForCompanionObjectInstanceFieldDescriptor = null;
|
||||
private final Map<ClassDescriptor, AccessorForCompanionObjectInstanceFieldDescriptor> accessorsForCompanionObjects =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
private static class AccessorKey {
|
||||
public final DeclarationDescriptor descriptor;
|
||||
@@ -67,8 +70,7 @@ public abstract class CodegenContext<T extends DeclarationDescriptor> {
|
||||
AccessorKey other = (AccessorKey) obj;
|
||||
return descriptor.equals(other.descriptor) &&
|
||||
accessorKind == other.accessorKind &&
|
||||
(superCallLabelTarget == null ? other.superCallLabelTarget == null
|
||||
: superCallLabelTarget.equals(other.superCallLabelTarget));
|
||||
Objects.equals(superCallLabelTarget, other.superCallLabelTarget);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -701,16 +703,6 @@ public abstract class CodegenContext<T extends DeclarationDescriptor> {
|
||||
(withinInline || !isInSamePackage(unwrappedDescriptor, descriptorContext.getContextDescriptor())));
|
||||
}
|
||||
|
||||
private static boolean isInSamePackage(DeclarationDescriptor descriptor1, DeclarationDescriptor descriptor2) {
|
||||
PackageFragmentDescriptor package1 =
|
||||
DescriptorUtils.getParentOfType(descriptor1, PackageFragmentDescriptor.class, false);
|
||||
PackageFragmentDescriptor package2 =
|
||||
DescriptorUtils.getParentOfType(descriptor2, PackageFragmentDescriptor.class, false);
|
||||
|
||||
return package2 != null && package1 != null &&
|
||||
package1.getFqName().equals(package2.getFqName());
|
||||
}
|
||||
|
||||
private void addChild(@NotNull CodegenContext child) {
|
||||
if (shouldAddChild(child.contextDescriptor)) {
|
||||
if (childContexts == null) {
|
||||
@@ -748,29 +740,23 @@ public abstract class CodegenContext<T extends DeclarationDescriptor> {
|
||||
return enclosingLocalLookup;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
@NotNull
|
||||
public AccessorForCompanionObjectInstanceFieldDescriptor markCompanionObjectDescriptorWithAccessorRequired(@NotNull ClassDescriptor companionObjectDescriptor) {
|
||||
assert DescriptorUtils.isCompanionObject(companionObjectDescriptor) : "Companion object expected: " + companionObjectDescriptor;
|
||||
|
||||
assert accessorForCompanionObjectInstanceFieldDescriptor == null
|
||||
|| accessorForCompanionObjectInstanceFieldDescriptor.getCompanionObjectDescriptor() == companionObjectDescriptor
|
||||
: "Unexpected companion object descriptor with accessor required: " + companionObjectDescriptor +
|
||||
"; should be " + accessorForCompanionObjectInstanceFieldDescriptor.getCompanionObjectDescriptor();
|
||||
|
||||
if (accessorForCompanionObjectInstanceFieldDescriptor == null) {
|
||||
accessorForCompanionObjectInstanceFieldDescriptor =
|
||||
new AccessorForCompanionObjectInstanceFieldDescriptor(
|
||||
companionObjectDescriptor,
|
||||
Name.identifier(JvmCodegenUtil.getCompanionObjectAccessorName(companionObjectDescriptor))
|
||||
);
|
||||
}
|
||||
|
||||
return accessorForCompanionObjectInstanceFieldDescriptor;
|
||||
public AccessorForCompanionObjectInstanceFieldDescriptor getOrCreateAccessorForCompanionObject(
|
||||
@NotNull ClassDescriptor companionObjectDescriptor
|
||||
) {
|
||||
return accessorsForCompanionObjects.computeIfAbsent(
|
||||
companionObjectDescriptor,
|
||||
it -> new AccessorForCompanionObjectInstanceFieldDescriptor(
|
||||
it,
|
||||
Name.identifier(JvmCodegenUtil.getCompanionObjectAccessorName(it))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AccessorForCompanionObjectInstanceFieldDescriptor getAccessorForCompanionObjectDescriptorIfRequired() {
|
||||
return accessorForCompanionObjectInstanceFieldDescriptor;
|
||||
@NotNull
|
||||
public Collection<AccessorForCompanionObjectInstanceFieldDescriptor> getRequiredAccessorsForCompanionObjects() {
|
||||
return accessorsForCompanionObjects.values();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ public class MethodContext extends CodegenContext<CallableMemberDescriptor> {
|
||||
return "Method: " + getContextDescriptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInlineMethodContext() {
|
||||
return InlineUtil.isInline(getFunctionDescriptor());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user