mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-05 01:50:56 +07:00
do not perform double conversion KtType<->JvmType, to avoid losing some type info in tricky cases (part of KTIJ-28926 K2: Create Function From Usage Fix)
GitOrigin-RevId: 1c86e6c4ef2c3ba3dbcff8caca906eac57d083aa
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9b68f11396
commit
bd069c730f
@@ -8,7 +8,7 @@ import com.intellij.psi.PsiTypeVisitor
|
||||
import com.intellij.psi.PsiTypes
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
|
||||
class ExpectedTypeWithNullability(val type: JvmType, val nullability: Nullability) : ExpectedType {
|
||||
open class ExpectedTypeWithNullability(val type: JvmType, val nullability: Nullability) : ExpectedType {
|
||||
override fun getTheType(): JvmType = type
|
||||
|
||||
override fun getTheKind(): ExpectedType.Kind = ExpectedType.Kind.EXACT
|
||||
|
||||
@@ -82,10 +82,10 @@ object CreateFromUsageUtil {
|
||||
|
||||
val declarationInPlace = when {
|
||||
declaration is KtPrimaryConstructor -> {
|
||||
(container as KtClass).createPrimaryConstructorIfAbsent().replaced(declaration)
|
||||
(container as KtClass).createPrimaryConstructorIfAbsent().replaced(declaration)
|
||||
}
|
||||
|
||||
declaration is KtProperty && container !is KtBlockExpression -> {
|
||||
declaration is KtProperty && container !is KtBlockExpression -> {
|
||||
val sibling = actualContainer.getChildOfType<KtProperty>() ?: when (actualContainer) {
|
||||
is KtClassBody -> actualContainer.declarations.firstOrNull() ?: actualContainer.rBrace
|
||||
is KtFile -> actualContainer.declarations.first()
|
||||
@@ -126,7 +126,7 @@ object CreateFromUsageUtil {
|
||||
sibling = container.body?.lBrace
|
||||
}
|
||||
|
||||
org.jetbrains.kotlin.idea.core.insertMembersAfterAndReformat(null, container, declaration, sibling)
|
||||
org.jetbrains.kotlin.idea.core.insertMembersAfterAndReformat(null, container, declaration, sibling)
|
||||
}
|
||||
else -> throw KotlinExceptionWithAttachments("Invalid containing element: ${container::class.java}")
|
||||
.withPsiAttachment("container", container)
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiNameHelper
|
||||
import com.intellij.psi.SmartPsiElementPointer
|
||||
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
|
||||
import org.jetbrains.kotlin.analysis.api.analyze
|
||||
import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol
|
||||
import org.jetbrains.kotlin.analysis.api.types.KtTypeParameterType
|
||||
@@ -38,16 +39,16 @@ internal class CreateKotlinCallableAction(
|
||||
private val myText: String,
|
||||
pointerToContainer: SmartPsiElementPointer<*>,
|
||||
) : CreateKotlinElementAction(request, pointerToContainer), JvmGroupIntentionAction {
|
||||
data class ParamCandidate(val names: Collection<String>, val renderedTypes: List<String>)
|
||||
private val parameterCandidates: List<ParamCandidate> = renderCandidatesOfParameterTypes()
|
||||
private val candidatesOfRenderedReturnType: List<String> = renderCandidatesOfReturnType()
|
||||
private val containerClassFqName: FqName? = (getContainer() as? KtClassOrObject)?.fqName
|
||||
|
||||
private val call: PsiElement? = when (request) {
|
||||
is CreateMethodFromKotlinUsageRequest -> request.call
|
||||
is CreateExecutableFromJavaUsageRequest<*> -> request.call
|
||||
else -> null
|
||||
}
|
||||
data class ParamCandidate(val names: Collection<String>, val renderedTypes: List<String>)
|
||||
private val parameterCandidates: List<ParamCandidate> = renderCandidatesOfParameterTypes()
|
||||
private val candidatesOfRenderedReturnType: List<String> = renderCandidatesOfReturnType()
|
||||
private val containerClassFqName: FqName? = (getContainer() as? KtClassOrObject)?.fqName
|
||||
|
||||
private val isForCompanion: Boolean = (request as? CreateMethodFromKotlinUsageRequest)?.isForCompanion == true
|
||||
|
||||
// Note that this property must be initialized after initializing above properties, because it has dependency on them.
|
||||
@@ -96,19 +97,12 @@ internal class CreateKotlinCallableAction(
|
||||
}
|
||||
|
||||
private fun renderCandidatesOfParameterTypes(): List<ParamCandidate> {
|
||||
request.expectedParameters.map { it.semanticNames }
|
||||
val container = getContainer()
|
||||
return request.expectedParameters.map { expectedParameter ->
|
||||
val types = if (container == null) listOf("Any") else
|
||||
analyze(container) {
|
||||
analyze(call as? KtElement ?: container) {
|
||||
expectedParameter.expectedTypes.map {
|
||||
val parameterType =
|
||||
if (it is ExpectedTypeWithNullability) {
|
||||
toKtTypeWithNullability(it, container)
|
||||
} else {
|
||||
it.theType.toKtType(container)
|
||||
}
|
||||
parameterType?.render(renderer = WITH_TYPE_NAMES_FOR_CREATE_ELEMENTS, position = Variance.INVARIANT) ?: "Any"
|
||||
renderTypeName(it, container) ?: "Any"
|
||||
}
|
||||
}
|
||||
ParamCandidate(expectedParameter.semanticNames, types)
|
||||
@@ -117,17 +111,24 @@ internal class CreateKotlinCallableAction(
|
||||
|
||||
private fun renderCandidatesOfReturnType(): List<String> {
|
||||
val container = getContainer() ?: return emptyList()
|
||||
return analyze(container) {
|
||||
return analyze(call as? KtElement ?: container) {
|
||||
request.returnType.mapNotNull { returnType ->
|
||||
val returnKtType = if (returnType is ExpectedTypeWithNullability) toKtTypeWithNullability(returnType, container) else returnType.theType.toKtType(container)
|
||||
if (returnKtType == null || returnKtType == builtinTypes.UNIT) null
|
||||
else returnKtType.render(renderer = WITH_TYPE_NAMES_FOR_CREATE_ELEMENTS, position = Variance.INVARIANT)
|
||||
renderTypeName(returnType, container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context (KtAnalysisSession)
|
||||
private fun renderTypeName(expectedType: ExpectedType, container: KtElement): String? {
|
||||
val returnKtType = if (expectedType is ExpectedKotlinType) expectedType.ktType else expectedType.toKtTypeWithNullability(container)
|
||||
if (returnKtType == null || returnKtType == builtinTypes.UNIT) return null
|
||||
if (!isAccessibleInCreationPlace(returnKtType, (request as? CreateMethodFromKotlinUsageRequest)?.call)) return null
|
||||
return returnKtType.render(renderer = WITH_TYPE_NAMES_FOR_CREATE_ELEMENTS, position = Variance.INVARIANT)
|
||||
}
|
||||
|
||||
private fun buildCallableAsString(): String? {
|
||||
if (call == null || getContainer() == null) return null
|
||||
val container = getContainer()
|
||||
if (call == null || container == null) return null
|
||||
val modifierListAsString =
|
||||
request.modifiers.filter{it != JvmModifier.PUBLIC}.joinToString(
|
||||
separator = " ",
|
||||
@@ -143,7 +144,7 @@ internal class CreateKotlinCallableAction(
|
||||
append(" ")
|
||||
|
||||
val (receiver, receiverTypeText) = if (request is CreateMethodFromKotlinUsageRequest) CreateKotlinCallableActionTextBuilder.renderReceiver(request) else "" to ""
|
||||
append(renderTypeParameterDeclarations(request, receiver, receiverTypeText))
|
||||
append(renderTypeParameterDeclarations(request, container, receiver, receiverTypeText))
|
||||
append(request.methodName)
|
||||
append("(")
|
||||
append(renderParameterList())
|
||||
@@ -153,10 +154,15 @@ internal class CreateKotlinCallableAction(
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderTypeParameterDeclarations(request: CreateMethodRequest, receiver:String, receiverTypeText: String): String {
|
||||
private fun renderTypeParameterDeclarations(
|
||||
request: CreateMethodRequest,
|
||||
container: KtElement,
|
||||
receiver: String,
|
||||
receiverTypeText: String
|
||||
): String {
|
||||
if (request is CreateMethodFromKotlinUsageRequest && request.receiverExpression != null && request.isExtension) {
|
||||
val t = if (receiver.isNotEmpty()) "$receiver " else receiver
|
||||
analyze (request.receiverExpression) {
|
||||
analyze (call as? KtElement ?: container) {
|
||||
val receiverSymbol = request.receiverExpression.resolveExpression()
|
||||
if (receiverSymbol is KtCallableSymbol && receiverSymbol.returnType is KtTypeParameterType) {
|
||||
return ("<$receiverTypeText> $t")
|
||||
|
||||
@@ -60,7 +60,7 @@ internal class CreateKotlinCallablePsiEditor(
|
||||
val passedContainerElement = pointerToContainer.element
|
||||
if (passedContainerElement == null) return
|
||||
val shouldComputeContainerFromAnchor = if (passedContainerElement is PsiFile) passedContainerElement == anchor.containingFile
|
||||
else passedContainerElement.getContainer() == anchor.getContainer()
|
||||
else passedContainerElement.getContainer() == anchor.getContainer()
|
||||
val insertContainer: PsiElement = if (shouldComputeContainerFromAnchor) {
|
||||
(anchor.getExtractionContainers().firstOrNull() ?: return)
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.intellij.lang.jvm.actions.CreateMethodRequest
|
||||
import com.intellij.lang.jvm.actions.ExpectedType
|
||||
import com.intellij.lang.jvm.types.JvmReferenceType
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
|
||||
import org.jetbrains.kotlin.analysis.api.analyze
|
||||
import org.jetbrains.kotlin.analysis.api.types.KtType
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
@@ -17,7 +16,7 @@ import org.jetbrains.kotlin.psi.KtSimpleNameExpression
|
||||
* A request to create Kotlin callable from the usage in Kotlin.
|
||||
*/
|
||||
internal class CreateMethodFromKotlinUsageRequest (
|
||||
private val functionCall: KtCallExpression,
|
||||
functionCall: KtCallExpression,
|
||||
modifiers: Collection<JvmModifier>,
|
||||
val receiverExpression: KtExpression?,
|
||||
val receiverType: KtType?, // (in case receiverExpression is null) it can be notnull when there's implicit receiver: `blah { unknownFunc() }`
|
||||
@@ -25,19 +24,16 @@ internal class CreateMethodFromKotlinUsageRequest (
|
||||
val isAbstractClassOrInterface: Boolean,
|
||||
val isForCompanion: Boolean
|
||||
) : CreateExecutableFromKotlinUsageRequest<KtCallExpression>(functionCall, modifiers), CreateMethodRequest {
|
||||
private val returnType = mutableListOf<ExpectedType>()
|
||||
private val returnType:List<ExpectedType> = initializeReturnType(functionCall)
|
||||
|
||||
init {
|
||||
analyze(functionCall) {
|
||||
initializeReturnType()
|
||||
}
|
||||
}
|
||||
|
||||
context (KtAnalysisSession)
|
||||
private fun initializeReturnType() {
|
||||
val returnJvmType = functionCall.getExpectedKotlinType() ?: return
|
||||
(returnJvmType.theType as? JvmReferenceType)?.let { if (it.resolve() == null) return }
|
||||
returnType.add(returnJvmType)
|
||||
private fun initializeReturnType(functionCall: KtCallExpression): List<ExpectedType> {
|
||||
return analyze(functionCall) {
|
||||
val returnJvmType = functionCall.getExpectedKotlinType() ?: return emptyList()
|
||||
if (returnJvmType !is ExpectedKotlinType) {
|
||||
(returnJvmType.theType as? JvmReferenceType)?.let { if (it.resolve() == null) return emptyList() }
|
||||
}
|
||||
listOf(returnJvmType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean = super.isValid() && getReferenceName() != null
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.kotlin.idea.k2.codeinsight.quickFixes.createFromUsage
|
||||
|
||||
import com.intellij.lang.jvm.actions.ExpectedTypeWithNullability
|
||||
import com.intellij.lang.jvm.types.JvmType
|
||||
import org.jetbrains.kotlin.analysis.api.types.KtType
|
||||
|
||||
class ExpectedKotlinType(val ktType: KtType, jvmType: JvmType) : ExpectedTypeWithNullability(jvmType, ktType.nullability.toNullability())
|
||||
@@ -4,6 +4,7 @@ package org.jetbrains.kotlin.idea.k2.codeinsight.quickFixes.createFromUsage
|
||||
import com.intellij.codeInsight.Nullability
|
||||
import com.intellij.lang.jvm.JvmClass
|
||||
import com.intellij.lang.jvm.actions.ExpectedParameter
|
||||
import com.intellij.lang.jvm.actions.ExpectedType
|
||||
import com.intellij.lang.jvm.actions.ExpectedTypeWithNullability
|
||||
import com.intellij.lang.jvm.actions.expectedParameter
|
||||
import com.intellij.lang.jvm.types.JvmType
|
||||
@@ -27,6 +28,7 @@ import org.jetbrains.kotlin.analysis.api.renderer.types.renderers.KtFlexibleType
|
||||
import org.jetbrains.kotlin.analysis.api.renderer.types.renderers.KtTypeProjectionRenderer
|
||||
import org.jetbrains.kotlin.analysis.api.symbols.*
|
||||
import org.jetbrains.kotlin.analysis.api.types.*
|
||||
import org.jetbrains.kotlin.analysis.api.types.KtIntersectionType
|
||||
import org.jetbrains.kotlin.analysis.utils.printer.PrettyPrinter
|
||||
import org.jetbrains.kotlin.asJava.findFacadeClass
|
||||
import org.jetbrains.kotlin.asJava.toLightClass
|
||||
@@ -72,7 +74,7 @@ context (KtAnalysisSession)
|
||||
internal fun KtType.convertToClass(): KtClass? = expandedClassSymbol?.psi as? KtClass
|
||||
|
||||
context (KtAnalysisSession)
|
||||
internal fun KtElement.getExpectedKotlinType(): ExpectedTypeWithNullability? {
|
||||
internal fun KtElement.getExpectedKotlinType(): ExpectedType? {
|
||||
var expectedType = getExpectedType()
|
||||
if (expectedType == null) {
|
||||
val parent = this.parent
|
||||
@@ -103,8 +105,9 @@ internal fun KtElement.getExpectedKotlinType(): ExpectedTypeWithNullability? {
|
||||
expectedType = getExpectedTypeByStringTemplateEntry(this)
|
||||
}
|
||||
}
|
||||
val jvmType = expectedType?.convertToJvmType(this) ?: return null
|
||||
return ExpectedTypeWithNullability.createExpectedKotlinType(jvmType, expectedType.nullability.toNullability())
|
||||
if (expectedType == null) return null
|
||||
val jvmType = expectedType.convertToJvmType(this) ?: return null
|
||||
return ExpectedKotlinType(expectedType, jvmType)
|
||||
}
|
||||
|
||||
// Given: `println("a = ${A().foo()}")`
|
||||
@@ -138,7 +141,7 @@ private fun getExpectedTypeByFunctionExpressionBody(expression: KtExpression): K
|
||||
}
|
||||
|
||||
context (KtAnalysisSession)
|
||||
private fun KtType.convertToJvmType(useSitePosition: PsiElement): JvmType? = asPsiType(useSitePosition, allowErrorTypes = false)
|
||||
internal fun KtType.convertToJvmType(useSitePosition: PsiElement): JvmType? = asPsiType(useSitePosition, allowErrorTypes = false)
|
||||
|
||||
context (KtAnalysisSession)
|
||||
internal fun KtExpression.getClassOfExpressionType(): PsiElement? = when (val symbol = resolveExpression()) {
|
||||
@@ -154,7 +157,7 @@ internal fun KtValueArgument.getExpectedParameterInfo(parameterIndex: Int): Expe
|
||||
val expectedArgumentType = argumentExpression?.getKtType()
|
||||
val parameterNames = parameterNameAsString?.let { sequenceOf(it) } ?: expectedArgumentType?.let { NAME_SUGGESTER.suggestTypeNames(it) }
|
||||
val parameterType = expectedArgumentType?.convertToJvmType(argumentExpression)
|
||||
val expectedType = if (parameterType == null) ExpectedTypeWithNullability.INVALID_TYPE else ExpectedTypeWithNullability.createExpectedKotlinType(parameterType, expectedArgumentType.nullability.toNullability())
|
||||
val expectedType = if (parameterType == null) ExpectedTypeWithNullability.INVALID_TYPE else ExpectedKotlinType(expectedArgumentType, parameterType)
|
||||
val names = parameterNames?.toList()?.toTypedArray() ?: arrayOf("p$parameterIndex")
|
||||
return expectedParameter(expectedType, *names)
|
||||
}
|
||||
@@ -236,13 +239,15 @@ internal fun JvmType.toKtType(useSitePosition: PsiElement): KtType? = when (this
|
||||
}
|
||||
|
||||
context (KtAnalysisSession)
|
||||
fun toKtTypeWithNullability(type: ExpectedTypeWithNullability, useSitePosition: PsiElement): KtType? {
|
||||
val ktTypeNullability = when (type.nullability) {
|
||||
fun ExpectedType.toKtTypeWithNullability(useSitePosition: PsiElement): KtType? {
|
||||
val nullability = if (this is ExpectedTypeWithNullability) this.nullability else null
|
||||
val ktTypeNullability = when (nullability) {
|
||||
Nullability.NOT_NULL -> KtTypeNullability.NON_NULLABLE
|
||||
Nullability.NULLABLE -> KtTypeNullability.NULLABLE
|
||||
Nullability.UNKNOWN -> KtTypeNullability.UNKNOWN
|
||||
null -> null
|
||||
}
|
||||
return type.type.toKtType(useSitePosition)?.withNullability(ktTypeNullability)
|
||||
return theType.toKtType(useSitePosition)?.let { if (ktTypeNullability == null) it else it.withNullability(ktTypeNullability) }
|
||||
}
|
||||
|
||||
fun KtTypeNullability.toNullability() : Nullability {
|
||||
@@ -252,3 +257,40 @@ fun KtTypeNullability.toNullability() : Nullability {
|
||||
KtTypeNullability.UNKNOWN -> Nullability.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// inspect `type` recursively and call `predicate` on all types inside, return true if all calls returned true
|
||||
context (KtAnalysisSession)
|
||||
private fun accept(type: KtType?, visited: MutableSet<KtType>, predicate: (KtType) -> Boolean) : Boolean {
|
||||
if (type == null || !visited.add(type)) return true
|
||||
if (!predicate.invoke(type)) return false
|
||||
return when (type) {
|
||||
is KtClassType -> type.qualifiers.flatMap { it.typeArguments }.map { it.type}.all { accept(it, visited, predicate)}
|
||||
&& (type !is KtFunctionalType || (accept(type.returnType, visited,predicate) && accept(type.receiverType, visited, predicate)))
|
||||
is KtFlexibleType -> accept(type.lowerBound, visited, predicate) && accept(type.upperBound, visited, predicate)
|
||||
is KtCapturedType -> accept(type.projection.type, visited, predicate)
|
||||
is KtDefinitelyNotNullType -> accept(type.original, visited, predicate)
|
||||
is KtIntersectionType -> type.conjuncts.all { accept(it, visited, predicate) }
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
// true if this type is accessible in the newly created method
|
||||
context (KtAnalysisSession)
|
||||
fun isAccessibleInCreationPlace(ktType: KtType, call: KtElement?): Boolean {
|
||||
if (call == null) return true
|
||||
fun KtTypeParameter.getOwningTypeParameterOwner(): KtTypeParameterListOwner? {
|
||||
val parameterList = parent as? KtTypeParameterList ?: return null
|
||||
return parameterList.parent as? KtTypeParameterListOwner
|
||||
}
|
||||
return accept(ktType, mutableSetOf()) { ktLeaf ->
|
||||
if (ktLeaf is KtTypeParameterType) {
|
||||
// having `<T> caller(T t) { unknownMethod(t); }` the type `T` is not accessible in created method unknownMethod(t)
|
||||
val owner = (ktLeaf.symbol.psi as? KtTypeParameter)?.getOwningTypeParameterOwner()
|
||||
// todo must have been "insertion point" instead of `call`
|
||||
owner == null || PsiTreeUtil.isAncestor(owner, call, false)
|
||||
} else {
|
||||
// KtErrorType means this type is unresolved in the context of container
|
||||
ktLeaf !is KtErrorType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,22 +45,24 @@ private fun getTargetCallExpression(element: KtElement): KtCallExpression? {
|
||||
private fun KtSimpleNameExpression.referenceNameOfElement(): Boolean = getReferencedNameElementType() == KtTokens.IDENTIFIER
|
||||
|
||||
internal fun buildRequestsAndActions(callExpression: KtCallExpression): List<IntentionAction> {
|
||||
val methodRequests = buildRequests(callExpression)
|
||||
val extensions = EP_NAME.extensions
|
||||
return methodRequests.flatMap { (targetClass, request) ->
|
||||
extensions.flatMap { ext ->
|
||||
ext.createAddMethodActions(targetClass, request)
|
||||
}
|
||||
}.groupActionsByType(KotlinLanguage.INSTANCE)
|
||||
analyze(callExpression) {
|
||||
val methodRequests = buildRequests(callExpression)
|
||||
val extensions = EP_NAME.extensions
|
||||
return methodRequests.flatMap { (targetClass, request) ->
|
||||
extensions.flatMap { ext ->
|
||||
ext.createAddMethodActions(targetClass, request)
|
||||
}
|
||||
}.groupActionsByType(KotlinLanguage.INSTANCE)
|
||||
}
|
||||
}
|
||||
|
||||
context(KtAnalysisSession)
|
||||
internal fun buildRequests(callExpression: KtCallExpression): List<Pair<JvmClass, CreateMethodRequest>> {
|
||||
val calleeExpression = callExpression.calleeExpression as? KtSimpleNameExpression ?: return emptyList()
|
||||
val requests = mutableListOf<Pair<JvmClass, CreateMethodRequest>>()
|
||||
val receiverExpression = calleeExpression.getReceiverExpression()
|
||||
|
||||
// Register default create-from-usage request.
|
||||
analyze(callExpression) {
|
||||
// TODO: Check whether this class or file can be edited (Use `canRefactor()`).
|
||||
val defaultContainerPsi = calleeExpression.getReceiverOrContainerPsiElement()
|
||||
val defaultClassForReceiverOrFile = calleeExpression.getReceiverOrContainerClass(defaultContainerPsi)
|
||||
@@ -116,7 +118,6 @@ internal fun buildRequests(callExpression: KtCallExpression): List<Pair<JvmClass
|
||||
isForCompanion = false
|
||||
))
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class A<T>(val t: T) {
|
||||
var x: A<Int> by foo(t, "")
|
||||
}
|
||||
|
||||
private fun foo(p0: T, string: String): ReadWriteProperty<Any, A<Int>> {
|
||||
private fun foo(p0: T, string: String): ReadWriteProperty<Any?, A<Int>> {
|
||||
<selection>TODO("Not yet implemented")<caret></selection>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user