mirror of
https://github.com/cincheo/jsweet.git
synced 2025-12-15 07:19:22 +00:00
added more tests and sound constraints on mixins
This commit is contained in:
parent
7cee074faa
commit
3c43b3301f
@ -27,20 +27,20 @@ import java.lang.annotation.Target;
|
|||||||
* final generated code (rather than the Java name).
|
* final generated code (rather than the Java name).
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* This needs to be used when the name of an element is not a valid Java
|
* It can be used when the name of an element is not a valid Java identifier. By
|
||||||
* identifier. By convention, JSweet implements a built-in convention to save
|
* convention, JSweet implements a built-in convention to save the use of @Name
|
||||||
* the use of @Name annotations:
|
* annotations:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Convention: <code>Keyword</code> in Java transpiles to
|
* <li>Convention: <code>Keyword</code> in Java transpiles to
|
||||||
* <code>keyword</code>, when keyword is a Java keyword (such as catch, finaly,
|
* <code>keyword</code>, when keyword is a Java keyword (such as catch, finally,
|
||||||
* int, long, and so forth)</li>
|
* int, long, and so forth)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Renaud Pawlak
|
* @author Renaud Pawlak
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE })
|
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE })
|
||||||
@Documented
|
@Documented
|
||||||
public @interface Name {
|
public @interface Name {
|
||||||
|
|
||||||
|
|||||||
@ -27,20 +27,20 @@ import java.lang.annotation.Target;
|
|||||||
* final generated code (rather than the Java name).
|
* final generated code (rather than the Java name).
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* This needs to be used when the name of an element is not a valid Java
|
* It can be used when the name of an element is not a valid Java identifier. By
|
||||||
* identifier. By convention, JSweet implements a built-in convention to save
|
* convention, JSweet implements a built-in convention to save the use of @Name
|
||||||
* the use of @Name annotations:
|
* annotations:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Convention: <code>Keyword</code> in Java transpiles to
|
* <li>Convention: <code>Keyword</code> in Java transpiles to
|
||||||
* <code>keyword</code>, when keyword is a Java keyword (such as catch, finaly,
|
* <code>keyword</code>, when keyword is a Java keyword (such as catch, finally,
|
||||||
* int, long, and so forth)</li>
|
* int, long, and so forth)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Renaud Pawlak
|
* @author Renaud Pawlak
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE })
|
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PACKAGE })
|
||||||
@Documented
|
@Documented
|
||||||
public @interface Name {
|
public @interface Name {
|
||||||
|
|
||||||
|
|||||||
@ -302,8 +302,15 @@ public enum JSweetProblem {
|
|||||||
/**
|
/**
|
||||||
* Raised when a template literal is not used properly.
|
* Raised when a template literal is not used properly.
|
||||||
*/
|
*/
|
||||||
MISUSED_TEMPLATE_MACRO(Severity.ERROR);
|
MISUSED_TEMPLATE_MACRO(Severity.ERROR),
|
||||||
|
/**
|
||||||
|
* Raised when a mixin does not have the same name as its target.
|
||||||
|
*/
|
||||||
|
WRONG_MIXIN_NAME(Severity.ERROR),
|
||||||
|
/**
|
||||||
|
* Raised when a mixin targets itself.
|
||||||
|
*/
|
||||||
|
SELF_MIXIN_TARGET(Severity.ERROR);
|
||||||
|
|
||||||
private Severity severity;
|
private Severity severity;
|
||||||
|
|
||||||
@ -334,11 +341,15 @@ public enum JSweetProblem {
|
|||||||
case MAPPED_TSC_ERROR:
|
case MAPPED_TSC_ERROR:
|
||||||
return String.format("%s", params);
|
return String.format("%s", params);
|
||||||
case NODE_CANNOT_START:
|
case NODE_CANNOT_START:
|
||||||
return String.format("cannot find Node.js: install first and make sure that the 'node' command is in your execution path", params);
|
return String.format(
|
||||||
|
"cannot find Node.js: install first and make sure that the 'node' command is in your execution path",
|
||||||
|
params);
|
||||||
case NODE_OBSOLETE_VERSION:
|
case NODE_OBSOLETE_VERSION:
|
||||||
return String.format("Node.js should be upgraded: %s < %s (recommended)", params);
|
return String.format("Node.js should be upgraded: %s < %s (recommended)", params);
|
||||||
case TSC_CANNOT_START:
|
case TSC_CANNOT_START:
|
||||||
return String.format("cannot find TypeScript compiler: install first and make sure that the 'tsc' command is in your execution path", params);
|
return String.format(
|
||||||
|
"cannot find TypeScript compiler: install first and make sure that the 'tsc' command is in your execution path",
|
||||||
|
params);
|
||||||
case JDK_TYPE:
|
case JDK_TYPE:
|
||||||
return String.format("invalid access to JDK type '%s' from JSweet", params);
|
return String.format("invalid access to JDK type '%s' from JSweet", params);
|
||||||
case JDK_METHOD:
|
case JDK_METHOD:
|
||||||
@ -360,12 +371,16 @@ public enum JSweetProblem {
|
|||||||
case UNINITIALIZED_FIELD:
|
case UNINITIALIZED_FIELD:
|
||||||
return String.format("field '%s' is not optional (see @Optional) but has not been initialized", params);
|
return String.format("field '%s' is not optional (see @Optional) but has not been initialized", params);
|
||||||
case USELESS_OPTIONAL_ANNOTATION:
|
case USELESS_OPTIONAL_ANNOTATION:
|
||||||
return String.format("useless @Optional field %s (fields are optional by default in classes, use @Interface to define %s as an interface)", params);
|
return String.format(
|
||||||
case JS_KEYWORD_CONFLICT:
|
"useless @Optional field %s (fields are optional by default in classes, use @Interface to define %s as an interface)",
|
||||||
return String.format("local variable name '%s' is not allowed and is automatically generated to '" + JSweetConfig.JS_KEYWORD_PREFIX + "%s'",
|
|
||||||
params);
|
params);
|
||||||
|
case JS_KEYWORD_CONFLICT:
|
||||||
|
return String.format("local variable name '%s' is not allowed and is automatically generated to '"
|
||||||
|
+ JSweetConfig.JS_KEYWORD_PREFIX + "%s'", params);
|
||||||
case INVALID_METHOD_BODY_IN_INTERFACE:
|
case INVALID_METHOD_BODY_IN_INTERFACE:
|
||||||
return String.format("method '%s' cannot define a body in interface '%s' (try 'abstract' or 'native' modifiers)", params);
|
return String.format(
|
||||||
|
"method '%s' cannot define a body in interface '%s' (try 'abstract' or 'native' modifiers)",
|
||||||
|
params);
|
||||||
case INVALID_PRIVATE_IN_INTERFACE:
|
case INVALID_PRIVATE_IN_INTERFACE:
|
||||||
return String.format("member '%s' cannot be private in interface '%s'", params);
|
return String.format("member '%s' cannot be private in interface '%s'", params);
|
||||||
case INVALID_FIELD_INITIALIZER_IN_INTERFACE:
|
case INVALID_FIELD_INITIALIZER_IN_INTERFACE:
|
||||||
@ -421,16 +436,22 @@ public enum JSweetProblem {
|
|||||||
case WILDCARD_IMPORT:
|
case WILDCARD_IMPORT:
|
||||||
return String.format("imports cannot use * wildcards: please import a specific element", params);
|
return String.format("imports cannot use * wildcards: please import a specific element", params);
|
||||||
case ENCLOSED_ROOT_PACKAGES:
|
case ENCLOSED_ROOT_PACKAGES:
|
||||||
return String.format("invalid package hierarchy: @Root package '%s' cannot be enclosed in @Root package '%s'", params);
|
return String.format(
|
||||||
|
"invalid package hierarchy: @Root package '%s' cannot be enclosed in @Root package '%s'", params);
|
||||||
case MULTIPLE_ROOT_PACKAGES_NOT_ALLOWED_WITH_MODULES:
|
case MULTIPLE_ROOT_PACKAGES_NOT_ALLOWED_WITH_MODULES:
|
||||||
return String.format("multipe @Root packages (including the default 'null' package) are not allowed when using modules, found packages: %s",
|
return String.format(
|
||||||
|
"multipe @Root packages (including the default 'null' package) are not allowed when using modules, found packages: %s",
|
||||||
params);
|
params);
|
||||||
case CLASS_OUT_OF_ROOT_PACKAGE_SCOPE:
|
case CLASS_OUT_OF_ROOT_PACKAGE_SCOPE:
|
||||||
return String.format("invalid package hierarchy: type '%s' is declared in a parent of @Root package '%s'", params);
|
return String.format("invalid package hierarchy: type '%s' is declared in a parent of @Root package '%s'",
|
||||||
|
params);
|
||||||
case WRONG_USE_OF_AMBIENT:
|
case WRONG_USE_OF_AMBIENT:
|
||||||
return String.format("wrong use of @Ambient on '%s': only types and globals can be declared as ambients", params);
|
return String.format("wrong use of @Ambient on '%s': only types and globals can be declared as ambients",
|
||||||
|
params);
|
||||||
case CANDY_VERSION_DISCREPANCY:
|
case CANDY_VERSION_DISCREPANCY:
|
||||||
return String.format("candy %s:%s was generated for a different version of the transpiler (current:%s, candy:%s)", params);
|
return String.format(
|
||||||
|
"candy %s:%s was generated for a different version of the transpiler (current:%s, candy:%s)",
|
||||||
|
params);
|
||||||
case GLOBALS_CAN_ONLY_HAVE_STATIC_MEMBERS:
|
case GLOBALS_CAN_ONLY_HAVE_STATIC_MEMBERS:
|
||||||
return String.format("globals classes can only define static members", params);
|
return String.format("globals classes can only define static members", params);
|
||||||
case GLOBALS_CLASS_CANNOT_HAVE_SUPERCLASS:
|
case GLOBALS_CLASS_CANNOT_HAVE_SUPERCLASS:
|
||||||
@ -438,21 +459,31 @@ public enum JSweetProblem {
|
|||||||
case GLOBALS_CLASS_CANNOT_BE_SUBCLASSED:
|
case GLOBALS_CLASS_CANNOT_BE_SUBCLASSED:
|
||||||
return String.format("globals classes cannot be subclassed", params);
|
return String.format("globals classes cannot be subclassed", params);
|
||||||
case CANNOT_ACCESS_THIS:
|
case CANNOT_ACCESS_THIS:
|
||||||
return String.format("'this' isn't defined in scope of %s", params);
|
return String.format("'this' isn't defined in scope of '%s'", params);
|
||||||
case CANNOT_ACCESS_STATIC_MEMBER_ON_THIS:
|
case CANNOT_ACCESS_STATIC_MEMBER_ON_THIS:
|
||||||
return String.format("member '%s' is static and cannot be accessed on 'this'", params);
|
return String.format("member '%s' is static and cannot be accessed on 'this'", params);
|
||||||
case UNTYPED_OBJECT_ODD_PARAMETER_COUNT:
|
case UNTYPED_OBJECT_ODD_PARAMETER_COUNT:
|
||||||
return String.format("wrong parameter count: method '$object' expects a list of key/value pairs as parameters", params);
|
return String.format(
|
||||||
|
"wrong parameter count: method '$object' expects a list of key/value pairs as parameters", params);
|
||||||
case UNTYPED_OBJECT_WRONG_KEY:
|
case UNTYPED_OBJECT_WRONG_KEY:
|
||||||
return String.format("wrong key: method '$object' expects a list of key/value pairs as parameters, where keys are string literals", params);
|
return String.format(
|
||||||
|
"wrong key: method '$object' expects a list of key/value pairs as parameters, where keys are string literals",
|
||||||
|
params);
|
||||||
case CYCLE_IN_STATIC_INITIALIZER_DEPENDENCIES:
|
case CYCLE_IN_STATIC_INITIALIZER_DEPENDENCIES:
|
||||||
return String.format("a cycle was detected in static intializers involving '%s'", params);
|
return String.format("a cycle was detected in static intializers involving '%s'", params);
|
||||||
case INTERNAL_TRANSPILER_ERROR:
|
case INTERNAL_TRANSPILER_ERROR:
|
||||||
return String.format("internal transpiler error");
|
return String.format("internal transpiler error");
|
||||||
case MISUSED_INSERT_MACRO:
|
case MISUSED_INSERT_MACRO:
|
||||||
return String.format("the %s macro argument must be a raw string literal");
|
return String.format("the '%s' macro argument must be a raw string literal", params);
|
||||||
case MISUSED_TEMPLATE_MACRO:
|
case MISUSED_TEMPLATE_MACRO:
|
||||||
return String.format("the %s macro last argument must be a raw string literal");
|
return String.format("the '%s' macro last argument must be a raw string literal", params);
|
||||||
|
case WRONG_MIXIN_NAME:
|
||||||
|
return String.format("the '%s' mixin must have the same root-relative name as its target ('%s')", params);
|
||||||
|
case SELF_MIXIN_TARGET:
|
||||||
|
return String.format(
|
||||||
|
"the '%s' mixin targets itself but should target another interface/declaration of the same name",
|
||||||
|
params);
|
||||||
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,7 +174,7 @@ public class JSweetTranspiler implements JSweetOptions {
|
|||||||
private boolean interfaceTracking = true;
|
private boolean interfaceTracking = true;
|
||||||
private boolean supportGetClass = true;
|
private boolean supportGetClass = true;
|
||||||
private boolean supportSaticLazyInitialization = true;
|
private boolean supportSaticLazyInitialization = true;
|
||||||
private boolean generateDefinitions = false;
|
private boolean generateDefinitions = true;
|
||||||
private ArrayList<File> jsLibFiles = new ArrayList<>();
|
private ArrayList<File> jsLibFiles = new ArrayList<>();
|
||||||
private File sourceRoot = null;
|
private File sourceRoot = null;
|
||||||
private boolean ignoreTypeScriptErrors = false;
|
private boolean ignoreTypeScriptErrors = false;
|
||||||
|
|||||||
@ -75,6 +75,8 @@ import org.jsweet.transpiler.util.JSDoc;
|
|||||||
import org.jsweet.transpiler.util.Util;
|
import org.jsweet.transpiler.util.Util;
|
||||||
|
|
||||||
import com.sun.source.tree.Tree.Kind;
|
import com.sun.source.tree.Tree.Kind;
|
||||||
|
import com.sun.tools.javac.code.Attribute;
|
||||||
|
import com.sun.tools.javac.code.Attribute.Compound;
|
||||||
import com.sun.tools.javac.code.Symbol;
|
import com.sun.tools.javac.code.Symbol;
|
||||||
import com.sun.tools.javac.code.Symbol.ClassSymbol;
|
import com.sun.tools.javac.code.Symbol.ClassSymbol;
|
||||||
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
import com.sun.tools.javac.code.Symbol.MethodSymbol;
|
||||||
@ -1184,6 +1186,19 @@ public class Java2TypeScriptTranslator extends AbstractTreePrinter {
|
|||||||
String mixin = null;
|
String mixin = null;
|
||||||
if (context.hasAnnotationType(classdecl.sym, JSweetConfig.ANNOTATION_MIXIN)) {
|
if (context.hasAnnotationType(classdecl.sym, JSweetConfig.ANNOTATION_MIXIN)) {
|
||||||
mixin = context.getAnnotationValue(classdecl.sym, JSweetConfig.ANNOTATION_MIXIN, null);
|
mixin = context.getAnnotationValue(classdecl.sym, JSweetConfig.ANNOTATION_MIXIN, null);
|
||||||
|
for (Compound c : classdecl.sym.getAnnotationMirrors()) {
|
||||||
|
if (JSweetConfig.ANNOTATION_MIXIN.equals(c.type.toString())) {
|
||||||
|
String targetName = getRootRelativeName(((Attribute.Class)c.values.head.snd).classType.tsym);
|
||||||
|
String mixinName = getRootRelativeName(classdecl.sym);
|
||||||
|
if(!mixinName.equals(targetName)) {
|
||||||
|
report(classdecl, JSweetProblem.WRONG_MIXIN_NAME, mixinName, targetName);
|
||||||
|
} else {
|
||||||
|
if(((Attribute.Class)c.values.head.snd).classType.tsym.equals(classdecl.sym)) {
|
||||||
|
report(classdecl, JSweetProblem.SELF_MIXIN_TARGET, mixinName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean extendsInterface = false;
|
boolean extendsInterface = false;
|
||||||
|
|||||||
@ -8,6 +8,9 @@ public class JQuery extends def.jquery.JQuery {
|
|||||||
|
|
||||||
public native void modal(String action);
|
public native void modal(String action);
|
||||||
|
|
||||||
public native void material_select();
|
public native JQuery material_select();
|
||||||
|
|
||||||
|
public native JQuery myExtension();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
transpiler/src/test/java/def/test2/ExtendedJQuery.java
Normal file
17
transpiler/src/test/java/def/test2/ExtendedJQuery.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package def.test2;
|
||||||
|
|
||||||
|
import jsweet.lang.Mixin;
|
||||||
|
import jsweet.lang.Name;
|
||||||
|
|
||||||
|
@Mixin(target = def.jquery.JQuery.class)
|
||||||
|
public class ExtendedJQuery extends def.jquery.JQuery {
|
||||||
|
public native void modal();
|
||||||
|
|
||||||
|
public native void modal(String action);
|
||||||
|
|
||||||
|
public native ExtendedJQuery material_select();
|
||||||
|
|
||||||
|
@Name("EXT")
|
||||||
|
public native ExtendedJQuery myExtension();
|
||||||
|
|
||||||
|
}
|
||||||
8
transpiler/src/test/java/def/test2/Globals.java
Normal file
8
transpiler/src/test/java/def/test2/Globals.java
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package def.test2;
|
||||||
|
|
||||||
|
import jsweet.lang.Erased;
|
||||||
|
|
||||||
|
public class Globals {
|
||||||
|
@Erased
|
||||||
|
public static native ExtendedJQuery $(CharSequence query);
|
||||||
|
}
|
||||||
8
transpiler/src/test/java/def/test3/JQuery.java
Normal file
8
transpiler/src/test/java/def/test3/JQuery.java
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package def.test3;
|
||||||
|
|
||||||
|
import jsweet.lang.Mixin;
|
||||||
|
|
||||||
|
@Mixin(target=JQuery.class)
|
||||||
|
public class JQuery extends def.jquery.JQuery {
|
||||||
|
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import def.test.Globals;
|
import def.test.Globals;
|
||||||
import def.test.JQuery;
|
import def.test.JQuery;
|
||||||
|
import def.test2.ExtendedJQuery;
|
||||||
import source.typing.ArraysOfLambdas;
|
import source.typing.ArraysOfLambdas;
|
||||||
import source.typing.ClassTypeAsFunction;
|
import source.typing.ClassTypeAsFunction;
|
||||||
import source.typing.ClassTypeAsTypeOf;
|
import source.typing.ClassTypeAsTypeOf;
|
||||||
@ -31,6 +32,7 @@ import source.typing.CustomStringTypes;
|
|||||||
import source.typing.InvalidIndexedAccesses;
|
import source.typing.InvalidIndexedAccesses;
|
||||||
import source.typing.Lambdas;
|
import source.typing.Lambdas;
|
||||||
import source.typing.MixinsWithDefs;
|
import source.typing.MixinsWithDefs;
|
||||||
|
import source.typing.MixinsWithDefsAndOtherName;
|
||||||
import source.typing.Numbers;
|
import source.typing.Numbers;
|
||||||
import source.typing.StringTypesUsage;
|
import source.typing.StringTypesUsage;
|
||||||
import source.typing.Tuples;
|
import source.typing.Tuples;
|
||||||
@ -161,4 +163,13 @@ public class TypingTests extends AbstractTest {
|
|||||||
}, getSourceFile(MixinsWithDefs.class), getSourceFile(Globals.class), getSourceFile(JQuery.class));
|
}, getSourceFile(MixinsWithDefs.class), getSourceFile(Globals.class), getSourceFile(JQuery.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWrongMixins() {
|
||||||
|
transpile(ModuleKind.none, logHandler -> {
|
||||||
|
Assert.assertTrue(logHandler.reportedProblems.contains(JSweetProblem.WRONG_MIXIN_NAME)
|
||||||
|
&& logHandler.reportedProblems.contains(JSweetProblem.SELF_MIXIN_TARGET));
|
||||||
|
}, getSourceFile(MixinsWithDefsAndOtherName.class), getSourceFile(def.test2.Globals.class),
|
||||||
|
getSourceFile(ExtendedJQuery.class), getSourceFile(def.test3.JQuery.class));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
package source.typing;
|
package source.typing;
|
||||||
|
|
||||||
import def.test.Globals;
|
|
||||||
import static def.test.Globals.$;
|
import static def.test.Globals.$;
|
||||||
|
|
||||||
|
import def.test.Globals;
|
||||||
|
import def.test.JQuery;
|
||||||
|
|
||||||
public class MixinsWithDefs {
|
public class MixinsWithDefs {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
$(".modal").modal();
|
$(".modal").modal();
|
||||||
$("select").material_select();
|
$("select").material_select().addClass("animated");
|
||||||
Globals.$("test").modal("");
|
Globals.$("test").modal("");
|
||||||
$("test").animate("test");
|
$("test").animate("test").addClass("animated");
|
||||||
$(".modal").attr("class");
|
$(".modal").attr("class");
|
||||||
|
JQuery extendedJQuery = $("test").myExtension();
|
||||||
|
extendedJQuery.addClass("test");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
package source.typing;
|
||||||
|
|
||||||
|
import static def.test2.Globals.$;
|
||||||
|
|
||||||
|
import def.test2.ExtendedJQuery;
|
||||||
|
import def.test2.Globals;
|
||||||
|
|
||||||
|
public class MixinsWithDefsAndOtherName {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
$(".modal").modal();
|
||||||
|
$("select").material_select().addClass("animated");
|
||||||
|
Globals.$("test").modal("");
|
||||||
|
$("test").animate("test").addClass("animated");
|
||||||
|
$(".modal").attr("class");
|
||||||
|
ExtendedJQuery extendedJQuery = $("test").myExtension();
|
||||||
|
extendedJQuery.addClass("test");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user