TypeScript: Support type arguments on tagged templates (#7754)
| Q | A | ------------------------ | --- | Fixed Issues? | #7747 (partly) | Patch: Bug Fix? | | Major: Breaking Change? | | Minor: New Feature? | Yes | Tests Added + Pass? | Yes | Documentation PR | | Any Dependency Changes? | | License | MIT @JamesHenry This changes the AST format. CC @DanielRosenwasser for review. Supports parsing type arguments on tagged template calls. Should wait on Microsoft/TypeScript#23430 to be merged so we're sure we have the final syntax.
This commit is contained in:
parent
db2a9fc96e
commit
8ee24fdfc0
@ -1,5 +1,6 @@
|
|||||||
export function TaggedTemplateExpression(node: Object) {
|
export function TaggedTemplateExpression(node: Object) {
|
||||||
this.print(node.tag, node);
|
this.print(node.tag, node);
|
||||||
|
this.print(node.typeParameters, node); // TS
|
||||||
this.print(node.quasi, node);
|
this.print(node.quasi, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/input.js
vendored
Normal file
1
packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/input.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
f<T>``;
|
||||||
1
packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/output.js
vendored
Normal file
1
packages/babel-generator/test/fixtures/typescript/type-arguments-tagged-template/output.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
f<T>``;
|
||||||
@ -563,22 +563,41 @@ export default class ExpressionParser extends LValParser {
|
|||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
} else if (this.match(tt.backQuote)) {
|
} else if (this.match(tt.backQuote)) {
|
||||||
const node = this.startNodeAt(startPos, startLoc);
|
return this.parseTaggedTemplateExpression(
|
||||||
node.tag = base;
|
startPos,
|
||||||
node.quasi = this.parseTemplate(true);
|
startLoc,
|
||||||
if (state.optionalChainMember) {
|
base,
|
||||||
this.raise(
|
state,
|
||||||
startPos,
|
);
|
||||||
"Tagged Template Literals are not allowed in optionalChain",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.finishNode(node, "TaggedTemplateExpression");
|
|
||||||
} else {
|
} else {
|
||||||
state.stop = true;
|
state.stop = true;
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseTaggedTemplateExpression(
|
||||||
|
startPos: number,
|
||||||
|
startLoc: Position,
|
||||||
|
base: N.Expression,
|
||||||
|
state: N.ParseSubscriptState,
|
||||||
|
typeArguments?: ?N.TsTypeParameterInstantiation,
|
||||||
|
): N.TaggedTemplateExpression {
|
||||||
|
const node: N.TaggedTemplateExpression = this.startNodeAt(
|
||||||
|
startPos,
|
||||||
|
startLoc,
|
||||||
|
);
|
||||||
|
node.tag = base;
|
||||||
|
node.quasi = this.parseTemplate(true);
|
||||||
|
if (typeArguments) node.typeParameters = typeArguments;
|
||||||
|
if (state.optionalChainMember) {
|
||||||
|
this.raise(
|
||||||
|
startPos,
|
||||||
|
"Tagged Template Literals are not allowed in optionalChain",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.finishNode(node, "TaggedTemplateExpression");
|
||||||
|
}
|
||||||
|
|
||||||
atPossibleAsync(base: N.Expression): boolean {
|
atPossibleAsync(base: N.Expression): boolean {
|
||||||
return (
|
return (
|
||||||
!this.state.containsEsc &&
|
!this.state.containsEsc &&
|
||||||
|
|||||||
@ -836,16 +836,6 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
|||||||
return this.finishNode(node, "TSTypeAssertion");
|
return this.finishNode(node, "TSTypeAssertion");
|
||||||
}
|
}
|
||||||
|
|
||||||
tsTryParseTypeArgumentsInExpression(
|
|
||||||
eatNextToken: boolean,
|
|
||||||
): ?N.TsTypeParameterInstantiation {
|
|
||||||
return this.tsTryParseAndCatch(() => {
|
|
||||||
const res = this.tsParseTypeArguments();
|
|
||||||
if (eatNextToken) this.expect(tt.parenL);
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tsParseHeritageClause(): $ReadOnlyArray<N.TsExpressionWithTypeArguments> {
|
tsParseHeritageClause(): $ReadOnlyArray<N.TsExpressionWithTypeArguments> {
|
||||||
return this.tsParseDelimitedList(
|
return this.tsParseDelimitedList(
|
||||||
"HeritageClauseElement",
|
"HeritageClauseElement",
|
||||||
@ -1376,38 +1366,53 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
|||||||
return this.finishNode(nonNullExpression, "TSNonNullExpression");
|
return this.finishNode(nonNullExpression, "TSNonNullExpression");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!noCalls && this.isRelational("<")) {
|
// There are number of things we are going to "maybe" parse, like type arguments on
|
||||||
if (this.atPossibleAsync(base)) {
|
// tagged template expressions. If any of them fail, walk it back and continue.
|
||||||
// Almost certainly this is a generic async function `async <T>() => ...
|
const result = this.tsTryParseAndCatch(() => {
|
||||||
// But it might be a call with a type argument `async<T>();`
|
if (this.isRelational("<")) {
|
||||||
const asyncArrowFn = this.tsTryParseGenericAsyncArrowFunction(
|
if (!noCalls && this.atPossibleAsync(base)) {
|
||||||
startPos,
|
// Almost certainly this is a generic async function `async <T>() => ...
|
||||||
startLoc,
|
// But it might be a call with a type argument `async<T>();`
|
||||||
);
|
const asyncArrowFn = this.tsTryParseGenericAsyncArrowFunction(
|
||||||
if (asyncArrowFn) {
|
startPos,
|
||||||
return asyncArrowFn;
|
startLoc,
|
||||||
|
);
|
||||||
|
if (asyncArrowFn) {
|
||||||
|
return asyncArrowFn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const node: N.CallExpression = this.startNodeAt(startPos, startLoc);
|
||||||
|
node.callee = base;
|
||||||
|
|
||||||
|
const typeArguments = this.tsParseTypeArguments();
|
||||||
|
|
||||||
|
if (typeArguments) {
|
||||||
|
if (!noCalls && this.eat(tt.parenL)) {
|
||||||
|
// possibleAsync always false here, because we would have handled it above.
|
||||||
|
// $FlowIgnore (won't be any undefined arguments)
|
||||||
|
node.arguments = this.parseCallExpressionArguments(
|
||||||
|
tt.parenR,
|
||||||
|
/* possibleAsync */ false,
|
||||||
|
);
|
||||||
|
node.typeParameters = typeArguments;
|
||||||
|
return this.finishCallExpression(node);
|
||||||
|
} else if (this.match(tt.backQuote)) {
|
||||||
|
return this.parseTaggedTemplateExpression(
|
||||||
|
startPos,
|
||||||
|
startLoc,
|
||||||
|
base,
|
||||||
|
state,
|
||||||
|
typeArguments,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: N.CallExpression = this.startNodeAt(startPos, startLoc);
|
this.unexpected();
|
||||||
node.callee = base;
|
});
|
||||||
|
|
||||||
// May be passing type arguments. But may just be the `<` operator.
|
if (result) return result;
|
||||||
// Note: With `/*eatNextToken*/ true` this also eats the `(` following the type arguments
|
|
||||||
const typeArguments = this.tsTryParseTypeArgumentsInExpression(
|
|
||||||
/*eatNextToken*/ true,
|
|
||||||
);
|
|
||||||
if (typeArguments) {
|
|
||||||
// possibleAsync always false here, because we would have handled it above.
|
|
||||||
// $FlowIgnore (won't be any undefined arguments)
|
|
||||||
node.arguments = this.parseCallExpressionArguments(
|
|
||||||
tt.parenR,
|
|
||||||
/* possibleAsync */ false,
|
|
||||||
);
|
|
||||||
node.typeParameters = typeArguments;
|
|
||||||
return this.finishCallExpression(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.parseSubscript(base, startPos, startLoc, noCalls, state);
|
return super.parseSubscript(base, startPos, startLoc, noCalls, state);
|
||||||
}
|
}
|
||||||
@ -2127,8 +2132,8 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
|||||||
jsxParseOpeningElementAfterName(
|
jsxParseOpeningElementAfterName(
|
||||||
node: N.JSXOpeningElement,
|
node: N.JSXOpeningElement,
|
||||||
): N.JSXOpeningElement {
|
): N.JSXOpeningElement {
|
||||||
const typeArguments = this.tsTryParseTypeArgumentsInExpression(
|
const typeArguments = this.tsTryParseAndCatch(() =>
|
||||||
/*eatNextToken*/ false,
|
this.tsParseTypeArguments(),
|
||||||
);
|
);
|
||||||
if (typeArguments) node.typeParameters = typeArguments;
|
if (typeArguments) node.typeParameters = typeArguments;
|
||||||
return super.jsxParseOpeningElementAfterName(node);
|
return super.jsxParseOpeningElementAfterName(node);
|
||||||
|
|||||||
@ -577,6 +577,7 @@ export type TaggedTemplateExpression = NodeBase & {
|
|||||||
type: "TaggedTemplateExpression",
|
type: "TaggedTemplateExpression",
|
||||||
tag: Expression,
|
tag: Expression,
|
||||||
quasi: TemplateLiteral,
|
quasi: TemplateLiteral,
|
||||||
|
typeParameters?: ?TypeParameterInstantiationBase, // TODO: Not in spec
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TemplateElement = NodeBase & {
|
export type TemplateElement = NodeBase & {
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
new C<T>
|
||||||
|
``
|
||||||
185
packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/output.json
vendored
Normal file
185
packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template-no-asi/output.json
vendored
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"type": "File",
|
||||||
|
"start": 0,
|
||||||
|
"end": 11,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"program": {
|
||||||
|
"type": "Program",
|
||||||
|
"start": 0,
|
||||||
|
"end": 11,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceType": "module",
|
||||||
|
"interpreter": null,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "ExpressionStatement",
|
||||||
|
"start": 0,
|
||||||
|
"end": 11,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expression": {
|
||||||
|
"type": "NewExpression",
|
||||||
|
"start": 0,
|
||||||
|
"end": 11,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"callee": {
|
||||||
|
"type": "TaggedTemplateExpression",
|
||||||
|
"start": 4,
|
||||||
|
"end": 11,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 4
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"start": 4,
|
||||||
|
"end": 5,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 4
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"identifierName": "C"
|
||||||
|
},
|
||||||
|
"name": "C"
|
||||||
|
},
|
||||||
|
"quasi": {
|
||||||
|
"type": "TemplateLiteral",
|
||||||
|
"start": 9,
|
||||||
|
"end": 11,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expressions": [],
|
||||||
|
"quasis": [
|
||||||
|
{
|
||||||
|
"type": "TemplateElement",
|
||||||
|
"start": 10,
|
||||||
|
"end": 10,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 1
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"raw": "",
|
||||||
|
"cooked": ""
|
||||||
|
},
|
||||||
|
"tail": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"typeParameters": {
|
||||||
|
"type": "TSTypeParameterInstantiation",
|
||||||
|
"start": 5,
|
||||||
|
"end": 8,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"type": "TSTypeReference",
|
||||||
|
"start": 6,
|
||||||
|
"end": 7,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 6
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typeName": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"start": 6,
|
||||||
|
"end": 7,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 6
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 7
|
||||||
|
},
|
||||||
|
"identifierName": "T"
|
||||||
|
},
|
||||||
|
"name": "T"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arguments": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"directives": []
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/input.js
vendored
Normal file
1
packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/input.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
f<T>``;
|
||||||
169
packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/output.json
vendored
Normal file
169
packages/babel-parser/test/fixtures/typescript/type-arguments/tagged-template/output.json
vendored
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
{
|
||||||
|
"type": "File",
|
||||||
|
"start": 0,
|
||||||
|
"end": 7,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"program": {
|
||||||
|
"type": "Program",
|
||||||
|
"start": 0,
|
||||||
|
"end": 7,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceType": "module",
|
||||||
|
"interpreter": null,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "ExpressionStatement",
|
||||||
|
"start": 0,
|
||||||
|
"end": 7,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expression": {
|
||||||
|
"type": "TaggedTemplateExpression",
|
||||||
|
"start": 0,
|
||||||
|
"end": 6,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"start": 0,
|
||||||
|
"end": 1,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 1
|
||||||
|
},
|
||||||
|
"identifierName": "f"
|
||||||
|
},
|
||||||
|
"name": "f"
|
||||||
|
},
|
||||||
|
"quasi": {
|
||||||
|
"type": "TemplateLiteral",
|
||||||
|
"start": 4,
|
||||||
|
"end": 6,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 4
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expressions": [],
|
||||||
|
"quasis": [
|
||||||
|
{
|
||||||
|
"type": "TemplateElement",
|
||||||
|
"start": 5,
|
||||||
|
"end": 5,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"raw": "",
|
||||||
|
"cooked": ""
|
||||||
|
},
|
||||||
|
"tail": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"typeParameters": {
|
||||||
|
"type": "TSTypeParameterInstantiation",
|
||||||
|
"start": 1,
|
||||||
|
"end": 4,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 1
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"type": "TSTypeReference",
|
||||||
|
"start": 2,
|
||||||
|
"end": 3,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typeName": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"start": 2,
|
||||||
|
"end": 3,
|
||||||
|
"loc": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 3
|
||||||
|
},
|
||||||
|
"identifierName": "T"
|
||||||
|
},
|
||||||
|
"name": "T"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"directives": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -268,6 +268,10 @@ export default declare((api, { jsxPragma = "React" }) => {
|
|||||||
JSXOpeningElement(path) {
|
JSXOpeningElement(path) {
|
||||||
path.node.typeParameters = null;
|
path.node.typeParameters = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
TaggedTemplateExpression(path) {
|
||||||
|
path.node.typeParameters = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
f<T>``;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
f``;
|
||||||
@ -510,6 +510,13 @@ defineType("TaggedTemplateExpression", {
|
|||||||
quasi: {
|
quasi: {
|
||||||
validate: assertNodeType("TemplateLiteral"),
|
validate: assertNodeType("TemplateLiteral"),
|
||||||
},
|
},
|
||||||
|
typeParameters: {
|
||||||
|
validate: assertNodeType(
|
||||||
|
"TypeParameterInstantiation",
|
||||||
|
"TSTypeParameterInstantiation",
|
||||||
|
),
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user