fix $73 reg #74 val assignment bug fix. Also, cosmetics on syntax highlighting
This commit is contained in:
parent
e765784170
commit
603023962e
@ -2644,7 +2644,8 @@ class Compiler(
|
||||
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
||||
val initStmt = statement(nameToken.pos) { scp ->
|
||||
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
|
||||
scp.addOrUpdateItem(storageName, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||
// Preserve mutability of declaration: do NOT use addOrUpdateItem here, as it creates mutable records
|
||||
scp.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||
ObjVoid
|
||||
}
|
||||
cls.instanceInitializers += initStmt
|
||||
@ -2652,7 +2653,8 @@ class Compiler(
|
||||
} else {
|
||||
// We are in instance scope already: perform initialization immediately
|
||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
||||
context.addOrUpdateItem(storageName, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||
// Preserve mutability of declaration: create record with correct mutability
|
||||
context.addItem(storageName, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||
initValue
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -172,6 +172,13 @@ class Script(
|
||||
if( a.compareTo(this, b) != 0 )
|
||||
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"))
|
||||
}
|
||||
// alias used in tests
|
||||
addVoidFn("assertEqual") {
|
||||
val a = requiredArg<Obj>(0)
|
||||
val b = requiredArg<Obj>(1)
|
||||
if( a.compareTo(this, b) != 0 )
|
||||
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"))
|
||||
}
|
||||
addVoidFn("assertNotEquals") {
|
||||
val a = requiredArg<Obj>(0)
|
||||
val b = requiredArg<Obj>(1)
|
||||
|
||||
@ -40,6 +40,8 @@ enum class HighlightKind {
|
||||
Label,
|
||||
Directive,
|
||||
Error,
|
||||
/** Enum constant (both declaration and usage). */
|
||||
EnumConstant,
|
||||
}
|
||||
|
||||
/** A highlighted span: character range and its semantic/lexical kind. */
|
||||
|
||||
@ -159,8 +159,10 @@ class SimpleLyngHighlighter : LyngHighlighter {
|
||||
}
|
||||
if (range.endExclusive > range.start) raw += HighlightSpan(range, k)
|
||||
}
|
||||
// Heuristics: mark enum constants in declaration blocks and on qualified usages Foo.BAR
|
||||
val overridden = applyEnumConstantHeuristics(text, src, tokens, raw)
|
||||
// Adjust single-line comment spans to extend till EOL to compensate for lexer offset/length quirks
|
||||
val adjusted = extendSingleLineCommentsToEol(text, raw)
|
||||
val adjusted = extendSingleLineCommentsToEol(text, overridden)
|
||||
// Spans are in order; merge adjacent of the same kind for compactness
|
||||
return mergeAdjacent(adjusted)
|
||||
}
|
||||
@ -202,3 +204,81 @@ private fun extendSingleLineCommentsToEol(
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect enum constants both in enum declarations and in qualified usages (TypeName.CONST)
|
||||
* and override corresponding identifier spans with EnumConstant kind.
|
||||
*/
|
||||
private fun applyEnumConstantHeuristics(
|
||||
text: String,
|
||||
src: Source,
|
||||
tokens: List<net.sergeych.lyng.Token>,
|
||||
spans: MutableList<HighlightSpan>
|
||||
): MutableList<HighlightSpan> {
|
||||
if (tokens.isEmpty() || spans.isEmpty()) return spans
|
||||
|
||||
// Build quick lookup from range start to span index for identifiers only
|
||||
val byStart = HashMap<Int, Int>(spans.size * 2)
|
||||
for (i in spans.indices) {
|
||||
val s = spans[i]
|
||||
if (s.kind == HighlightKind.Identifier) byStart[s.range.start] = i
|
||||
}
|
||||
|
||||
fun overrideIdAtToken(idx: Int) {
|
||||
val t = tokens[idx]
|
||||
if (t.type != Type.ID) return
|
||||
val start = src.offsetOf(t.pos)
|
||||
val spanIndex = byStart[start] ?: return
|
||||
spans[spanIndex] = HighlightSpan(spans[spanIndex].range, HighlightKind.EnumConstant)
|
||||
}
|
||||
|
||||
// 1) Enum declarations: enum Name { CONST1, CONST2 }
|
||||
var i = 0
|
||||
while (i < tokens.size) {
|
||||
val t = tokens[i]
|
||||
if (t.type == Type.ID && t.value.equals("enum", ignoreCase = true)) {
|
||||
// expect: ID(enum) ID(name) LBRACE (ID (COMMA ID)* ) RBRACE
|
||||
var j = i + 1
|
||||
// skip optional whitespace/newlines tokens are separate types, so we just check IDs and braces
|
||||
if (j < tokens.size && tokens[j].type == Type.ID) j++ else { i++; continue }
|
||||
if (j < tokens.size && tokens[j].type == Type.LBRACE) {
|
||||
j++
|
||||
while (j < tokens.size) {
|
||||
val tk = tokens[j]
|
||||
if (tk.type == Type.RBRACE) { j++; break }
|
||||
if (tk.type == Type.ID) {
|
||||
// enum entry declaration
|
||||
overrideIdAtToken(j)
|
||||
j++
|
||||
// optional comma
|
||||
if (j < tokens.size && tokens[j].type == Type.COMMA) { j++ ; continue }
|
||||
continue
|
||||
}
|
||||
// Any unexpected token ends enum entries scan
|
||||
break
|
||||
}
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// 2) Qualified usages: Something.CONST where CONST is ALL_UPPERCASE (with digits/underscores)
|
||||
fun isAllUpperCase(name: String): Boolean = name.isNotEmpty() && name.all { it == '_' || it.isDigit() || (it.isLetter() && it.isUpperCase()) }
|
||||
i = 1
|
||||
while (i + 0 < tokens.size) {
|
||||
val dotTok = tokens[i]
|
||||
if (dotTok.type == Type.DOT && i + 1 < tokens.size) {
|
||||
val next = tokens[i + 1]
|
||||
if (next.type == Type.ID && isAllUpperCase(next.value)) {
|
||||
overrideIdAtToken(i + 1)
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return spans
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.canAccessMember
|
||||
@ -41,11 +43,17 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
if (!canAccessMember(it.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
scope,
|
||||
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
return it
|
||||
}
|
||||
// Try MI-mangled lookup along linearization (C3 MRO): ClassName::name
|
||||
val cls = objClass
|
||||
|
||||
// self first, then parents
|
||||
fun findMangled(): ObjRecord? {
|
||||
// self
|
||||
@ -66,7 +74,12 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${declaring?.className ?: "?"})"))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
scope,
|
||||
"can't access field $name (declared in ${declaring?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
return rec
|
||||
}
|
||||
// Fall back to methods/properties on class
|
||||
@ -83,7 +96,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
if (!canAccessMember(f.visibility, decl, caller))
|
||||
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise()
|
||||
ObjIllegalAssignmentException(
|
||||
scope,
|
||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
||||
).raise()
|
||||
}
|
||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
if (f.value.assign(scope, newValue) == null)
|
||||
@ -99,6 +115,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val rec = findMangled()
|
||||
if (rec != null) {
|
||||
val declaring = when {
|
||||
@ -109,7 +126,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 // do not default to objClass for outsiders
|
||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
||||
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${declaring?.className ?: "?"})").raise()
|
||||
ObjIllegalAssignmentException(
|
||||
scope,
|
||||
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
|
||||
).raise()
|
||||
}
|
||||
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
if (rec.value.assign(scope, newValue) == null)
|
||||
@ -119,14 +139,21 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
super.writeField(scope, name, newValue)
|
||||
}
|
||||
|
||||
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
|
||||
onNotFoundResult: (()->Obj?)?): Obj =
|
||||
override suspend fun invokeInstanceMethod(
|
||||
scope: Scope, name: String, args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
): Obj =
|
||||
instanceScope[name]?.let { rec ->
|
||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 ?: if (scope.thisObj === this) objClass else null
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
scope,
|
||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
// execute with lexical class context propagated to declaring class
|
||||
val saved = instanceScope.currentClassCtx
|
||||
instanceScope.currentClassCtx = decl
|
||||
@ -134,7 +161,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
rec.value.invoke(
|
||||
instanceScope,
|
||||
this,
|
||||
args)
|
||||
args
|
||||
)
|
||||
} finally {
|
||||
instanceScope.currentClassCtx = saved
|
||||
}
|
||||
@ -146,7 +174,12 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||
val caller = caller0 ?: if (scope.thisObj === this) objClass else null
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
scope,
|
||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
val saved = instanceScope.currentClassCtx
|
||||
instanceScope.currentClassCtx = decl
|
||||
try {
|
||||
@ -181,20 +214,45 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
serializeStateVars(scope, encoder)
|
||||
}
|
||||
|
||||
protected val instanceVars: Map<String, ObjRecord> by lazy {
|
||||
override suspend fun toJson(scope: Scope): JsonElement {
|
||||
// Call the class-provided map serializer:
|
||||
val custom = invokeInstanceMethod(scope, "toJsonObject", Arguments.EMPTY, { ObjVoid })
|
||||
if (custom != ObjVoid) {
|
||||
// class json serializer returned something, so use it:
|
||||
return custom.toJson(scope)
|
||||
}
|
||||
// no class serializer, serialize from constructor
|
||||
val result = mutableMapOf<String, JsonElement>()
|
||||
val meta = objClass.constructorMeta
|
||||
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
|
||||
for (entry in meta.params)
|
||||
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
|
||||
for (i in serializingVars)
|
||||
result[i.key] = i.value.value.toJson(scope)
|
||||
return JsonObject(result)
|
||||
}
|
||||
|
||||
val instanceVars: Map<String, ObjRecord> by lazy {
|
||||
instanceScope.objects.filter { it.value.type.serializable }
|
||||
}
|
||||
|
||||
protected suspend fun serializeStateVars(scope: Scope,encoder: LynonEncoder) {
|
||||
val serializingVars: Map<String, ObjRecord> by lazy {
|
||||
instanceScope.objects.filter {
|
||||
it.value.type.serializable &&
|
||||
it.value.type == ObjRecord.Type.Field &&
|
||||
it.value.isMutable }
|
||||
}
|
||||
|
||||
protected suspend fun serializeStateVars(scope: Scope, encoder: LynonEncoder) {
|
||||
val vars = instanceVars.values.map { it.value }
|
||||
if( vars.isNotEmpty()) {
|
||||
if (vars.isNotEmpty()) {
|
||||
encoder.encodeAnyList(scope, vars)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
|
||||
val localVars = instanceVars.values.toList()
|
||||
if( localVars.isNotEmpty() ) {
|
||||
if (localVars.isNotEmpty()) {
|
||||
val vars = decoder.decodeAnyList(scope)
|
||||
if (vars.size > instanceVars.size)
|
||||
scope.raiseIllegalArgument("serialized vars has bigger size than instance vars")
|
||||
@ -250,7 +308,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
scope,
|
||||
"can't access field $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
return rec
|
||||
}
|
||||
}
|
||||
@ -261,7 +324,15 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
if (!canAccessMember(r.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||
return when (val value = r.value) {
|
||||
is net.sergeych.lyng.Statement -> ObjRecord(value.execute(instance.instanceScope.createChildScope(scope.pos, newThisObj = instance)), r.isMutable)
|
||||
is net.sergeych.lyng.Statement -> ObjRecord(
|
||||
value.execute(
|
||||
instance.instanceScope.createChildScope(
|
||||
scope.pos,
|
||||
newThisObj = instance
|
||||
)
|
||||
), r.isMutable
|
||||
)
|
||||
|
||||
else -> r
|
||||
}
|
||||
}
|
||||
@ -273,7 +344,10 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
val decl = f.declaringClass ?: startClass
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(f.visibility, decl, caller))
|
||||
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
|
||||
ObjIllegalAssignmentException(
|
||||
scope,
|
||||
"can't assign to field $name (declared in ${decl.className})"
|
||||
).raise()
|
||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||
return
|
||||
@ -284,7 +358,10 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(f.visibility, decl, caller))
|
||||
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise()
|
||||
ObjIllegalAssignmentException(
|
||||
scope,
|
||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
||||
).raise()
|
||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||
return
|
||||
@ -299,7 +376,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
if (r.value.assign(scope, newValue) == null) r.value = newValue
|
||||
}
|
||||
|
||||
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: (() -> Obj?)?): Obj {
|
||||
override suspend fun invokeInstanceMethod(
|
||||
scope: Scope,
|
||||
name: String,
|
||||
args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
): Obj {
|
||||
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
|
||||
memberFromAncestor(name)?.let { rec ->
|
||||
val decl = rec.declaringClass ?: startClass
|
||||
@ -320,7 +402,12 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
|
||||
scope.raiseError(
|
||||
ObjAccessException(
|
||||
scope,
|
||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
val saved = instance.instanceScope.currentClassCtx
|
||||
instance.instanceScope.currentClassCtx = decl
|
||||
try {
|
||||
|
||||
@ -3815,4 +3815,47 @@ class ScriptTest {
|
||||
assertEquals(JSTest1("bar", 1, true), x.decodeSerializable<JSTest1>())
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun testInstanceVars() = runTest {
|
||||
// val x = eval("""
|
||||
// class T(x,y)
|
||||
// T(1, 2)
|
||||
// """.trimIndent()) as ObjInstance
|
||||
// println(x.serializingVars.map { "${it.key}=${it.value.value}"})
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun memberValCantBeAssigned() = runTest {
|
||||
eval("""
|
||||
class Point(foo,bar) {
|
||||
val t = 42
|
||||
}
|
||||
val p = Point(1,2)
|
||||
// val should not be assignable:
|
||||
assertThrows { p = Point(3,4) }
|
||||
|
||||
// val field must be readonly:
|
||||
assertThrows { p.t = "bad" }
|
||||
|
||||
// and the value should not be changed
|
||||
assertEqual(42, p.t)
|
||||
""")
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun testClassToJson() = runTest {
|
||||
// val x = eval("""
|
||||
// import lyng.serialization
|
||||
// class Point(foo,bar) {
|
||||
// val t = 42
|
||||
// }
|
||||
// val p = Point(1,2)
|
||||
// p.t = 121
|
||||
// println(Point(10,"bar").toJsonString())
|
||||
// Lynon.encode(Point(1,2))
|
||||
// """.trimIndent())
|
||||
// println((x as ObjBitBuffer).bitArray.asUByteArray().toDump())
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@ -154,6 +154,7 @@ fun ensureLyngHighlightStyles() {
|
||||
.hl-class { color: #5a32a3; font-weight: 600; }
|
||||
.hl-val { color: #1b7f5a; }
|
||||
.hl-var { color: #1b7f5a; text-decoration: underline dotted currentColor; }
|
||||
.hl-enumc { color: #b08800; font-weight: 600; }
|
||||
.hl-param { color: #0969da; font-style: italic; }
|
||||
.hl-num { color: #005cc5; }
|
||||
.hl-str { color: #032f62; }
|
||||
@ -177,6 +178,7 @@ fun ensureLyngHighlightStyles() {
|
||||
[data-bs-theme="dark"] .hl-class{ color: #d2a8ff; font-weight: 700; }
|
||||
[data-bs-theme="dark"] .hl-val { color: #7ee787; }
|
||||
[data-bs-theme="dark"] .hl-var { color: #7ee787; text-decoration: underline dotted currentColor; }
|
||||
[data-bs-theme="dark"] .hl-enumc{ color: #f2cc60; font-weight: 700; }
|
||||
[data-bs-theme="dark"] .hl-param{ color: #a5d6ff; font-style: italic; }
|
||||
[data-bs-theme="dark"] .hl-num { color: #79c0ff; }
|
||||
[data-bs-theme="dark"] .hl-str,
|
||||
@ -677,6 +679,7 @@ private fun cssClassForKind(kind: HighlightKind): String = when (kind) {
|
||||
HighlightKind.Label -> "hl-lbl"
|
||||
HighlightKind.Directive -> "hl-dir"
|
||||
HighlightKind.Error -> "hl-err"
|
||||
HighlightKind.EnumConstant -> "hl-enumc"
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user