K2: report errors related to SubClassOptInRequired annotation
Related to KT-41886
This commit is contained in:
committed by
teamcity
parent
c67c5cad27
commit
7d3368da58
+7
-3
@@ -92,12 +92,16 @@ fun FirClassLikeSymbol<*>.getTargetAnnotation(): FirAnnotation? {
|
|||||||
|
|
||||||
fun FirExpression.extractClassesFromArgument(): List<FirRegularClassSymbol> {
|
fun FirExpression.extractClassesFromArgument(): List<FirRegularClassSymbol> {
|
||||||
return unfoldArrayOrVararg().mapNotNull {
|
return unfoldArrayOrVararg().mapNotNull {
|
||||||
if (it !is FirGetClassCall) return@mapNotNull null
|
it.extractClassFromArgument()
|
||||||
val qualifier = it.argument as? FirResolvedQualifier ?: return@mapNotNull null
|
|
||||||
qualifier.symbol as? FirRegularClassSymbol
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun FirExpression.extractClassFromArgument(): FirRegularClassSymbol? {
|
||||||
|
if (this !is FirGetClassCall) return null
|
||||||
|
val qualifier = argument as? FirResolvedQualifier ?: return null
|
||||||
|
return qualifier.symbol as? FirRegularClassSymbol
|
||||||
|
}
|
||||||
|
|
||||||
fun checkRepeatedAnnotation(
|
fun checkRepeatedAnnotation(
|
||||||
useSiteTarget: AnnotationUseSiteTarget?,
|
useSiteTarget: AnnotationUseSiteTarget?,
|
||||||
existingTargetsForAnnotation: MutableList<AnnotationUseSiteTarget?>,
|
existingTargetsForAnnotation: MutableList<AnnotationUseSiteTarget?>,
|
||||||
|
|||||||
+62
-25
@@ -25,10 +25,7 @@ import org.jetbrains.kotlin.fir.scopes.processDirectlyOverriddenProperties
|
|||||||
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
|
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
|
||||||
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
|
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
|
||||||
import org.jetbrains.kotlin.fir.symbols.lazyResolveToPhase
|
import org.jetbrains.kotlin.fir.symbols.lazyResolveToPhase
|
||||||
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
|
import org.jetbrains.kotlin.fir.symbols.impl.*
|
||||||
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
|
|
||||||
import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol
|
|
||||||
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
|
|
||||||
import org.jetbrains.kotlin.fir.types.*
|
import org.jetbrains.kotlin.fir.types.*
|
||||||
import org.jetbrains.kotlin.name.ClassId
|
import org.jetbrains.kotlin.name.ClassId
|
||||||
import org.jetbrains.kotlin.name.Name
|
import org.jetbrains.kotlin.name.Name
|
||||||
@@ -41,7 +38,8 @@ object FirOptInUsageBaseChecker {
|
|||||||
val annotationClassId: ClassId,
|
val annotationClassId: ClassId,
|
||||||
val severity: Severity,
|
val severity: Severity,
|
||||||
val message: String?,
|
val message: String?,
|
||||||
val supertypeName: String? = null
|
val supertypeName: String? = null,
|
||||||
|
val fromSupertype: Boolean = false
|
||||||
) {
|
) {
|
||||||
enum class Severity { WARNING, ERROR }
|
enum class Severity { WARNING, ERROR }
|
||||||
companion object {
|
companion object {
|
||||||
@@ -77,25 +75,34 @@ object FirOptInUsageBaseChecker {
|
|||||||
fun FirBasedSymbol<*>.loadExperimentalitiesFromAnnotationTo(session: FirSession, result: MutableCollection<Experimentality>) {
|
fun FirBasedSymbol<*>.loadExperimentalitiesFromAnnotationTo(session: FirSession, result: MutableCollection<Experimentality>) {
|
||||||
lazyResolveToPhase(FirResolvePhase.STATUS)
|
lazyResolveToPhase(FirResolvePhase.STATUS)
|
||||||
@OptIn(SymbolInternals::class)
|
@OptIn(SymbolInternals::class)
|
||||||
fir.loadExperimentalitiesFromAnnotationTo(session, result)
|
fir.loadExperimentalitiesFromAnnotationTo(session, result, fromSupertype = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun FirDeclaration.loadExperimentalitiesFromAnnotationTo(
|
private fun FirDeclaration.loadExperimentalitiesFromAnnotationTo(
|
||||||
session: FirSession,
|
session: FirSession,
|
||||||
result: MutableCollection<Experimentality>
|
result: MutableCollection<Experimentality>,
|
||||||
|
fromSupertype: Boolean
|
||||||
) {
|
) {
|
||||||
for (annotation in annotations) {
|
for (annotation in annotations) {
|
||||||
val annotationType = annotation.annotationTypeRef.coneTypeSafe<ConeClassLikeType>()
|
val annotationType = annotation.annotationTypeRef.coneTypeSafe<ConeClassLikeType>() ?: continue
|
||||||
val className = when (this) {
|
val className = when (this) {
|
||||||
is FirRegularClass -> name.asString()
|
is FirRegularClass -> name.asString()
|
||||||
is FirCallableDeclaration -> symbol.callableId.className?.shortName()?.asString()
|
is FirCallableDeclaration -> symbol.callableId.className?.shortName()?.asString()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
result.addIfNotNull(
|
result.addIfNotNull(
|
||||||
annotationType?.lookupTag?.toFirRegularClassSymbol(
|
annotationType.lookupTag.toFirRegularClassSymbol(
|
||||||
session
|
session
|
||||||
)?.loadExperimentalityForMarkerAnnotation(className)
|
)?.loadExperimentalityForMarkerAnnotation(className)
|
||||||
)
|
)
|
||||||
|
if (fromSupertype) {
|
||||||
|
if (annotationType.lookupTag.classId == OptInNames.SUBCLASS_OPT_IN_REQUIRED_CLASS_ID) {
|
||||||
|
val annotationClass = annotation.findArgumentByName(OptInNames.USE_EXPERIMENTAL_ANNOTATION_CLASS) ?: continue
|
||||||
|
result.addIfNotNull(
|
||||||
|
annotationClass.extractClassFromArgument()?.loadExperimentalityForMarkerAnnotation()?.copy(fromSupertype = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +129,12 @@ object FirOptInUsageBaseChecker {
|
|||||||
fun FirBasedSymbol<*>.loadExperimentalities(
|
fun FirBasedSymbol<*>.loadExperimentalities(
|
||||||
context: CheckerContext, fromSetter: Boolean, dispatchReceiverType: ConeKotlinType?
|
context: CheckerContext, fromSetter: Boolean, dispatchReceiverType: ConeKotlinType?
|
||||||
): Set<Experimentality> = loadExperimentalities(
|
): Set<Experimentality> = loadExperimentalities(
|
||||||
context, knownExperimentalities = null, visited = mutableSetOf(), fromSetter, dispatchReceiverType
|
context, knownExperimentalities = null, visited = mutableSetOf(), fromSetter, dispatchReceiverType, fromSupertype = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun FirClassLikeSymbol<*>.loadExperimentalitiesFromSupertype(context: CheckerContext): Set<Experimentality> = loadExperimentalities(
|
||||||
|
context, knownExperimentalities = null, visited = mutableSetOf(),
|
||||||
|
fromSetter = false, dispatchReceiverType = null, fromSupertype = true
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(SymbolInternals::class)
|
@OptIn(SymbolInternals::class)
|
||||||
@@ -132,6 +144,7 @@ object FirOptInUsageBaseChecker {
|
|||||||
visited: MutableSet<FirDeclaration>,
|
visited: MutableSet<FirDeclaration>,
|
||||||
fromSetter: Boolean,
|
fromSetter: Boolean,
|
||||||
dispatchReceiverType: ConeKotlinType?,
|
dispatchReceiverType: ConeKotlinType?,
|
||||||
|
fromSupertype: Boolean,
|
||||||
): Set<Experimentality> {
|
): Set<Experimentality> {
|
||||||
lazyResolveToPhase(FirResolvePhase.STATUS)
|
lazyResolveToPhase(FirResolvePhase.STATUS)
|
||||||
val fir = this.fir
|
val fir = this.fir
|
||||||
@@ -145,12 +158,14 @@ object FirOptInUsageBaseChecker {
|
|||||||
val parentClassScope = parentClassSymbol?.unsubstitutedScope(context)
|
val parentClassScope = parentClassSymbol?.unsubstitutedScope(context)
|
||||||
if (this is FirNamedFunctionSymbol) {
|
if (this is FirNamedFunctionSymbol) {
|
||||||
parentClassScope?.processDirectlyOverriddenFunctions(this) {
|
parentClassScope?.processDirectlyOverriddenFunctions(this) {
|
||||||
it.loadExperimentalities(context, result, visited, fromSetter = false, dispatchReceiverType = null)
|
it.loadExperimentalities(
|
||||||
|
context, result, visited, fromSetter = false, dispatchReceiverType = null, fromSupertype = false
|
||||||
|
)
|
||||||
ProcessorAction.NEXT
|
ProcessorAction.NEXT
|
||||||
}
|
}
|
||||||
} else if (this is FirPropertySymbol) {
|
} else if (this is FirPropertySymbol) {
|
||||||
parentClassScope?.processDirectlyOverriddenProperties(this) {
|
parentClassScope?.processDirectlyOverriddenProperties(this) {
|
||||||
it.loadExperimentalities(context, result, visited, fromSetter, dispatchReceiverType = null)
|
it.loadExperimentalities(context, result, visited, fromSetter, dispatchReceiverType = null, fromSupertype = false)
|
||||||
ProcessorAction.NEXT
|
ProcessorAction.NEXT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,19 +181,25 @@ object FirOptInUsageBaseChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dispatchReceiverType == null) {
|
if (dispatchReceiverType == null) {
|
||||||
parentClassSymbol?.loadExperimentalities(context, result, visited, fromSetter = false, dispatchReceiverType = null)
|
parentClassSymbol?.loadExperimentalities(
|
||||||
|
context, result, visited, fromSetter = false, dispatchReceiverType = null, fromSupertype = false
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
dispatchReceiverType.addExperimentalities(context, result, visited)
|
dispatchReceiverType.addExperimentalities(context, result, visited)
|
||||||
}
|
}
|
||||||
if (fromSetter && this is FirPropertySymbol) {
|
if (fromSetter && this is FirPropertySymbol) {
|
||||||
setterSymbol?.loadExperimentalities(context, result, visited, fromSetter = false, dispatchReceiverType)
|
setterSymbol?.loadExperimentalities(
|
||||||
|
context, result, visited, fromSetter = false, dispatchReceiverType, fromSupertype = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (this is FirRegularClassSymbol && fir is FirRegularClass && !fir.isLocal) {
|
} else if (this is FirRegularClassSymbol && fir is FirRegularClass && !fir.isLocal) {
|
||||||
val parentClassSymbol = outerClassSymbol(context)
|
val parentClassSymbol = outerClassSymbol(context)
|
||||||
parentClassSymbol?.loadExperimentalities(context, result, visited, fromSetter = false, dispatchReceiverType = null)
|
parentClassSymbol?.loadExperimentalities(
|
||||||
|
context, result, visited, fromSetter = false, dispatchReceiverType = null, fromSupertype = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fir.loadExperimentalitiesFromAnnotationTo(session, result)
|
fir.loadExperimentalitiesFromAnnotationTo(session, result, fromSupertype)
|
||||||
|
|
||||||
if (fir is FirTypeAlias) {
|
if (fir is FirTypeAlias) {
|
||||||
fir.expandedTypeRef.coneType.addExperimentalities(context, result, visited)
|
fir.expandedTypeRef.coneType.addExperimentalities(context, result, visited)
|
||||||
@@ -205,7 +226,7 @@ object FirOptInUsageBaseChecker {
|
|||||||
) {
|
) {
|
||||||
if (this !is ConeClassLikeType) return
|
if (this !is ConeClassLikeType) return
|
||||||
lookupTag.toSymbol(context.session)?.loadExperimentalities(
|
lookupTag.toSymbol(context.session)?.loadExperimentalities(
|
||||||
context, result, visited, fromSetter = false, dispatchReceiverType = null
|
context, result, visited, fromSetter = false, dispatchReceiverType = null, fromSupertype = false
|
||||||
)
|
)
|
||||||
fullyExpandedType(context.session).typeArguments.forEach {
|
fullyExpandedType(context.session).typeArguments.forEach {
|
||||||
if (!it.isStarProjection) it.type?.addExperimentalities(context, result, visited)
|
if (!it.isStarProjection) it.type?.addExperimentalities(context, result, visited)
|
||||||
@@ -230,8 +251,8 @@ object FirOptInUsageBaseChecker {
|
|||||||
context: CheckerContext,
|
context: CheckerContext,
|
||||||
reporter: DiagnosticReporter
|
reporter: DiagnosticReporter
|
||||||
) {
|
) {
|
||||||
for ((annotationClassId, severity, message) in experimentalities) {
|
for ((annotationClassId, severity, message, _, fromSupertype) in experimentalities) {
|
||||||
if (!isExperimentalityAcceptableInContext(annotationClassId, context)) {
|
if (!isExperimentalityAcceptableInContext(annotationClassId, context, fromSupertype)) {
|
||||||
val (diagnostic, verb) = when (severity) {
|
val (diagnostic, verb) = when (severity) {
|
||||||
Experimentality.Severity.WARNING -> FirErrors.OPT_IN_USAGE to "should"
|
Experimentality.Severity.WARNING -> FirErrors.OPT_IN_USAGE to "should"
|
||||||
Experimentality.Severity.ERROR -> FirErrors.OPT_IN_USAGE_ERROR to "must"
|
Experimentality.Severity.ERROR -> FirErrors.OPT_IN_USAGE_ERROR to "must"
|
||||||
@@ -252,8 +273,8 @@ object FirOptInUsageBaseChecker {
|
|||||||
reporter: DiagnosticReporter
|
reporter: DiagnosticReporter
|
||||||
) {
|
) {
|
||||||
for ((annotationClassId, severity, markerMessage, supertypeName) in experimentalities) {
|
for ((annotationClassId, severity, markerMessage, supertypeName) in experimentalities) {
|
||||||
if (!symbol.fir.isExperimentalityAcceptable(annotationClassId) &&
|
if (!symbol.fir.isExperimentalityAcceptable(annotationClassId, fromSupertype = false) &&
|
||||||
!isExperimentalityAcceptableInContext(annotationClassId, context)
|
!isExperimentalityAcceptableInContext(annotationClassId, context, fromSupertype = false)
|
||||||
) {
|
) {
|
||||||
val (diagnostic, verb) = when (severity) {
|
val (diagnostic, verb) = when (severity) {
|
||||||
Experimentality.Severity.WARNING -> FirErrors.OPT_IN_OVERRIDE to "should"
|
Experimentality.Severity.WARNING -> FirErrors.OPT_IN_OVERRIDE to "should"
|
||||||
@@ -272,7 +293,8 @@ object FirOptInUsageBaseChecker {
|
|||||||
|
|
||||||
private fun isExperimentalityAcceptableInContext(
|
private fun isExperimentalityAcceptableInContext(
|
||||||
annotationClassId: ClassId,
|
annotationClassId: ClassId,
|
||||||
context: CheckerContext
|
context: CheckerContext,
|
||||||
|
fromSupertype: Boolean
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val languageVersionSettings = context.session.languageVersionSettings
|
val languageVersionSettings = context.session.languageVersionSettings
|
||||||
val fqNameAsString = annotationClassId.asFqNameString()
|
val fqNameAsString = annotationClassId.asFqNameString()
|
||||||
@@ -280,15 +302,16 @@ object FirOptInUsageBaseChecker {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for (annotationContainer in context.annotationContainers) {
|
for (annotationContainer in context.annotationContainers) {
|
||||||
if (annotationContainer.isExperimentalityAcceptable(annotationClassId)) {
|
if (annotationContainer.isExperimentalityAcceptable(annotationClassId, fromSupertype)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun FirAnnotationContainer.isExperimentalityAcceptable(annotationClassId: ClassId): Boolean {
|
private fun FirAnnotationContainer.isExperimentalityAcceptable(annotationClassId: ClassId, fromSupertype: Boolean): Boolean {
|
||||||
return getAnnotationByClassId(annotationClassId) != null || isAnnotatedWithOptIn(annotationClassId)
|
return getAnnotationByClassId(annotationClassId) != null || isAnnotatedWithOptIn(annotationClassId) ||
|
||||||
|
fromSupertype && isAnnotatedWithSubclassOptInRequired(annotationClassId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun FirAnnotationContainer.isAnnotatedWithOptIn(annotationClassId: ClassId): Boolean {
|
private fun FirAnnotationContainer.isAnnotatedWithOptIn(annotationClassId: ClassId): Boolean {
|
||||||
@@ -305,6 +328,20 @@ object FirOptInUsageBaseChecker {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun FirAnnotationContainer.isAnnotatedWithSubclassOptInRequired(annotationClassId: ClassId): Boolean {
|
||||||
|
for (annotation in annotations) {
|
||||||
|
val coneType = annotation.annotationTypeRef.coneType as? ConeClassLikeType
|
||||||
|
if (coneType?.lookupTag?.classId != OptInNames.SUBCLASS_OPT_IN_REQUIRED_CLASS_ID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val annotationClass = annotation.findArgumentByName(OptInNames.USE_EXPERIMENTAL_ANNOTATION_CLASS) ?: continue
|
||||||
|
if (annotationClass.extractClassFromArgument()?.classId == annotationClassId) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private val LEVEL = Name.identifier("level")
|
private val LEVEL = Name.identifier("level")
|
||||||
private val MESSAGE = Name.identifier("message")
|
private val MESSAGE = Name.identifier("message")
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -15,6 +15,7 @@ import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
|
|||||||
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.OPT_IN_CAN_ONLY_BE_USED_AS_ANNOTATION
|
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.OPT_IN_CAN_ONLY_BE_USED_AS_ANNOTATION
|
||||||
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.OPT_IN_MARKER_CAN_ONLY_BE_USED_AS_ANNOTATION_OR_ARGUMENT_IN_OPT_IN
|
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.OPT_IN_MARKER_CAN_ONLY_BE_USED_AS_ANNOTATION_OR_ARGUMENT_IN_OPT_IN
|
||||||
import org.jetbrains.kotlin.diagnostics.reportOn
|
import org.jetbrains.kotlin.diagnostics.reportOn
|
||||||
|
import org.jetbrains.kotlin.fir.declarations.FirClass
|
||||||
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
|
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
|
||||||
import org.jetbrains.kotlin.fir.declarations.getAnnotationByClassId
|
import org.jetbrains.kotlin.fir.declarations.getAnnotationByClassId
|
||||||
import org.jetbrains.kotlin.fir.expressions.FirAnnotation
|
import org.jetbrains.kotlin.fir.expressions.FirAnnotation
|
||||||
@@ -55,9 +56,13 @@ object FirOptInUsageTypeRefChecker : FirTypeRefChecker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isSupertypeRef = typeRef in (context.containingDeclarations.lastOrNull() as? FirClass)?.superTypeRefs.orEmpty()
|
||||||
with(FirOptInUsageBaseChecker) {
|
with(FirOptInUsageBaseChecker) {
|
||||||
val experimentalities = symbol.loadExperimentalities(context, fromSetter = false, dispatchReceiverType = null) +
|
val classifierExperimentalities =
|
||||||
loadExperimentalitiesFromConeArguments(context, coneType.typeArguments.toList())
|
if (isSupertypeRef) symbol.loadExperimentalitiesFromSupertype(context)
|
||||||
|
else symbol.loadExperimentalities(context, fromSetter = false, dispatchReceiverType = null)
|
||||||
|
val experimentalities =
|
||||||
|
classifierExperimentalities + loadExperimentalitiesFromConeArguments(context, coneType.typeArguments.toList())
|
||||||
reportNotAcceptedExperimentalities(experimentalities, typeRef, context, reporter)
|
reportNotAcceptedExperimentalities(experimentalities, typeRef, context, reporter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
@RequiresOptIn
|
|
||||||
annotation class Marker
|
|
||||||
|
|
||||||
@SubclassOptInRequired(Marker::class)
|
|
||||||
open class Base
|
|
||||||
|
|
||||||
@Marker
|
|
||||||
class DerivedFirst : Base()
|
|
||||||
|
|
||||||
@OptIn(Marker::class)
|
|
||||||
class DerivedSecond : Base()
|
|
||||||
|
|
||||||
@SubclassOptInRequired(Marker::class)
|
|
||||||
open class DerivedThird : Base()
|
|
||||||
|
|
||||||
open class DerivedFourth : Base()
|
|
||||||
|
|
||||||
class GrandDerivedThird : DerivedThird()
|
|
||||||
|
|
||||||
// Question: should we have an error also here?
|
|
||||||
class GrandDerivedFourth : DerivedFourth()
|
|
||||||
|
|
||||||
@Marker
|
|
||||||
open class Marked
|
|
||||||
|
|
||||||
@SubclassOptInRequired(Marker::class)
|
|
||||||
class DerivedMarked : <!OPT_IN_USAGE_ERROR!>Marked<!>()
|
|
||||||
|
|
||||||
fun test() {
|
|
||||||
val b = Base()
|
|
||||||
val d1 = <!OPT_IN_USAGE_ERROR!>DerivedFirst<!>()
|
|
||||||
val d2 = DerivedSecond()
|
|
||||||
val d3 = DerivedThird()
|
|
||||||
val d4 = DerivedFourth()
|
|
||||||
}
|
|
||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
// FIR_IDENTICAL
|
||||||
@RequiresOptIn
|
@RequiresOptIn
|
||||||
annotation class Marker
|
annotation class Marker
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user