[Clang] Fix parsing of (auto(x))
.
Allow auto(x) to appear in a parenthesis expression. The pattern (auto( can appear as part of a declarator, so the parser is modified to avoid the ambiguity, in a way consistent with the proposed resolution to CWG1223. Reviewed By: aaron.ballman, #clang-language-wg Differential Revision: https://reviews.llvm.org/D149276
This commit is contained in:
parent
55acb70b21
commit
1b0ba1c12f
@ -468,6 +468,8 @@ Bug Fixes to C++ Support
|
||||
- Some predefined expressions are now treated as string literals in MSVC
|
||||
compatibility mode.
|
||||
(`#114 <https://github.com/llvm/llvm-project/issues/114>`_)
|
||||
- Fix parsing of `auto(x)`, when it is surrounded by parentheses.
|
||||
(`#62494 <https://github.com/llvm/llvm-project/issues/62494>`_)
|
||||
|
||||
Bug Fixes to AST Handling
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -2523,10 +2523,10 @@ private:
|
||||
enum TentativeCXXTypeIdContext {
|
||||
TypeIdInParens,
|
||||
TypeIdUnambiguous,
|
||||
TypeIdAsTemplateArgument
|
||||
TypeIdAsTemplateArgument,
|
||||
TypeIdInTrailingReturnType,
|
||||
};
|
||||
|
||||
|
||||
/// isTypeIdInParens - Assumes that a '(' was parsed and now we want to know
|
||||
/// whether the parens contain an expression or a type-id.
|
||||
/// Returns true for a type-id and false for an expression.
|
||||
@ -2654,14 +2654,16 @@ private:
|
||||
TPResult TryParseProtocolQualifiers();
|
||||
TPResult TryParsePtrOperatorSeq();
|
||||
TPResult TryParseOperatorId();
|
||||
TPResult TryParseInitDeclaratorList();
|
||||
TPResult TryParseInitDeclaratorList(bool MayHaveTrailingReturnType = false);
|
||||
TPResult TryParseDeclarator(bool mayBeAbstract, bool mayHaveIdentifier = true,
|
||||
bool mayHaveDirectInit = false);
|
||||
bool mayHaveDirectInit = false,
|
||||
bool mayHaveTrailingReturnType = false);
|
||||
TPResult TryParseParameterDeclarationClause(
|
||||
bool *InvalidAsDeclaration = nullptr, bool VersusTemplateArg = false,
|
||||
ImplicitTypenameContext AllowImplicitTypename =
|
||||
ImplicitTypenameContext::No);
|
||||
TPResult TryParseFunctionDeclarator();
|
||||
TPResult TryParseFunctionDeclarator(bool MayHaveTrailingReturnType = false);
|
||||
bool NameAfterArrowIsNonType();
|
||||
TPResult TryParseBracketDeclarator();
|
||||
TPResult TryConsumeDeclarationSpecifier();
|
||||
|
||||
|
@ -6502,7 +6502,8 @@ void Parser::ParseDirectDeclarator(Declarator &D) {
|
||||
// that it's an initializer instead.
|
||||
if (D.mayOmitIdentifier() && D.mayBeFollowedByCXXDirectInit()) {
|
||||
RevertingTentativeParsingAction PA(*this);
|
||||
if (TryParseDeclarator(true, D.mayHaveIdentifier(), true) ==
|
||||
if (TryParseDeclarator(true, D.mayHaveIdentifier(), true,
|
||||
D.getDeclSpec().getTypeSpecType() == TST_auto) ==
|
||||
TPResult::False) {
|
||||
D.SetIdentifier(nullptr, Tok.getLocation());
|
||||
goto PastIdentifier;
|
||||
|
@ -262,6 +262,7 @@ Parser::TPResult Parser::TryConsumeDeclarationSpecifier() {
|
||||
/// attribute-specifier-seqopt type-specifier-seq declarator
|
||||
///
|
||||
Parser::TPResult Parser::TryParseSimpleDeclaration(bool AllowForRangeDecl) {
|
||||
bool DeclSpecifierIsAuto = Tok.is(tok::kw_auto);
|
||||
if (TryConsumeDeclarationSpecifier() == TPResult::Error)
|
||||
return TPResult::Error;
|
||||
|
||||
@ -277,7 +278,8 @@ Parser::TPResult Parser::TryParseSimpleDeclaration(bool AllowForRangeDecl) {
|
||||
assert(TPR == TPResult::False);
|
||||
}
|
||||
|
||||
TPResult TPR = TryParseInitDeclaratorList();
|
||||
TPResult TPR = TryParseInitDeclaratorList(
|
||||
/*mayHaveTrailingReturnType=*/DeclSpecifierIsAuto);
|
||||
if (TPR != TPResult::Ambiguous)
|
||||
return TPR;
|
||||
|
||||
@ -314,10 +316,15 @@ Parser::TPResult Parser::TryParseSimpleDeclaration(bool AllowForRangeDecl) {
|
||||
/// '{' initializer-list ','[opt] '}'
|
||||
/// '{' '}'
|
||||
///
|
||||
Parser::TPResult Parser::TryParseInitDeclaratorList() {
|
||||
Parser::TPResult
|
||||
Parser::TryParseInitDeclaratorList(bool MayHaveTrailingReturnType) {
|
||||
while (true) {
|
||||
// declarator
|
||||
TPResult TPR = TryParseDeclarator(false/*mayBeAbstract*/);
|
||||
TPResult TPR = TryParseDeclarator(
|
||||
/*mayBeAbstract=*/false,
|
||||
/*mayHaveIdentifier=*/true,
|
||||
/*mayHaveDirectInit=*/false,
|
||||
/*mayHaveTrailingReturnType=*/MayHaveTrailingReturnType);
|
||||
if (TPR != TPResult::Ambiguous)
|
||||
return TPR;
|
||||
|
||||
@ -532,13 +539,18 @@ Parser::isCXXConditionDeclarationOrInitStatement(bool CanBeInitStatement,
|
||||
RevertingTentativeParsingAction PA(*this);
|
||||
|
||||
// FIXME: A tag definition unambiguously tells us this is an init-statement.
|
||||
bool MayHaveTrailingReturnType = Tok.is(tok::kw_auto);
|
||||
if (State.update(TryConsumeDeclarationSpecifier()))
|
||||
return State.result();
|
||||
assert(Tok.is(tok::l_paren) && "Expected '('");
|
||||
|
||||
while (true) {
|
||||
// Consume a declarator.
|
||||
if (State.update(TryParseDeclarator(false/*mayBeAbstract*/)))
|
||||
if (State.update(TryParseDeclarator(
|
||||
/*mayBeAbstract=*/false,
|
||||
/*mayHaveIdentifier=*/true,
|
||||
/*mayHaveDirectInit=*/false,
|
||||
/*mayHaveTrailingReturnType=*/MayHaveTrailingReturnType)))
|
||||
return State.result();
|
||||
|
||||
// Attributes, asm label, or an initializer imply this is not an expression.
|
||||
@ -623,13 +635,16 @@ bool Parser::isCXXTypeId(TentativeCXXTypeIdContext Context, bool &isAmbiguous) {
|
||||
// We need tentative parsing...
|
||||
|
||||
RevertingTentativeParsingAction PA(*this);
|
||||
bool MayHaveTrailingReturnType = Tok.is(tok::kw_auto);
|
||||
|
||||
// type-specifier-seq
|
||||
TryConsumeDeclarationSpecifier();
|
||||
assert(Tok.is(tok::l_paren) && "Expected '('");
|
||||
|
||||
// declarator
|
||||
TPR = TryParseDeclarator(true/*mayBeAbstract*/, false/*mayHaveIdentifier*/);
|
||||
TPR = TryParseDeclarator(true /*mayBeAbstract*/, false /*mayHaveIdentifier*/,
|
||||
/*mayHaveDirectInit=*/false,
|
||||
MayHaveTrailingReturnType);
|
||||
|
||||
// In case of an error, let the declaration parsing code handle it.
|
||||
if (TPR == TPResult::Error)
|
||||
@ -658,6 +673,9 @@ bool Parser::isCXXTypeId(TentativeCXXTypeIdContext Context, bool &isAmbiguous) {
|
||||
TPR = TPResult::True;
|
||||
isAmbiguous = true;
|
||||
|
||||
} else if (Context == TypeIdInTrailingReturnType) {
|
||||
TPR = TPResult::True;
|
||||
isAmbiguous = true;
|
||||
} else
|
||||
TPR = TPResult::False;
|
||||
}
|
||||
@ -1042,7 +1060,8 @@ Parser::TPResult Parser::TryParseOperatorId() {
|
||||
///
|
||||
Parser::TPResult Parser::TryParseDeclarator(bool mayBeAbstract,
|
||||
bool mayHaveIdentifier,
|
||||
bool mayHaveDirectInit) {
|
||||
bool mayHaveDirectInit,
|
||||
bool mayHaveTrailingReturnType) {
|
||||
// declarator:
|
||||
// direct-declarator
|
||||
// ptr-operator declarator
|
||||
@ -1084,7 +1103,7 @@ Parser::TPResult Parser::TryParseDeclarator(bool mayBeAbstract,
|
||||
ImplicitTypenameContext::No))) { // 'int(int)' is a function.
|
||||
// '(' parameter-declaration-clause ')' cv-qualifier-seq[opt]
|
||||
// exception-specification[opt]
|
||||
TPResult TPR = TryParseFunctionDeclarator();
|
||||
TPResult TPR = TryParseFunctionDeclarator(mayHaveTrailingReturnType);
|
||||
if (TPR != TPResult::Ambiguous)
|
||||
return TPR;
|
||||
} else {
|
||||
@ -1123,7 +1142,7 @@ Parser::TPResult Parser::TryParseDeclarator(bool mayBeAbstract,
|
||||
// direct-declarator '(' parameter-declaration-clause ')'
|
||||
// cv-qualifier-seq[opt] exception-specification[opt]
|
||||
ConsumeParen();
|
||||
TPR = TryParseFunctionDeclarator();
|
||||
TPR = TryParseFunctionDeclarator(mayHaveTrailingReturnType);
|
||||
} else if (Tok.is(tok::l_square)) {
|
||||
// direct-declarator '[' constant-expression[opt] ']'
|
||||
// direct-abstract-declarator[opt] '[' constant-expression[opt] ']'
|
||||
@ -1390,6 +1409,16 @@ Parser::isCXXDeclarationSpecifier(ImplicitTypenameContext AllowImplicitTypename,
|
||||
return isCXXDeclarationSpecifier(ImplicitTypenameContext::Yes,
|
||||
BracedCastResult, InvalidAsDeclSpec);
|
||||
|
||||
case tok::kw_auto: {
|
||||
if (!getLangOpts().CPlusPlus23)
|
||||
return TPResult::True;
|
||||
if (NextToken().is(tok::l_brace))
|
||||
return TPResult::False;
|
||||
if (NextToken().is(tok::l_paren))
|
||||
return TPResult::Ambiguous;
|
||||
return TPResult::True;
|
||||
}
|
||||
|
||||
case tok::coloncolon: { // ::foo::bar
|
||||
const Token &Next = NextToken();
|
||||
if (Next.isOneOf(tok::kw_new, // ::new
|
||||
@ -1423,7 +1452,6 @@ Parser::isCXXDeclarationSpecifier(ImplicitTypenameContext AllowImplicitTypename,
|
||||
case tok::kw_static:
|
||||
case tok::kw_extern:
|
||||
case tok::kw_mutable:
|
||||
case tok::kw_auto:
|
||||
case tok::kw___thread:
|
||||
case tok::kw_thread_local:
|
||||
case tok::kw__Thread_local:
|
||||
@ -2002,6 +2030,7 @@ Parser::TPResult Parser::TryParseParameterDeclarationClause(
|
||||
return TPR;
|
||||
|
||||
bool SeenType = false;
|
||||
bool DeclarationSpecifierIsAuto = Tok.is(tok::kw_auto);
|
||||
do {
|
||||
SeenType |= isCXXDeclarationSpecifierAType();
|
||||
if (TryConsumeDeclarationSpecifier() == TPResult::Error)
|
||||
@ -2023,7 +2052,11 @@ Parser::TPResult Parser::TryParseParameterDeclarationClause(
|
||||
|
||||
// declarator
|
||||
// abstract-declarator[opt]
|
||||
TPR = TryParseDeclarator(true/*mayBeAbstract*/);
|
||||
TPR = TryParseDeclarator(
|
||||
/*mayBeAbstract=*/true,
|
||||
/*mayHaveIdentifier=*/true,
|
||||
/*mayHaveDirectInit=*/false,
|
||||
/*mayHaveTrailingReturnType=*/DeclarationSpecifierIsAuto);
|
||||
if (TPR != TPResult::Ambiguous)
|
||||
return TPR;
|
||||
|
||||
@ -2077,7 +2110,8 @@ Parser::TPResult Parser::TryParseParameterDeclarationClause(
|
||||
/// exception-specification:
|
||||
/// 'throw' '(' type-id-list[opt] ')'
|
||||
///
|
||||
Parser::TPResult Parser::TryParseFunctionDeclarator() {
|
||||
Parser::TPResult
|
||||
Parser::TryParseFunctionDeclarator(bool MayHaveTrailingReturnType) {
|
||||
// The '(' is already parsed.
|
||||
|
||||
TPResult TPR = TryParseParameterDeclarationClause();
|
||||
@ -2122,7 +2156,50 @@ Parser::TPResult Parser::TryParseFunctionDeclarator() {
|
||||
}
|
||||
}
|
||||
|
||||
// attribute-specifier-seq
|
||||
if (!TrySkipAttributes())
|
||||
return TPResult::Ambiguous;
|
||||
|
||||
// trailing-return-type
|
||||
if (Tok.is(tok::arrow) && MayHaveTrailingReturnType) {
|
||||
if (TPR == TPResult::True)
|
||||
return TPR;
|
||||
ConsumeToken();
|
||||
if (Tok.is(tok::identifier) && NameAfterArrowIsNonType()) {
|
||||
return TPResult::False;
|
||||
}
|
||||
if (isCXXTypeId(TentativeCXXTypeIdContext::TypeIdInTrailingReturnType))
|
||||
return TPResult::True;
|
||||
}
|
||||
|
||||
return TPResult::Ambiguous;
|
||||
}
|
||||
|
||||
// When parsing an identifier after an arrow it may be a member expression,
|
||||
// in which case we should not annotate it as an independant expression
|
||||
// so we just lookup that name, if it's not a type the construct is not
|
||||
// a function declaration.
|
||||
bool Parser::NameAfterArrowIsNonType() {
|
||||
assert(Tok.is(tok::identifier));
|
||||
Token Next = NextToken();
|
||||
if (Next.is(tok::coloncolon))
|
||||
return false;
|
||||
IdentifierInfo *Name = Tok.getIdentifierInfo();
|
||||
SourceLocation NameLoc = Tok.getLocation();
|
||||
CXXScopeSpec SS;
|
||||
TentativeParseCCC CCC(Next);
|
||||
Sema::NameClassification Classification =
|
||||
Actions.ClassifyName(getCurScope(), SS, Name, NameLoc, Next, &CCC);
|
||||
switch (Classification.getKind()) {
|
||||
case Sema::NC_OverloadSet:
|
||||
case Sema::NC_NonType:
|
||||
case Sema::NC_VarTemplate:
|
||||
case Sema::NC_FunctionTemplate:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// '[' constant-expression[opt] ']'
|
||||
|
@ -1947,7 +1947,7 @@ bool Parser::TryAnnotateTypeOrScopeToken(
|
||||
assert((Tok.is(tok::identifier) || Tok.is(tok::coloncolon) ||
|
||||
Tok.is(tok::kw_typename) || Tok.is(tok::annot_cxxscope) ||
|
||||
Tok.is(tok::kw_decltype) || Tok.is(tok::annot_template_id) ||
|
||||
Tok.is(tok::kw___super)) &&
|
||||
Tok.is(tok::kw___super) || Tok.is(tok::kw_auto)) &&
|
||||
"Cannot be a type or scope token!");
|
||||
|
||||
if (Tok.is(tok::kw_typename)) {
|
||||
|
@ -31,6 +31,55 @@ namespace dr1213 { // dr1213: 7
|
||||
#endif
|
||||
}
|
||||
|
||||
#if __cplusplus >= 201103L
|
||||
namespace dr1223 { // dr1227: yes open
|
||||
struct M;
|
||||
template <typename T>
|
||||
struct V;
|
||||
struct S {
|
||||
S* operator()();
|
||||
int N;
|
||||
int M;
|
||||
#if __cplusplus > 202002L
|
||||
template <typename T>
|
||||
static constexpr auto V = 0;
|
||||
void f(char);
|
||||
void f(int);
|
||||
void mem(S s) {
|
||||
auto(s)()->M; //expected-warning {{expression result unused}}
|
||||
auto(s)()->V<int>; //expected-warning {{expression result unused}}
|
||||
auto(s)()->f(0);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
void f(S s) {
|
||||
{
|
||||
#if __cplusplus > 202002L
|
||||
auto(s)()->N; //expected-warning {{expression result unused}}
|
||||
#endif
|
||||
auto(s)()->M;
|
||||
}
|
||||
{
|
||||
S(s)()->N; //expected-warning {{expression result unused}}
|
||||
S(s)()->M; //expected-warning {{expression result unused}}
|
||||
}
|
||||
}
|
||||
|
||||
struct A {
|
||||
A(int*);
|
||||
A *operator()();
|
||||
};
|
||||
typedef struct BB { int C[2]; } *B, C;
|
||||
void g() {
|
||||
A a(B ()->C);
|
||||
A b(auto ()->C);
|
||||
static_assert(sizeof(B ()->C[1] == sizeof(int)), "");
|
||||
sizeof(auto () -> C[1]); // expected-error{{function cannot return array type 'C[1]'}}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#if __cplusplus >= 201103L
|
||||
namespace dr1227 { // dr1227: yes
|
||||
template <class T> struct A { using X = typename T::X; }; // expected-error {{type 'int' cannot be used prior to '::' because it has no members}}
|
||||
|
@ -42,3 +42,11 @@ void arr() {
|
||||
int (*_Atomic atomic_ptr_to_int);
|
||||
*atomic_ptr_to_int = 42;
|
||||
}
|
||||
|
||||
namespace function_with_trailing {
|
||||
struct Foo {
|
||||
Foo(int);
|
||||
};
|
||||
template <typename T> void bar()
|
||||
{ Foo _(T::method()->mem()); }
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// RUN: %clang_cc1 -std=c++17 %s -verify -fcxx-exceptions
|
||||
// RUN: %clang_cc1 -std=c++17 %s -verify=expected,cxx17 -fcxx-exceptions
|
||||
// RUN: %clang_cc1 -std=c++2b %s -verify=expected,cxx2b -fcxx-exceptions
|
||||
// RUN: not %clang_cc1 -std=c++17 %s -emit-llvm-only -fcxx-exceptions
|
||||
|
||||
struct S { int a, b, c; };
|
||||
@ -30,7 +31,7 @@ namespace ForRangeDecl {
|
||||
namespace OtherDecl {
|
||||
// A parameter-declaration is not a simple-declaration.
|
||||
// This parses as an array declaration.
|
||||
void f(auto [a, b, c]); // expected-error {{'auto' not allowed in function prototype}} expected-error {{'a'}}
|
||||
void f(auto [a, b, c]); // cxx17-error {{'auto' not allowed in function prototype}} expected-error {{'a'}}
|
||||
|
||||
void g() {
|
||||
// A condition is allowed as a Clang extension.
|
||||
@ -57,7 +58,7 @@ namespace OtherDecl {
|
||||
namespace GoodSpecifiers {
|
||||
void f() {
|
||||
int n[1];
|
||||
const volatile auto &[a] = n;
|
||||
const volatile auto &[a] = n; // cxx2b-warning {{volatile qualifier in structured binding declaration is deprecated}}
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,8 +68,8 @@ namespace BadSpecifiers {
|
||||
struct S { int n; } s;
|
||||
void f() {
|
||||
// storage-class-specifiers
|
||||
static auto &[a] = n; // expected-warning {{declared 'static' is a C++20 extension}}
|
||||
thread_local auto &[b] = n; // expected-warning {{declared 'thread_local' is a C++20 extension}}
|
||||
static auto &[a] = n; // cxx17-warning {{declared 'static' is a C++20 extension}}
|
||||
thread_local auto &[b] = n; // cxx17-warning {{declared 'thread_local' is a C++20 extension}}
|
||||
extern auto &[c] = n; // expected-error {{cannot be declared 'extern'}} expected-error {{declaration of block scope identifier with linkage cannot have an initializer}}
|
||||
struct S {
|
||||
mutable auto &[d] = n; // expected-error {{not permitted in this context}}
|
||||
@ -85,16 +86,19 @@ namespace BadSpecifiers {
|
||||
}
|
||||
|
||||
static constexpr inline thread_local auto &[j1] = n; // expected-error {{cannot be declared with 'constexpr inline' specifiers}}
|
||||
static thread_local auto &[j2] = n; // expected-warning {{declared with 'static thread_local' specifiers is a C++20 extension}}
|
||||
static thread_local auto &[j2] = n; // cxx17-warning {{declared with 'static thread_local' specifiers is a C++20 extension}}
|
||||
|
||||
inline auto &[k] = n; // expected-error {{cannot be declared 'inline'}}
|
||||
|
||||
const int K = 5;
|
||||
auto ([c]) = s; // expected-error {{decomposition declaration cannot be declared with parentheses}}
|
||||
void g() {
|
||||
// defining-type-specifiers other than cv-qualifiers and 'auto'
|
||||
S [a] = s; // expected-error {{cannot be declared with type 'S'}}
|
||||
decltype(auto) [b] = s; // expected-error {{cannot be declared with type 'decltype(auto)'}}
|
||||
auto ([c]) = s; // expected-error {{cannot be declared with parentheses}}
|
||||
auto ([c2]) = s; // cxx17-error {{decomposition declaration cannot be declared with parenthese}} \
|
||||
// cxx2b-error {{use of undeclared identifier 'c2'}} \
|
||||
// cxx2b-error {{expected body of lambda expression}} \
|
||||
|
||||
// FIXME: This error is not very good.
|
||||
auto [d]() = s; // expected-error {{expected ';'}} expected-error {{expected expression}}
|
||||
|
@ -18,7 +18,37 @@ struct looks_like_declaration {
|
||||
|
||||
using T = looks_like_declaration *;
|
||||
void f() { T(&a)->n = 1; }
|
||||
// FIXME: They should be deemed expressions without breaking function pointer
|
||||
// parameter declarations with trailing return types.
|
||||
// void g() { auto(&a)->n = 0; }
|
||||
// void h() { auto{&a}->n = 0; }
|
||||
void g() { auto(&a)->n = 0; } // cxx23-warning {{before C++23}} \
|
||||
// cxx20-error {{declaration of variable 'a' with deduced type 'auto (&)' requires an initializer}} \
|
||||
// cxx20-error {{expected ';' at end of declaration}}
|
||||
void h() { auto{&a}->n = 0; } // cxx23-warning {{before C++23}} \
|
||||
// cxx20-error {{expected unqualified-id}} \
|
||||
// cxx20-error {{expected expression}}
|
||||
|
||||
void e(auto (*p)(int y) -> decltype(y)) {}
|
||||
|
||||
struct M;
|
||||
struct S{
|
||||
S operator()();
|
||||
S* operator->();
|
||||
int N;
|
||||
int M;
|
||||
} s; // expected-note {{here}}
|
||||
|
||||
void test() {
|
||||
auto(s)()->N; // cxx23-warning {{expression result unused}} \
|
||||
// cxx23-warning {{before C++23}} \
|
||||
// cxx20-error {{unknown type name 'N'}}
|
||||
auto(s)()->M; // expected-error {{redefinition of 's' as different kind of symbol}}
|
||||
}
|
||||
|
||||
void test_paren() {
|
||||
int a = (auto(0)); // cxx23-warning {{before C++23}} \
|
||||
// cxx20-error {{expected expression}} \
|
||||
// cxx20-error {{expected ')'}} \
|
||||
// cxx20-note {{to match this '('}}
|
||||
int b = (auto{0}); // cxx23-warning {{before C++23}} \
|
||||
// cxx20-error {{expected expression}} \
|
||||
// cxx20-error {{expected ')'}} \
|
||||
// cxx20-note {{to match this '('}}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user