Skip to content

Commit 1594413

Browse files
Add Clang attribute to ensure that fields are initialized explicitly (#102040)
This is a new Clang-specific attribute to ensure that field initializations are performed explicitly. For example, if we have ``` struct B { [[clang::explicit]] int f1; }; ``` then the diagnostic would trigger if we do `B b{};`: ``` field 'f1' is left uninitialized, but was marked as requiring initialization ``` This prevents callers from accidentally forgetting to initialize fields, particularly when new fields are added to the class.
1 parent 576b538 commit 1594413

16 files changed

+404
-7
lines changed

clang/include/clang/AST/Decl.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4285,6 +4285,14 @@ class RecordDecl : public TagDecl {
42854285
RecordDeclBits.HasNonTrivialToPrimitiveCopyCUnion = V;
42864286
}
42874287

4288+
bool hasUninitializedExplicitInitFields() const {
4289+
return RecordDeclBits.HasUninitializedExplicitInitFields;
4290+
}
4291+
4292+
void setHasUninitializedExplicitInitFields(bool V) {
4293+
RecordDeclBits.HasUninitializedExplicitInitFields = V;
4294+
}
4295+
42884296
/// Determine whether this class can be passed in registers. In C++ mode,
42894297
/// it must have at least one trivial, non-deleted copy or move constructor.
42904298
/// FIXME: This should be set as part of completeDefinition.

clang/include/clang/AST/DeclBase.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,9 @@ class DeclContext {
14461446
/// hasLazyLocalLexicalLookups, hasLazyExternalLexicalLookups
14471447
friend class ASTWriter;
14481448

1449+
protected:
1450+
enum { NumOdrHashBits = 25 };
1451+
14491452
// We use uint64_t in the bit-fields below since some bit-fields
14501453
// cross the unsigned boundary and this breaks the packing.
14511454

@@ -1667,6 +1670,14 @@ class DeclContext {
16671670
LLVM_PREFERRED_TYPE(bool)
16681671
uint64_t HasNonTrivialToPrimitiveCopyCUnion : 1;
16691672

1673+
/// True if any field is marked as requiring explicit initialization with
1674+
/// [[clang::requires_explicit_initialization]].
1675+
/// In C++, this is also set for types without a user-provided default
1676+
/// constructor, and is propagated from any base classes and/or member
1677+
/// variables whose types are aggregates.
1678+
LLVM_PREFERRED_TYPE(bool)
1679+
uint64_t HasUninitializedExplicitInitFields : 1;
1680+
16701681
/// Indicates whether this struct is destroyed in the callee.
16711682
LLVM_PREFERRED_TYPE(bool)
16721683
uint64_t ParamDestroyedInCallee : 1;
@@ -1681,7 +1692,7 @@ class DeclContext {
16811692

16821693
/// True if a valid hash is stored in ODRHash. This should shave off some
16831694
/// extra storage and prevent CXXRecordDecl to store unused bits.
1684-
uint64_t ODRHash : 26;
1695+
uint64_t ODRHash : NumOdrHashBits;
16851696
};
16861697

16871698
/// Number of inherited and non-inherited bits in RecordDeclBitfields.

clang/include/clang/Basic/Attr.td

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,13 @@ def Leaf : InheritableAttr {
19021902
let SimpleHandler = 1;
19031903
}
19041904

1905+
def ExplicitInit : InheritableAttr {
1906+
let Spellings = [Clang<"requires_explicit_initialization">];
1907+
let Subjects = SubjectList<[Field], ErrorDiag>;
1908+
let Documentation = [ExplicitInitDocs];
1909+
let SimpleHandler = 1;
1910+
}
1911+
19051912
def LifetimeBound : DeclOrTypeAttr {
19061913
let Spellings = [Clang<"lifetimebound", 0>];
19071914
let Subjects = SubjectList<[ParmVar, ImplicitObjectParameter], ErrorDiag>;

clang/include/clang/Basic/AttrDocs.td

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,55 @@ is not specified.
16841684
}];
16851685
}
16861686

1687+
def ExplicitInitDocs : Documentation {
1688+
let Category = DocCatField;
1689+
let Content = [{
1690+
The ``clang::requires_explicit_initialization`` attribute indicates that a
1691+
field of an aggregate must be initialized explicitly by the user when an object
1692+
of the aggregate type is constructed. The attribute supports both C and C++,
1693+
but its usage is invalid on non-aggregates.
1694+
1695+
Note that this attribute is *not* a memory safety feature, and is *not* intended
1696+
to guard against use of uninitialized memory.
1697+
1698+
Rather, it is intended for use in "parameter-objects", used to simulate,
1699+
for example, the passing of named parameters.
1700+
The attribute generates a warning when explicit initializers for such
1701+
variables are not provided (this occurs regardless of whether any in-class field
1702+
initializers exist):
1703+
1704+
.. code-block:: c++
1705+
1706+
struct Buffer {
1707+
void *address [[clang::requires_explicit_initialization]];
1708+
size_t length [[clang::requires_explicit_initialization]] = 0;
1709+
};
1710+
1711+
struct ArrayIOParams {
1712+
size_t count [[clang::requires_explicit_initialization]];
1713+
size_t element_size [[clang::requires_explicit_initialization]];
1714+
int flags = 0;
1715+
};
1716+
1717+
size_t ReadArray(FILE *file, struct Buffer buffer,
1718+
struct ArrayIOParams params);
1719+
1720+
int main() {
1721+
unsigned int buf[512];
1722+
ReadArray(stdin, {
1723+
buf
1724+
// warning: field 'length' is not explicitly initialized
1725+
}, {
1726+
.count = sizeof(buf) / sizeof(*buf),
1727+
// warning: field 'element_size' is not explicitly initialized
1728+
// (Note that a missing initializer for 'flags' is not diagnosed, because
1729+
// the field is not marked as requiring explicit initialization.)
1730+
});
1731+
}
1732+
1733+
}];
1734+
}
1735+
16871736
def NoUniqueAddressDocs : Documentation {
16881737
let Category = DocCatField;
16891738
let Content = [{

clang/include/clang/Basic/DiagnosticASTKinds.td

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,16 @@ def note_constexpr_assumption_failed : Note<
396396
def err_experimental_clang_interp_failed : Error<
397397
"the experimental clang interpreter failed to evaluate an expression">;
398398

399+
def warn_attribute_needs_aggregate : Warning<
400+
"%0 attribute is ignored in non-aggregate type %1">,
401+
InGroup<IgnoredAttributes>;
402+
403+
def warn_cxx20_compat_requires_explicit_init_non_aggregate : Warning<
404+
"explicit initialization of field %1 will not be enforced in C++20 and later "
405+
"because %2 has a user-declared constructor, making the type no longer an "
406+
"aggregate">,
407+
DefaultIgnore, InGroup<CXX20Compat>;
408+
399409
def warn_integer_constant_overflow : Warning<
400410
"overflow in expression; result is %0 with type %1">,
401411
InGroup<DiagGroup<"integer-overflow">>;

clang/include/clang/Basic/DiagnosticGroups.td

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ def Trigraphs : DiagGroup<"trigraphs">;
796796
def UndefinedReinterpretCast : DiagGroup<"undefined-reinterpret-cast">;
797797
def ReinterpretBaseClass : DiagGroup<"reinterpret-base-class">;
798798
def Unicode : DiagGroup<"unicode">;
799+
def UninitializedExplicitInit : DiagGroup<"uninitialized-explicit-init">;
799800
def UninitializedMaybe : DiagGroup<"conditional-uninitialized">;
800801
def UninitializedSometimes : DiagGroup<"sometimes-uninitialized">;
801802
def UninitializedStaticSelfInit : DiagGroup<"static-self-init">;

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2347,6 +2347,9 @@ def err_init_reference_member_uninitialized : Error<
23472347
"reference member of type %0 uninitialized">;
23482348
def note_uninit_reference_member : Note<
23492349
"uninitialized reference member is here">;
2350+
def warn_field_requires_explicit_init : Warning<
2351+
"field %select{%1|in %1}0 requires explicit initialization but is not "
2352+
"explicitly initialized">, InGroup<UninitializedExplicitInit>;
23502353
def warn_field_is_uninit : Warning<"field %0 is uninitialized when used here">,
23512354
InGroup<Uninitialized>;
23522355
def warn_base_class_is_uninit : Warning<

clang/lib/AST/Decl.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5031,6 +5031,7 @@ RecordDecl::RecordDecl(Kind DK, TagKind TK, const ASTContext &C,
50315031
setHasNonTrivialToPrimitiveDefaultInitializeCUnion(false);
50325032
setHasNonTrivialToPrimitiveDestructCUnion(false);
50335033
setHasNonTrivialToPrimitiveCopyCUnion(false);
5034+
setHasUninitializedExplicitInitFields(false);
50345035
setParamDestroyedInCallee(false);
50355036
setArgPassingRestrictions(RecordArgPassingKind::CanPassInRegs);
50365037
setIsRandomized(false);
@@ -5231,9 +5232,10 @@ unsigned RecordDecl::getODRHash() {
52315232
// Only calculate hash on first call of getODRHash per record.
52325233
ODRHash Hash;
52335234
Hash.AddRecordDecl(this);
5234-
// For RecordDecl the ODRHash is stored in the remaining 26
5235-
// bit of RecordDeclBits, adjust the hash to accomodate.
5236-
setODRHash(Hash.CalculateHash() >> 6);
5235+
// For RecordDecl the ODRHash is stored in the remaining
5236+
// bits of RecordDeclBits, adjust the hash to accommodate.
5237+
static_assert(sizeof(Hash.CalculateHash()) * CHAR_BIT == 32);
5238+
setODRHash(Hash.CalculateHash() >> (32 - NumOdrHashBits));
52375239
return RecordDeclBits.ODRHash;
52385240
}
52395241

clang/lib/AST/DeclCXX.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "clang/AST/TypeLoc.h"
3030
#include "clang/AST/UnresolvedSet.h"
3131
#include "clang/Basic/Diagnostic.h"
32+
#include "clang/Basic/DiagnosticAST.h"
3233
#include "clang/Basic/IdentifierTable.h"
3334
#include "clang/Basic/LLVM.h"
3435
#include "clang/Basic/LangOptions.h"
@@ -457,6 +458,10 @@ CXXRecordDecl::setBases(CXXBaseSpecifier const * const *Bases,
457458
if (BaseClassDecl->hasMutableFields())
458459
data().HasMutableFields = true;
459460

461+
if (BaseClassDecl->hasUninitializedExplicitInitFields() &&
462+
BaseClassDecl->isAggregate())
463+
setHasUninitializedExplicitInitFields(true);
464+
460465
if (BaseClassDecl->hasUninitializedReferenceMember())
461466
data().HasUninitializedReferenceMember = true;
462467

@@ -1113,6 +1118,9 @@ void CXXRecordDecl::addedMember(Decl *D) {
11131118
} else if (!T.isCXX98PODType(Context))
11141119
data().PlainOldData = false;
11151120

1121+
if (Field->hasAttr<ExplicitInitAttr>())
1122+
setHasUninitializedExplicitInitFields(true);
1123+
11161124
if (T->isReferenceType()) {
11171125
if (!Field->hasInClassInitializer())
11181126
data().HasUninitializedReferenceMember = true;
@@ -1372,6 +1380,10 @@ void CXXRecordDecl::addedMember(Decl *D) {
13721380
if (!FieldRec->hasCopyAssignmentWithConstParam())
13731381
data().ImplicitCopyAssignmentHasConstParam = false;
13741382

1383+
if (FieldRec->hasUninitializedExplicitInitFields() &&
1384+
FieldRec->isAggregate())
1385+
setHasUninitializedExplicitInitFields(true);
1386+
13751387
if (FieldRec->hasUninitializedReferenceMember() &&
13761388
!Field->hasInClassInitializer())
13771389
data().HasUninitializedReferenceMember = true;
@@ -2188,6 +2200,33 @@ void CXXRecordDecl::completeDefinition(CXXFinalOverriderMap *FinalOverriders) {
21882200
for (conversion_iterator I = conversion_begin(), E = conversion_end();
21892201
I != E; ++I)
21902202
I.setAccess((*I)->getAccess());
2203+
2204+
ASTContext &Context = getASTContext();
2205+
2206+
if (isAggregate() && hasUserDeclaredConstructor() &&
2207+
!Context.getLangOpts().CPlusPlus20) {
2208+
// Diagnose any aggregate behavior changes in C++20
2209+
for (const FieldDecl *FD : fields()) {
2210+
if (const auto *AT = FD->getAttr<ExplicitInitAttr>())
2211+
Context.getDiagnostics().Report(
2212+
AT->getLocation(),
2213+
diag::warn_cxx20_compat_requires_explicit_init_non_aggregate)
2214+
<< AT << FD << Context.getRecordType(this);
2215+
}
2216+
}
2217+
2218+
if (!isAggregate() && hasUninitializedExplicitInitFields()) {
2219+
// Diagnose any fields that required explicit initialization in a
2220+
// non-aggregate type. (Note that the fields may not be directly in this
2221+
// type, but in a subobject. In such cases we don't emit diagnoses here.)
2222+
for (const FieldDecl *FD : fields()) {
2223+
if (const auto *AT = FD->getAttr<ExplicitInitAttr>())
2224+
Context.getDiagnostics().Report(AT->getLocation(),
2225+
diag::warn_attribute_needs_aggregate)
2226+
<< AT << Context.getRecordType(this);
2227+
}
2228+
setHasUninitializedExplicitInitFields(false);
2229+
}
21912230
}
21922231

21932232
bool CXXRecordDecl::mayBeAbstract() const {

clang/lib/Sema/SemaDecl.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19232,6 +19232,8 @@ void Sema::ActOnFields(Scope *S, SourceLocation RecLoc, Decl *EnclosingDecl,
1923219232
if (FT.hasNonTrivialToPrimitiveCopyCUnion() || Record->isUnion())
1923319233
Record->setHasNonTrivialToPrimitiveCopyCUnion(true);
1923419234
}
19235+
if (FD->hasAttr<ExplicitInitAttr>())
19236+
Record->setHasUninitializedExplicitInitFields(true);
1923519237
if (FT.isDestructedType()) {
1923619238
Record->setNonTrivialToPrimitiveDestroy(true);
1923719239
Record->setParamDestroyedInCallee(true);

clang/lib/Sema/SemaInit.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,13 @@ static void CheckStringInit(Expr *Str, QualType &DeclT, const ArrayType *AT,
264264
updateStringLiteralType(Str, DeclT);
265265
}
266266

267+
void emitUninitializedExplicitInitFields(Sema &S, const RecordDecl *R) {
268+
for (const FieldDecl *Field : R->fields()) {
269+
if (Field->hasAttr<ExplicitInitAttr>())
270+
S.Diag(Field->getLocation(), diag::note_entity_declared_at) << Field;
271+
}
272+
}
273+
267274
//===----------------------------------------------------------------------===//
268275
// Semantic checking for initializer lists.
269276
//===----------------------------------------------------------------------===//
@@ -738,6 +745,14 @@ void InitListChecker::FillInEmptyInitForField(unsigned Init, FieldDecl *Field,
738745
ILE->updateInit(SemaRef.Context, Init, Filler);
739746
return;
740747
}
748+
749+
if (!VerifyOnly && Field->hasAttr<ExplicitInitAttr>()) {
750+
SemaRef.Diag(ILE->getExprLoc(), diag::warn_field_requires_explicit_init)
751+
<< /* Var-in-Record */ 0 << Field;
752+
SemaRef.Diag(Field->getLocation(), diag::note_entity_declared_at)
753+
<< Field;
754+
}
755+
741756
// C++1y [dcl.init.aggr]p7:
742757
// If there are fewer initializer-clauses in the list than there are
743758
// members in the aggregate, then each member not explicitly initialized
@@ -4558,6 +4573,14 @@ static void TryConstructorInitialization(Sema &S,
45584573

45594574
CXXConstructorDecl *CtorDecl = cast<CXXConstructorDecl>(Best->Function);
45604575
if (Result != OR_Deleted) {
4576+
if (!IsListInit && Kind.getKind() == InitializationKind::IK_Default &&
4577+
DestRecordDecl != nullptr && DestRecordDecl->isAggregate() &&
4578+
DestRecordDecl->hasUninitializedExplicitInitFields()) {
4579+
S.Diag(Kind.getLocation(), diag::warn_field_requires_explicit_init)
4580+
<< /* Var-in-Record */ 1 << DestRecordDecl;
4581+
emitUninitializedExplicitInitFields(S, DestRecordDecl);
4582+
}
4583+
45614584
// C++11 [dcl.init]p6:
45624585
// If a program calls for the default initialization of an object
45634586
// of a const-qualified type T, T shall be a class type with a
@@ -5852,6 +5875,12 @@ static void TryOrBuildParenListInitialization(
58525875
} else {
58535876
// We've processed all of the args, but there are still members that
58545877
// have to be initialized.
5878+
if (!VerifyOnly && FD->hasAttr<ExplicitInitAttr>()) {
5879+
S.Diag(Kind.getLocation(), diag::warn_field_requires_explicit_init)
5880+
<< /* Var-in-Record */ 0 << FD;
5881+
S.Diag(FD->getLocation(), diag::note_entity_declared_at) << FD;
5882+
}
5883+
58555884
if (FD->hasInClassInitializer()) {
58565885
if (!VerifyOnly) {
58575886
// C++ [dcl.init]p16.6.2.2
@@ -6457,6 +6486,19 @@ void InitializationSequence::InitializeFrom(Sema &S,
64576486
}
64586487
}
64596488

6489+
if (!S.getLangOpts().CPlusPlus &&
6490+
Kind.getKind() == InitializationKind::IK_Default) {
6491+
RecordDecl *Rec = DestType->getAsRecordDecl();
6492+
if (Rec && Rec->hasUninitializedExplicitInitFields()) {
6493+
VarDecl *Var = dyn_cast_or_null<VarDecl>(Entity.getDecl());
6494+
if (Var && !Initializer) {
6495+
S.Diag(Var->getLocation(), diag::warn_field_requires_explicit_init)
6496+
<< /* Var-in-Record */ 1 << Rec;
6497+
emitUninitializedExplicitInitFields(S, Rec);
6498+
}
6499+
}
6500+
}
6501+
64606502
// - If the destination type is a reference type, see 8.5.3.
64616503
if (DestType->isReferenceType()) {
64626504
// C++0x [dcl.init.ref]p1:
@@ -7310,6 +7352,22 @@ PerformConstructorInitialization(Sema &S,
73107352
if (S.DiagnoseUseOfDecl(Step.Function.FoundDecl, Loc))
73117353
return ExprError();
73127354

7355+
if (Kind.getKind() == InitializationKind::IK_Value &&
7356+
Constructor->isImplicit()) {
7357+
auto *RD = Step.Type.getCanonicalType()->getAsCXXRecordDecl();
7358+
if (RD && RD->isAggregate() && RD->hasUninitializedExplicitInitFields()) {
7359+
unsigned I = 0;
7360+
for (const FieldDecl *FD : RD->fields()) {
7361+
if (I >= ConstructorArgs.size() && FD->hasAttr<ExplicitInitAttr>()) {
7362+
S.Diag(Loc, diag::warn_field_requires_explicit_init)
7363+
<< /* Var-in-Record */ 0 << FD;
7364+
S.Diag(FD->getLocation(), diag::note_entity_declared_at) << FD;
7365+
}
7366+
++I;
7367+
}
7368+
}
7369+
}
7370+
73137371
TypeSourceInfo *TSInfo = Entity.getTypeSourceInfo();
73147372
if (!TSInfo)
73157373
TSInfo = S.Context.getTrivialTypeSourceInfo(Entity.getType(), Loc);

clang/lib/Serialization/ASTReaderDecl.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@ RedeclarableResult ASTDeclReader::VisitRecordDeclImpl(RecordDecl *RD) {
832832
RecordDeclBits.getNextBit());
833833
RD->setHasNonTrivialToPrimitiveDestructCUnion(RecordDeclBits.getNextBit());
834834
RD->setHasNonTrivialToPrimitiveCopyCUnion(RecordDeclBits.getNextBit());
835+
RD->setHasUninitializedExplicitInitFields(RecordDeclBits.getNextBit());
835836
RD->setParamDestroyedInCallee(RecordDeclBits.getNextBit());
836837
RD->setArgPassingRestrictions(
837838
(RecordArgPassingKind)RecordDeclBits.getNextBits(/*Width=*/2));

clang/lib/Serialization/ASTWriterDecl.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ void ASTDeclWriter::VisitRecordDecl(RecordDecl *D) {
611611
RecordDeclBits.addBit(D->hasNonTrivialToPrimitiveDefaultInitializeCUnion());
612612
RecordDeclBits.addBit(D->hasNonTrivialToPrimitiveDestructCUnion());
613613
RecordDeclBits.addBit(D->hasNonTrivialToPrimitiveCopyCUnion());
614+
RecordDeclBits.addBit(D->hasUninitializedExplicitInitFields());
614615
RecordDeclBits.addBit(D->isParamDestroyedInCallee());
615616
RecordDeclBits.addBits(llvm::to_underlying(D->getArgPassingRestrictions()), 2);
616617
Record.push_back(RecordDeclBits);
@@ -2480,7 +2481,8 @@ void ASTWriter::WriteDeclAbbrevs() {
24802481
// isNonTrivialToPrimitiveCopy, isNonTrivialToPrimitiveDestroy,
24812482
// hasNonTrivialToPrimitiveDefaultInitializeCUnion,
24822483
// hasNonTrivialToPrimitiveDestructCUnion,
2483-
// hasNonTrivialToPrimitiveCopyCUnion, isParamDestroyedInCallee,
2484+
// hasNonTrivialToPrimitiveCopyCUnion,
2485+
// hasUninitializedExplicitInitFields, isParamDestroyedInCallee,
24842486
// getArgPassingRestrictions
24852487
// ODRHash
24862488
Abv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 26));

clang/test/Misc/pragma-attribute-supported-attributes-list.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
// CHECK-NEXT: EnumExtensibility (SubjectMatchRule_enum)
8080
// CHECK-NEXT: Error (SubjectMatchRule_function)
8181
// CHECK-NEXT: ExcludeFromExplicitInstantiation (SubjectMatchRule_variable, SubjectMatchRule_function, SubjectMatchRule_record)
82+
// CHECK-NEXT: ExplicitInit (SubjectMatchRule_field)
8283
// CHECK-NEXT: ExternalSourceSymbol ((SubjectMatchRule_record, SubjectMatchRule_enum, SubjectMatchRule_enum_constant, SubjectMatchRule_field, SubjectMatchRule_function, SubjectMatchRule_namespace, SubjectMatchRule_objc_category, SubjectMatchRule_objc_implementation, SubjectMatchRule_objc_interface, SubjectMatchRule_objc_method, SubjectMatchRule_objc_property, SubjectMatchRule_objc_protocol, SubjectMatchRule_record, SubjectMatchRule_type_alias, SubjectMatchRule_variable))
8384
// CHECK-NEXT: FlagEnum (SubjectMatchRule_enum)
8485
// CHECK-NEXT: Flatten (SubjectMatchRule_function)

0 commit comments

Comments
 (0)