mirror of
https://github.com/irmen/prog8.git
synced 2025-11-01 22:16:16 +00:00
fix many ptr deref errors
This commit is contained in:
@@ -102,7 +102,7 @@ private fun builtinSizeof(args: List<Expression>, position: Position, program: P
|
||||
if(args[0] is NumericLiteral)
|
||||
return NumericLiteral.optimalInteger(program.memsizer.memorySize(dt.getOrUndef(), null), position)
|
||||
|
||||
val target = (args[0] as IdentifierReference).targetStatement(program)
|
||||
val target = (args[0] as IdentifierReference).targetStatement()
|
||||
?: throw CannotEvaluateException("sizeof", "no target")
|
||||
|
||||
return when {
|
||||
|
||||
@@ -81,7 +81,7 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
|
||||
checkLongType(identifier)
|
||||
val stmt = identifier.targetStatement(program)
|
||||
val stmt = identifier.targetStatement(program.builtinFunctions)
|
||||
if(stmt==null) {
|
||||
if(identifier.parent is ArrayIndexedExpression) {
|
||||
// might be a pointer dereference chain
|
||||
@@ -106,7 +106,7 @@ internal class AstChecker(private val program: Program,
|
||||
|
||||
if(identifier.nameInSource.size>1) {
|
||||
val lookupModule = identifier.definingScope.lookup(identifier.nameInSource.take(1))
|
||||
if(lookupModule is VarDecl) {
|
||||
if(lookupModule is VarDecl && !lookupModule.datatype.isPointer) {
|
||||
errors.err("ambiguous symbol name, block name expected but found variable", identifier.position)
|
||||
}
|
||||
}
|
||||
@@ -340,7 +340,7 @@ internal class AstChecker(private val program: Program,
|
||||
is InlineAssembly,
|
||||
is IStatementContainer -> true
|
||||
is Assignment -> {
|
||||
val target = statement.target.identifier!!.targetStatement(program)
|
||||
val target = statement.target.identifier!!.targetStatement()
|
||||
target === statement.previousSibling() // an initializer assignment is okay
|
||||
}
|
||||
else -> false
|
||||
@@ -378,7 +378,7 @@ internal class AstChecker(private val program: Program,
|
||||
count++
|
||||
}
|
||||
override fun visit(jump: Jump) {
|
||||
val jumpTarget = (jump.target as? IdentifierReference)?.targetStatement(program)
|
||||
val jumpTarget = (jump.target as? IdentifierReference)?.targetStatement()
|
||||
if(jumpTarget!=null) {
|
||||
val sub = jump.definingSubroutine
|
||||
val targetSub = jumpTarget as? Subroutine ?: jumpTarget.definingSubroutine
|
||||
@@ -613,8 +613,8 @@ internal class AstChecker(private val program: Program,
|
||||
|
||||
val ident = repeatLoop.iterations as? IdentifierReference
|
||||
if(ident!=null) {
|
||||
val targetVar = ident.targetVarDecl()
|
||||
if(targetVar==null)
|
||||
val target = ident.targetStatement()
|
||||
if(target !is VarDecl && target !is StructFieldRef)
|
||||
errors.err("invalid assignment value", ident.position)
|
||||
}
|
||||
super.visit(repeatLoop)
|
||||
@@ -627,7 +627,7 @@ internal class AstChecker(private val program: Program,
|
||||
if(valueDt.isKnown && !(valueDt isAssignableTo targetDt) && !targetDt.isIterable) {
|
||||
if(!(valueDt issimpletype BaseDataType.STR && targetDt issimpletype BaseDataType.UWORD)) {
|
||||
if(targetDt.isUnknown) {
|
||||
if(assignment.target.identifier?.targetStatement(program)!=null)
|
||||
if(assignment.target.identifier?.targetStatement(program.builtinFunctions)!=null)
|
||||
errors.err("target datatype is unknown", assignment.target.position)
|
||||
// otherwise, another error about missing symbol is already reported.
|
||||
}
|
||||
@@ -734,6 +734,9 @@ internal class AstChecker(private val program: Program,
|
||||
errors.undefined(targetIdentifier.nameInSource, targetIdentifier.position)
|
||||
return
|
||||
}
|
||||
is StructFieldRef -> {
|
||||
// all is well
|
||||
}
|
||||
!is VarDecl -> {
|
||||
errors.err("assignment LHS must be register or variable", assignment.position)
|
||||
return
|
||||
@@ -1297,7 +1300,7 @@ internal class AstChecker(private val program: Program,
|
||||
count++
|
||||
}
|
||||
override fun visit(jump: Jump) {
|
||||
val jumpTarget = (jump.target as? IdentifierReference)?.targetStatement(program)
|
||||
val jumpTarget = (jump.target as? IdentifierReference)?.targetStatement()
|
||||
if(jumpTarget!=null) {
|
||||
val sub = jump.definingSubroutine
|
||||
val targetSub = jumpTarget as? Subroutine ?: jumpTarget.definingSubroutine
|
||||
@@ -1727,7 +1730,7 @@ internal class AstChecker(private val program: Program,
|
||||
if(args[0] is AddressOf)
|
||||
errors.err("can't call this indirectly, just use normal function call syntax", args[0].position)
|
||||
else if(args[0] is IdentifierReference) {
|
||||
val callTarget = (args[0] as IdentifierReference).targetStatement(program)
|
||||
val callTarget = (args[0] as IdentifierReference).targetStatement(program.builtinFunctions)
|
||||
if(callTarget !is VarDecl)
|
||||
errors.err("can't call this indirectly, just use normal function call syntax", args[0].position)
|
||||
}
|
||||
@@ -1841,32 +1844,41 @@ internal class AstChecker(private val program: Program,
|
||||
|
||||
override fun visit(arrayIndexedExpression: ArrayIndexedExpression) {
|
||||
checkLongType(arrayIndexedExpression)
|
||||
val target = arrayIndexedExpression.arrayvar.targetStatement(program)
|
||||
val target = arrayIndexedExpression.arrayvar.targetStatement(program.builtinFunctions)
|
||||
if(target is VarDecl) {
|
||||
if(!target.datatype.isIterable && !target.datatype.isUnsignedWord && !target.datatype.isPointer)
|
||||
errors.err("indexing requires an iterable, address uword, or pointer variable", arrayIndexedExpression.position)
|
||||
if (!target.datatype.isIterable && !target.datatype.isUnsignedWord && !target.datatype.isPointer)
|
||||
errors.err(
|
||||
"indexing requires an iterable, address uword, or pointer variable",
|
||||
arrayIndexedExpression.position
|
||||
)
|
||||
val indexVariable = arrayIndexedExpression.indexer.indexExpr as? IdentifierReference
|
||||
if(indexVariable!=null) {
|
||||
if(indexVariable.targetVarDecl()?.datatype?.isSigned==true) {
|
||||
errors.err("variable array indexing can't be performed with signed variables", indexVariable.position)
|
||||
if (indexVariable != null) {
|
||||
if (indexVariable.targetVarDecl()?.datatype?.isSigned == true) {
|
||||
errors.err(
|
||||
"variable array indexing can't be performed with signed variables",
|
||||
indexVariable.position
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
val arraysize = target.arraysize?.constIndex()
|
||||
val index = arrayIndexedExpression.indexer.constIndex()
|
||||
if(arraysize!=null) {
|
||||
if(index!=null && (index<0 || index>=arraysize))
|
||||
if (arraysize != null) {
|
||||
if (index != null && (index < 0 || index >= arraysize))
|
||||
errors.err("index out of bounds", arrayIndexedExpression.indexer.position)
|
||||
} else if(target.datatype.isString) {
|
||||
if(target.value is StringLiteral) {
|
||||
} else if (target.datatype.isString) {
|
||||
if (target.value is StringLiteral) {
|
||||
// check string lengths for non-memory mapped strings
|
||||
val stringLen = (target.value as StringLiteral).value.length
|
||||
if (index != null && (index < 0 || index >= stringLen))
|
||||
errors.err("index out of bounds", arrayIndexedExpression.indexer.position)
|
||||
}
|
||||
} else if(index!=null && index<0) {
|
||||
} else if (index != null && index < 0) {
|
||||
errors.err("index out of bounds", arrayIndexedExpression.indexer.position)
|
||||
}
|
||||
} else if(target is StructFieldRef) {
|
||||
if(!target.type.isPointer && !target.type.isUnsignedWord)
|
||||
errors.err("cannot array index on this field type", arrayIndexedExpression.indexer.position)
|
||||
} else {
|
||||
val parentExpr = arrayIndexedExpression.parent
|
||||
if(parentExpr is BinaryExpression) {
|
||||
@@ -2036,7 +2048,6 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
|
||||
override fun visit(deref: PtrDereference) {
|
||||
// unfortunately the AST regarding pointer dereferencing is a bit of a mess, and we cannot do precise type checking on elements inside such expressions yet.
|
||||
if(deref.inferType(program).isUnknown)
|
||||
errors.err("unable to determine type of dereferenced pointer expression", deref.position)
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ internal fun Subroutine.hasRtsInAsm(checkOnlyLastInstruction: Boolean): Boolean
|
||||
}
|
||||
|
||||
internal fun IdentifierReference.checkFunctionOrLabelExists(program: Program, statement: Statement, errors: IErrorReporter): Statement? {
|
||||
when (val targetStatement = this.targetStatement(program)) {
|
||||
when (val targetStatement = targetStatement(program.builtinFunctions)) {
|
||||
is Label, is Subroutine, is BuiltinFunctionPlaceholder -> return targetStatement
|
||||
is VarDecl -> {
|
||||
if(statement is Jump) {
|
||||
@@ -227,7 +227,7 @@ internal fun IdentifierReference.checkFunctionOrLabelExists(program: Program, st
|
||||
else
|
||||
errors.err("cannot call that: ${this.nameInSource.joinToString(".")}", this.position)
|
||||
}
|
||||
is Alias, is StructDecl -> {
|
||||
is Alias, is StructDecl, is StructFieldRef -> {
|
||||
return targetStatement
|
||||
}
|
||||
null -> {
|
||||
|
||||
@@ -41,7 +41,7 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
}
|
||||
|
||||
override fun visit(alias: Alias) {
|
||||
if(alias.target.targetStatement(program)==null)
|
||||
if(alias.target.targetStatement(program.builtinFunctions)==null)
|
||||
errors.err("undefined symbol: ${alias.target.nameInSource.joinToString(".") }", alias.target.position)
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
}
|
||||
|
||||
override fun visit(deref: PtrDereference) {
|
||||
val first = deref.identifier.targetStatement(program)
|
||||
val first = deref.identifier.targetStatement()
|
||||
if(first==null)
|
||||
errors.undefined(deref.identifier.nameInSource, deref.identifier.position)
|
||||
|
||||
@@ -206,13 +206,13 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
|
||||
private fun visitFunctionCall(call: IFunctionCall) {
|
||||
if(call.target.nameInSource==listOf("rnd") || call.target.nameInSource==listOf("rndw")) {
|
||||
val target = call.target.targetStatement(program)
|
||||
val target = call.target.targetStatement(program.builtinFunctions)
|
||||
if(target==null) {
|
||||
errors.err("rnd() and rndw() builtin functions have been moved into the math module", call.position)
|
||||
return
|
||||
}
|
||||
}
|
||||
when (val target = call.target.targetStatement(program)) {
|
||||
when (val target = call.target.targetStatement(program.builtinFunctions)) {
|
||||
is Subroutine -> {
|
||||
val expectedNumberOfArgs: Int = target.parameters.size
|
||||
if(call.args.size != expectedNumberOfArgs) {
|
||||
@@ -252,7 +252,7 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
if(target.type!=VarDeclType.VAR || !target.datatype.isUnsignedWord)
|
||||
errors.err("wrong address variable datatype, expected uword", call.target.position)
|
||||
}
|
||||
is Alias, is StructDecl -> {}
|
||||
is Alias, is StructDecl, is StructFieldRef -> {}
|
||||
null -> {}
|
||||
else -> errors.err("cannot call this as a subroutine or function", call.target.position)
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ class AstPreprocessor(val program: Program,
|
||||
}
|
||||
|
||||
override fun after(alias: Alias, parent: Node): Iterable<IAstModification> {
|
||||
val tgt = alias.target.targetStatement(program)
|
||||
val tgt = alias.target.targetStatement(program.builtinFunctions)
|
||||
if(tgt is Block) {
|
||||
errors.err("cannot alias blocks", alias.target.position)
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ _after:
|
||||
if(parent is PtrIndexedDereference || parent.parent is PtrIndexedDereference)
|
||||
return noModifications
|
||||
|
||||
if(identifier.nameInSource.size>1 && identifier.targetStatement(program)==null) {
|
||||
if(identifier.nameInSource.size>1 && identifier.targetStatement()==null) {
|
||||
// the a.b.c.d could be a pointer dereference chain a^^.b^^^.c^^^.d
|
||||
for(i in identifier.nameInSource.size-1 downTo 1) {
|
||||
val symbol = identifier.definingScope.lookup(identifier.nameInSource.take(i)) as? VarDecl
|
||||
|
||||
@@ -130,7 +130,7 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
}
|
||||
|
||||
override fun after(alias: Alias, parent: Node): Iterable<IAstModification> {
|
||||
val target = alias.target.targetStatement(program)
|
||||
val target = alias.target.targetStatement()
|
||||
if(target is Alias) {
|
||||
val newAlias = Alias(alias.alias, target.target, alias.position)
|
||||
return listOf(IAstModification.ReplaceNode(alias, newAlias, parent))
|
||||
@@ -139,11 +139,11 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
}
|
||||
|
||||
override fun after(identifier: IdentifierReference, parent: Node): Iterable<IAstModification> {
|
||||
val target = identifier.targetStatement(program)
|
||||
val target = identifier.targetStatement()
|
||||
|
||||
// don't replace an identifier in an Alias or when the alias points to another alias (that will be resolved first elsewhere)
|
||||
if(target is Alias && parent !is Alias) {
|
||||
if(target.target.targetStatement(program) !is Alias)
|
||||
if(target.target.targetStatement() !is Alias)
|
||||
return listOf(IAstModification.ReplaceNode(identifier, target.target.copy(position = identifier.position), parent))
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
val rightIndex = expr.right as? ArrayIndexedExpression
|
||||
if (leftIdent != null && rightIndex != null) {
|
||||
// maybe recombine IDENTIFIER . ARRAY[IDX] --> COMBINEDIDENTIFIER[IDX]
|
||||
val leftTarget = leftIdent.targetStatement(null)
|
||||
val leftTarget = leftIdent.targetStatement()
|
||||
if(leftTarget==null || leftTarget !is StructDecl) {
|
||||
val combinedName = leftIdent.nameInSource + rightIndex.arrayvar.nameInSource
|
||||
val combined = IdentifierReference(combinedName, leftIdent.position)
|
||||
|
||||
@@ -121,9 +121,9 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
|
||||
throw FatalAstException("unknown dt")
|
||||
}
|
||||
val start = transform(deref.identifier)
|
||||
val deref = PtPointerDeref(type, deref.chain, deref.field,deref.position)
|
||||
deref.add(start)
|
||||
return deref
|
||||
val result = PtPointerDeref(type, deref.chain, deref.field,deref.position)
|
||||
result.add(start)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun transform(ifExpr: IfExpression): PtIfExpression {
|
||||
|
||||
@@ -282,7 +282,7 @@ class TypecastsAdder(val program: Program, val options: CompilationOptions, val
|
||||
// see if a typecast is needed to convert the arguments into the required parameter type
|
||||
|
||||
val modifications = mutableListOf<IAstModification>()
|
||||
val paramsPossibleDatatypes = when(val sub = call.target.targetStatement(program)) {
|
||||
val paramsPossibleDatatypes = when(val sub = call.target.targetStatement(program.builtinFunctions)) {
|
||||
is BuiltinFunctionPlaceholder -> {
|
||||
BuiltinFunctions.getValue(sub.name).parameters.map {
|
||||
it.possibleDatatypes.map { dt ->
|
||||
@@ -465,7 +465,7 @@ class TypecastsAdder(val program: Program, val options: CompilationOptions, val
|
||||
for((index, elt) in array.value.withIndex()) {
|
||||
if (elt is IdentifierReference) {
|
||||
val eltType = elt.inferType(program)
|
||||
val tgt = elt.targetStatement(program)
|
||||
val tgt = elt.targetStatement()
|
||||
if(eltType.isIterable || tgt is Subroutine || tgt is Label || tgt is Block) {
|
||||
val addressof = AddressOf(elt, null, null, false, elt.position)
|
||||
addressof.linkParents(array)
|
||||
|
||||
@@ -112,13 +112,13 @@ internal class VerifyFunctionArgTypes(val program: Program, val options: Compila
|
||||
if(firstUnknownDt>=0) {
|
||||
// if an uword is expected but a pointer is provided, that is okay without a cast
|
||||
val identifier = call.args[0] as? IdentifierReference
|
||||
return if(identifier==null || identifier.targetStatement(program)!=null)
|
||||
return if(identifier==null || identifier.targetStatement(program.builtinFunctions)!=null)
|
||||
Pair("argument ${firstUnknownDt + 1} invalid argument type", call.args[firstUnknownDt].position)
|
||||
else
|
||||
null
|
||||
}
|
||||
val argtypes = argITypes.map { it.getOrUndef() }
|
||||
val target = call.target.targetStatement(program)
|
||||
val target = call.target.targetStatement(program.builtinFunctions)
|
||||
if (target is Subroutine) {
|
||||
val consideredParamTypes: List<DataType> = target.parameters.map { it.type }
|
||||
if(argtypes.size != consideredParamTypes.size)
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.kotest.matchers.collections.shouldContain
|
||||
import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual
|
||||
import io.kotest.matchers.maps.shouldNotContainKey
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import prog8.ast.Program
|
||||
import prog8.ast.statements.Block
|
||||
@@ -312,4 +313,20 @@ xyz {
|
||||
val blocks2 = result2.codegenAst!!.allBlocks().toList()
|
||||
blocks2.any { it.name=="xyz" } shouldBe false
|
||||
}
|
||||
|
||||
test("symbol lookup of pointer fields should mark variable as used in callgraph") {
|
||||
val src = """
|
||||
main {
|
||||
struct List {
|
||||
^^uword s
|
||||
ubyte n
|
||||
}
|
||||
sub start() {
|
||||
^^List l1 = List()
|
||||
l1.s^^ = 2
|
||||
}
|
||||
}"""
|
||||
|
||||
compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestCompilerOnImportsAndIncludes: FunSpec({
|
||||
str0.definingScope.name shouldBe "main"
|
||||
|
||||
val id1 = (args[1] as AddressOf).identifier!!
|
||||
val lbl1 = id1.targetStatement(program) as Label
|
||||
val lbl1 = id1.targetStatement() as Label
|
||||
lbl1.name shouldBe "foo_bar"
|
||||
lbl1.definingScope.name shouldBe "main"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import io.kotest.matchers.types.instanceOf
|
||||
import prog8.ast.expressions.PtrDereference
|
||||
import prog8.ast.statements.Assignment
|
||||
import prog8.ast.statements.VarDecl
|
||||
import prog8.code.ast.PtAssignment
|
||||
@@ -363,4 +364,61 @@ main {
|
||||
st[4] shouldBe instanceOf<Assignment>()
|
||||
st[5] shouldBe instanceOf<Assignment>()
|
||||
}
|
||||
|
||||
test("indexing pointers with index 0 is just a direct pointer dereference") {
|
||||
val src="""
|
||||
main {
|
||||
struct List {
|
||||
^^uword s
|
||||
ubyte n
|
||||
}
|
||||
sub start() {
|
||||
^^List l1 = List()
|
||||
cx16.r0 = l1.s[0]
|
||||
l1.s[0] = 4242
|
||||
cx16.r1 = l1.s^^
|
||||
|
||||
^^word @shared wptr
|
||||
cx16.r0s = wptr[0]
|
||||
cx16.r1s = wptr^^
|
||||
wptr[0] = 4242
|
||||
}
|
||||
|
||||
}"""
|
||||
|
||||
val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = false)!!
|
||||
val st = result.compilerAst.entrypoint.statements
|
||||
st.size shouldBe 10
|
||||
val dr0 = (st[2] as Assignment).value as PtrDereference
|
||||
val dr1 = (st[3] as Assignment).target.pointerDereference!!
|
||||
val dr2 = (st[4] as Assignment).value as PtrDereference
|
||||
|
||||
val dr3 = (st[6] as Assignment).value as PtrDereference
|
||||
val dr4 = (st[7] as Assignment).value as PtrDereference
|
||||
val dr5 = (st[8] as Assignment).target.pointerDereference!!
|
||||
|
||||
dr0.identifier.nameInSource shouldBe listOf("l1", "s")
|
||||
dr0.chain.size shouldBe 0
|
||||
dr0.field shouldBe null
|
||||
|
||||
dr1.identifier.nameInSource shouldBe listOf("l1", "s")
|
||||
dr1.chain.size shouldBe 0
|
||||
dr1.field shouldBe null
|
||||
|
||||
dr2.identifier.nameInSource shouldBe listOf("l1", "s")
|
||||
dr2.chain.size shouldBe 0
|
||||
dr2.field shouldBe null
|
||||
|
||||
dr3.identifier.nameInSource shouldBe listOf("wptr")
|
||||
dr3.chain.size shouldBe 0
|
||||
dr3.field shouldBe null
|
||||
|
||||
dr4.identifier.nameInSource shouldBe listOf("wptr")
|
||||
dr4.chain.size shouldBe 0
|
||||
dr4.field shouldBe null
|
||||
|
||||
dr5.identifier.nameInSource shouldBe listOf("wptr")
|
||||
dr5.chain.size shouldBe 0
|
||||
dr5.field shouldBe null
|
||||
}
|
||||
})
|
||||
@@ -63,11 +63,11 @@ class TestIdentifierRef: FunSpec({
|
||||
val mainref = ((stmts[1] as Assignment).value as AddressOf).identifier!!
|
||||
wwref.nameInSource shouldBe listOf("ww")
|
||||
wwref.wasStringLiteral() shouldBe false
|
||||
wwref.targetStatement(program) shouldBe instanceOf<VarDecl>()
|
||||
wwref.targetStatement() shouldBe instanceOf<VarDecl>()
|
||||
wwref.targetVarDecl()!!.name shouldBe "ww"
|
||||
wwref.targetVarDecl()!!.parent shouldBe instanceOf<Block>()
|
||||
mainref.nameInSource shouldBe listOf("main")
|
||||
mainref.wasStringLiteral() shouldBe false
|
||||
mainref.targetStatement(program) shouldBe instanceOf<Block>()
|
||||
mainref.targetStatement() shouldBe instanceOf<Block>()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user