/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // A more aggressive check for unused C struct/C++ class members than what plain Clang offers. On // the one hand, unlike -Wunused-private-field, it warns about all members regardless of access // specifiers, if all code that can use a class has been seen. On the other hand, it warns about // all kinds of members. But it uses some heuristics (the type showing up in sizeof, alignof, // offsetof, certain casts) to determine that seemingly unused data members are probably used after // all; the used heuristics were enough to not require any explicit [[maybe_unused]] decorations // across the existing code base. #include #include #include "config_clang.h" #include "check.hxx" #include "compat.hxx" #include "plugin.hxx" namespace { // Whether the CXXRecordDecl itself or one of its enclosing classes is a template: bool isTemplated(CXXRecordDecl const* decl) { if (decl->getDescribedClassTemplate() != nullptr) { return true; } if (auto const d = dyn_cast(decl->getParent())) { return isTemplated(d); } return false; } bool isWarnUnusedType(QualType type) { if (auto const t = type->getAs()) { if (t->getDecl()->hasAttr()) { return true; } } return loplugin::isExtraWarnUnusedType(type); } class UnusedMember final : public loplugin::FilteringPlugin { public: explicit UnusedMember(loplugin::InstantiationData const& data) : FilteringPlugin(data) { } #if CLANG_VERSION < 60000 bool TraverseAlignedAttr(AlignedAttr* attr) { bool ret = FilteringPlugin::TraverseAlignedAttr(attr); PostTraverseAlignedAttr(attr, ret); return ret; } bool PostTraverseAlignedAttr(AlignedAttr* attr, bool run) { if (!run) { return false; } if (attr->isAlignmentExpr()) { if (!TraverseStmt(attr->getAlignmentExpr())) { return false; } } else if (auto const tsi = attr->getAlignmentType()) { if (!TraverseTypeLoc(tsi->getTypeLoc())) { return false; } } return true; } #endif bool VisitCXXRecordDecl(CXXRecordDecl const* decl) //TODO: non-CXX RecordDecl? { if (ignoreLocation(decl)) { return true; } if (!decl->isThisDeclarationADefinition()) { return true; } if (!handler.isAllRelevantCodeDefined(decl)) { return true; } if (!compiler.getSourceManager().isInMainFile(decl->getLocation())) { // include/rtl/instance.hxx declares entities in an unnamed namespace return true; } if (isTemplated(decl) || isa(decl)) { return true; } if (decl->isUnion() && decl->getIdentifier() == nullptr) { return true; //TODO } for (auto i = decl->decls_begin(); i != decl->decls_end(); ++i) { auto const d = *i; if (d->isImplicit() || isa(d) || isa(d)) { //TODO: only filter out UsingDecls that are actually used (if only to silence // -Woverloaded-virtual) continue; } if (isa(d) || isa(d)) { //TODO: only filter out ones that are not instantiated at all continue; } if (auto const d1 = dyn_cast(d)) { //TODO: determine whether the friendship is actually required auto const d2 = d1->getFriendDecl(); if (d2 == nullptr) { // happens for "friend class C;" continue; } if (auto const d3 = dyn_cast(d2)) { #if 0 //TODO: friend function definitions are not marked as referenced even if used? if (!d3->isThisDeclarationADefinition()) //TODO: do this check for all kinds? #endif { continue; } } } if (d->isReferenced()) { continue; } if (d->hasAttr()) { continue; } // Check individual members instead of the whole CXXRecordDecl for coming from a macro, // as CppUnit's CPPUNIT_TEST_SUITE_END (cppunit/extensions/HelperMacros.h) contains a // partial member list ending in // // private: /* dummy typedef so that the macro can still end with ';'*/ // typedef int CppUnitDummyTypedefForSemiColonEnding__ // if (compiler.getSourceManager().isMacroBodyExpansion(d->getLocation())) { return true; } if (auto const d1 = dyn_cast(d)) { if (d1->isUnnamedBitfield()) { continue; } if (!isWarnWhenUnusedType(d1->getType())) { continue; } deferred_.insert(d1); continue; } if (auto const d1 = dyn_cast(d)) { if (d1->isDeletedAsWritten()) // TODO: just isDeleted? { continue; } if (d1->isExplicitlyDefaulted()) { continue; } } else if (auto const d2 = dyn_cast(d)) { if (d2->getIdentifier() == nullptr) { continue; } if (isa(d2)) { deferred_.insert(d2); continue; } } else if (auto const d3 = dyn_cast(d)) { // Some types, like (specializations of) std::iterator_traits, have specific // requirements on their members; only covers std::iterator_traits for now (TODO: // check that at least some member is actually used) // (isa(decl) is already filtered out // above): if (isa(decl) && loplugin::DeclCheck(decl).Struct("iterator_traits").StdNamespace()) { auto const id = d3->getIdentifier(); assert(id != nullptr); auto const n = id->getName(); if (n == "difference_type" || n == "iterator_category" || n == "pointer" || n == "reference" || n == "value_type") { continue; } } } report(DiagnosticsEngine::Warning, "unused class member", d->getLocation()) << d->getSourceRange(); } return true; } bool VisitOffsetOfExpr(OffsetOfExpr const* expr) { if (ignoreLocation(expr)) { return true; } auto const t1 = expr->getTypeSourceInfo()->getType(); RecordDecl const* d; if (auto const t2 = t1->getAs()) { d = t2->getDecl(); } else { d = t1->castAs()->getDecl(); } recordRecordDeclAndBases(d); return true; } bool VisitUnaryExprOrTypeTraitExpr(UnaryExprOrTypeTraitExpr const* expr) { if (ignoreLocation(expr)) { return true; } switch (expr->getKind()) { case UETT_SizeOf: case UETT_AlignOf: #if CLANG_VERSION >= 80000 case UETT_PreferredAlignOf: #endif break; default: return true; } if (!expr->isArgumentType()) { return true; } auto t = expr->getArgumentType(); if (auto const t1 = t->getAs()) { t = t1->getPointeeType(); } if (auto const t1 = t->getAsArrayTypeUnsafe()) { t = compiler.getASTContext().getBaseElementType(t1); } if (auto const t1 = t->getAs()) { recordRecordDeclAndBases(t1->getDecl()); } return true; } // Handling implicit, C-style, static and reinterpret casts between void* and record types // (though reinterpret_cast would be ruled out by loplugin:redundantcast): bool VisitCastExpr(CastExpr const* expr) { if (ignoreLocation(expr)) { return true; } auto const t1 = expr->getType(); auto const t2 = compat::getSubExprAsWritten(expr)->getType(); if (loplugin::TypeCheck(t1).Pointer().Void()) { recordCastedRecordDecl(t2); } else if (loplugin::TypeCheck(t2).Pointer().Void()) { recordCastedRecordDecl(t1); } return true; } bool VisitCXXReinterpretCastExpr(CXXReinterpretCastExpr const* expr) { if (ignoreLocation(expr)) { return true; } recordCastedRecordDecl(expr->getTypeAsWritten()); recordCastedRecordDecl(expr->getSubExprAsWritten()->getType()); return true; } bool VisitElaboratedTypeLoc(ElaboratedTypeLoc tloc) { if (ignoreLocation(tloc)) { return true; } auto const tl = tloc.getNamedTypeLoc().getAs(); if (tl.isNull()) { return true; } if (tl.isDefinition()) { return true; } if (auto const d = dyn_cast(tl.getDecl())) { // For some reason, using an elaborated type specifier in (at least) a FieldDecl, as in // // enum E { ... }; // enum E e; // // doesn't cause the EnumDecl to be marked as referenced. (This should fix it, but note // the warning at : // "[...] a type written 'struct foo' should be represented as an ElaboratedTypeLoc. We // currently only do that when C++ is enabled [...]" deferred_.erase(d->getCanonicalDecl()); } return true; } void postRun() override { for (auto const d : deferred_) { if (auto const d1 = dyn_cast(d)) { bool layout = false; for (auto d2 = d1->getParent();;) { if (layout_.find(d2->getCanonicalDecl()) != layout_.end()) { layout = true; break; } // Heuristic to recursively check parent RecordDecl if given RecordDecl is // unnamed and either an anonymous struct (or union, but which are already // filtered out anyway), or defined in a non-static data member declaration // (TODO: which is erroneously approximated here with getTypedefNameForAnonDecl // for now, which fails to filter out RecordDecls in static data member // declarations): if (!(d2->getDeclName().isEmpty() && (d2->isAnonymousStructOrUnion() || d2->getTypedefNameForAnonDecl() == nullptr))) { break; } d2 = dyn_cast(d2->getParent()); if (d2 == nullptr) { break; } } if (layout) { continue; } } report(DiagnosticsEngine::Warning, "unused class member", d->getLocation()) << d->getSourceRange(); } } private: void run() override { if (TraverseDecl(compiler.getASTContext().getTranslationUnitDecl())) { postRun(); } } bool isWarnWhenUnusedType(QualType type) { auto t = type; if (auto const t1 = t->getAs()) { t = t1->getPointeeType(); } return t.isTrivialType(compiler.getASTContext()) || isWarnUnusedType(t); } void recordRecordDeclAndBases(RecordDecl const* decl) { if (!layout_.insert(decl->getCanonicalDecl()).second) { return; } if (auto const d2 = dyn_cast_or_null(decl->getDefinition())) { for (auto i = d2->bases_begin(); i != d2->bases_end(); ++i) { recordRecordDeclAndBases(i->getType()->castAs()->getDecl()); } //TODO: doesn't iterate vbases, but presence of such would run counter to the layout // heuristic anyway } } void recordCastedRecordDecl(QualType type) { for (auto t = type;;) { if (auto const t1 = t->getAs()) { t = t1->getPointeeType(); continue; } if (auto const t1 = t->getAs()) { recordRecordDeclAndBases(t1->getDecl()); } break; } } // RecordDecls whose layout (i.e., contained FieldDecls) must presumably not be changed: std::set layout_; std::set deferred_; }; loplugin::Plugin::Registration unusedmember("unusedmember"); } /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */