/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * 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/. */ #include #include #include #include #include #include #include "plugin.hxx" #include "compat.hxx" /** This performs two analyses: (1) look for unused fields (2) look for fields that are write-only We dmp a list of calls to methods, and a list of field definitions. Then we will post-process the 2 lists and find the set of unused methods. Be warned that it produces around 5G of log file. The process goes something like this: $ make check $ make FORCE_COMPILE_ALL=1 COMPILER_PLUGIN_TOOL='unusedfields' check $ ./compilerplugins/clang/unusedfields.py and then $ for dir in *; do make FORCE_COMPILE_ALL=1 UPDATE_FILES=$dir COMPILER_PLUGIN_TOOL='unusedfieldsremove' $dir; done to auto-remove the method declarations Note that the actual process may involve a fair amount of undoing, hand editing, and general messing around to get it to work :-) */ namespace { struct MyFieldInfo { const RecordDecl* parentRecord; std::string parentClass; std::string fieldName; std::string fieldType; std::string sourceLocation; std::string access; }; bool operator < (const MyFieldInfo &lhs, const MyFieldInfo &rhs) { return std::tie(lhs.parentClass, lhs.fieldName) < std::tie(rhs.parentClass, rhs.fieldName); } // try to limit the voluminous output a little static std::set touchedFromInsideSet; static std::set touchedFromConstructorSet; static std::set touchedFromOutsideSet; static std::set readFromSet; static std::set definitionSet; class UnusedFields: public RecursiveASTVisitor, public loplugin::Plugin { public: explicit UnusedFields(InstantiationData const & data): Plugin(data) {} virtual void run() override; bool shouldVisitTemplateInstantiations () const { return true; } bool shouldVisitImplicitCode() const { return true; } bool VisitFieldDecl( const FieldDecl* ); bool VisitMemberExpr( const MemberExpr* ); bool VisitDeclRefExpr( const DeclRefExpr* ); private: MyFieldInfo niceName(const FieldDecl*); void checkTouched(const FieldDecl* fieldDecl, const Expr* memberExpr); }; void UnusedFields::run() { TraverseDecl(compiler.getASTContext().getTranslationUnitDecl()); if (!isUnitTestMode()) { // dump all our output in one write call - this is to try and limit IO "crosstalk" between multiple processes // writing to the same logfile std::string output; for (const MyFieldInfo & s : touchedFromInsideSet) output += "inside:\t" + s.parentClass + "\t" + s.fieldName + "\n"; for (const MyFieldInfo & s : touchedFromConstructorSet) output += "constructor:\t" + s.parentClass + "\t" + s.fieldName + "\n"; for (const MyFieldInfo & s : touchedFromOutsideSet) output += "outside:\t" + s.parentClass + "\t" + s.fieldName + "\n"; for (const MyFieldInfo & s : readFromSet) output += "read:\t" + s.parentClass + "\t" + s.fieldName + "\n"; for (const MyFieldInfo & s : definitionSet) { output += "definition:\t" + s.access + "\t" + s.parentClass + "\t" + s.fieldName + "\t" + s.fieldType + "\t" + s.sourceLocation + "\n"; } std::ofstream myfile; myfile.open( SRCDIR "/loplugin.unusedfields.log", std::ios::app | std::ios::out); myfile << output; myfile.close(); } else { for (const MyFieldInfo & s : readFromSet) { report( DiagnosticsEngine::Warning, "read %0", s.parentRecord->getLocStart()) << s.fieldName; } } } MyFieldInfo UnusedFields::niceName(const FieldDecl* fieldDecl) { MyFieldInfo aInfo; const RecordDecl* recordDecl = fieldDecl->getParent(); if (const CXXRecordDecl* cxxRecordDecl = dyn_cast(recordDecl)) { if (cxxRecordDecl->getTemplateInstantiationPattern()) cxxRecordDecl = cxxRecordDecl->getTemplateInstantiationPattern(); aInfo.parentRecord = cxxRecordDecl; aInfo.parentClass = cxxRecordDecl->getQualifiedNameAsString(); } else { aInfo.parentRecord = recordDecl; aInfo.parentClass = recordDecl->getQualifiedNameAsString(); } aInfo.fieldName = fieldDecl->getNameAsString(); // sometimes the name (if it's an anonymous thing) contains the full path of the build folder, which we don't need size_t idx = aInfo.fieldName.find(SRCDIR); if (idx != std::string::npos) { aInfo.fieldName = aInfo.fieldName.replace(idx, strlen(SRCDIR), ""); } aInfo.fieldType = fieldDecl->getType().getAsString(); SourceLocation expansionLoc = compiler.getSourceManager().getExpansionLoc( fieldDecl->getLocation() ); StringRef name = compiler.getSourceManager().getFilename(expansionLoc); aInfo.sourceLocation = std::string(name.substr(strlen(SRCDIR)+1)) + ":" + std::to_string(compiler.getSourceManager().getSpellingLineNumber(expansionLoc)); normalizeDotDotInFilePath(aInfo.sourceLocation); switch (fieldDecl->getAccess()) { case AS_public: aInfo.access = "public"; break; case AS_private: aInfo.access = "private"; break; case AS_protected: aInfo.access = "protected"; break; default: aInfo.access = "unknown"; break; } return aInfo; } bool UnusedFields::VisitFieldDecl( const FieldDecl* fieldDecl ) { fieldDecl = fieldDecl->getCanonicalDecl(); if (ignoreLocation( fieldDecl )) { return true; } // ignore stuff that forms part of the stable URE interface if (isInUnoIncludeFile(compiler.getSourceManager().getSpellingLoc(fieldDecl->getLocation()))) { return true; } definitionSet.insert(niceName(fieldDecl)); return true; } static char easytolower(char in) { if (in<='Z' && in>='A') return in-('Z'-'z'); return in; } bool startswith(const std::string& rStr, const char* pSubStr) { return rStr.compare(0, strlen(pSubStr), pSubStr) == 0; } bool UnusedFields::VisitMemberExpr( const MemberExpr* memberExpr ) { const ValueDecl* decl = memberExpr->getMemberDecl(); const FieldDecl* fieldDecl = dyn_cast(decl); if (!fieldDecl) { return true; } fieldDecl = fieldDecl->getCanonicalDecl(); if (ignoreLocation(fieldDecl)) { return true; } // ignore stuff that forms part of the stable URE interface if (isInUnoIncludeFile(compiler.getSourceManager().getSpellingLoc(fieldDecl->getLocation()))) { return true; } MyFieldInfo fieldInfo = niceName(fieldDecl); // for the touched-from-outside analysis checkTouched(fieldDecl, memberExpr); // for the write-only analysis auto parentsRange = compiler.getASTContext().getParents(*memberExpr); const Stmt* child = memberExpr; const Stmt* parent = parentsRange.begin() == parentsRange.end() ? nullptr : parentsRange.begin()->get(); // walk up the tree until we find something interesting bool bPotentiallyReadFrom = false; bool bDump = false; do { if (!parent) { // check if we're inside a CXXCtorInitializer or a VarDecl auto parentsRange = compiler.getASTContext().getParents(*child); if ( parentsRange.begin() != parentsRange.end()) { const Decl* decl = parentsRange.begin()->get(); if (decl && (isa(decl) || isa(decl))) bPotentiallyReadFrom = true; } if (!bPotentiallyReadFrom) return true; break; } if (isa(parent) || isa(parent) || isa(parent) || isa(parent) #if CLANG_VERSION >= 40000 || isa(parent) #endif || isa(parent)) { child = parent; auto parentsRange = compiler.getASTContext().getParents(*parent); parent = parentsRange.begin() == parentsRange.end() ? nullptr : parentsRange.begin()->get(); } else if (auto unaryOperator = dyn_cast(parent)) { UnaryOperator::Opcode op = unaryOperator->getOpcode(); if (op == UO_AddrOf || op == UO_Deref || op == UO_Plus || op == UO_Minus || op == UO_Not || op == UO_LNot || op == UO_PreInc || op == UO_PostInc || op == UO_PreDec || op == UO_PostDec) { bPotentiallyReadFrom = true; break; } child = parent; auto parentsRange = compiler.getASTContext().getParents(*parent); parent = parentsRange.begin() == parentsRange.end() ? nullptr : parentsRange.begin()->get(); } else if (auto caseStmt = dyn_cast(parent)) { bPotentiallyReadFrom = caseStmt->getLHS() == child || caseStmt->getRHS() == child; break; } else if (auto ifStmt = dyn_cast(parent)) { bPotentiallyReadFrom = ifStmt->getCond() == child; break; } else if (auto doStmt = dyn_cast(parent)) { bPotentiallyReadFrom = doStmt->getCond() == child; break; } else if (auto callExpr = dyn_cast(parent)) { // check for calls to ReadXXX() type methods and the operator>>= methods on Any. const FunctionDecl * calleeFunctionDecl = callExpr->getDirectCallee(); if (calleeFunctionDecl && calleeFunctionDecl->getIdentifier()) { std::string name = calleeFunctionDecl->getNameAsString(); std::transform(name.begin(), name.end(), name.begin(), easytolower); if (startswith(name, "read") || name.find(">>=") != std::string::npos) // this is a write-only call ; else if (name == "clear" || name == "dispose" || name == "clearAndDispose" || name == "swap") // we're abusing the write-only analysis here to look for fields which don't have anything useful // being done to them, so we're ignoring things like std::vector::clear, std::vector::swap, // and VclPtr::clearAndDispose ; else bPotentiallyReadFrom = true; } else bPotentiallyReadFrom = true; break; } else if (auto binaryOp = dyn_cast(parent)) { BinaryOperator::Opcode op = binaryOp->getOpcode(); // If the child is on the LHS and it is an assignment, we are obviously not reading from it if (!(binaryOp->getLHS() == child && op == BO_Assign)) { bPotentiallyReadFrom = true; } break; } else if (isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent)) { bPotentiallyReadFrom = true; break; } else if (isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent) || isa(parent)) { break; } else { bPotentiallyReadFrom = true; bDump = true; break; } } while (true); if (bDump) { report( DiagnosticsEngine::Warning, "oh dear, what can the matter be?", memberExpr->getLocStart()) << memberExpr->getSourceRange(); report( DiagnosticsEngine::Note, "parent over here", parent->getLocStart()) << parent->getSourceRange(); parent->dump(); memberExpr->dump(); } if (bPotentiallyReadFrom) readFromSet.insert(fieldInfo); return true; } bool UnusedFields::VisitDeclRefExpr( const DeclRefExpr* declRefExpr ) { const Decl* decl = declRefExpr->getDecl(); const FieldDecl* fieldDecl = dyn_cast(decl); if (!fieldDecl) { return true; } fieldDecl = fieldDecl->getCanonicalDecl(); if (ignoreLocation(fieldDecl)) { return true; } // ignore stuff that forms part of the stable URE interface if (isInUnoIncludeFile(compiler.getSourceManager().getSpellingLoc(fieldDecl->getLocation()))) { return true; } checkTouched(fieldDecl, declRefExpr); return true; } void UnusedFields::checkTouched(const FieldDecl* fieldDecl, const Expr* memberExpr) { const FunctionDecl* memberExprParentFunction = parentFunctionDecl(memberExpr); const CXXMethodDecl* methodDecl = dyn_cast_or_null(memberExprParentFunction); MyFieldInfo fieldInfo = niceName(fieldDecl); // it's touched from somewhere outside a class if (!methodDecl) { touchedFromOutsideSet.insert(fieldInfo); return; } auto constructorDecl = dyn_cast(methodDecl); if (methodDecl->isCopyAssignmentOperator() || methodDecl->isMoveAssignmentOperator()) { // ignore move/copy operator, it's self->self } else if (constructorDecl && (constructorDecl->isCopyConstructor() || constructorDecl->isMoveConstructor())) { // ignore move/copy constructor, it's self->self } else if (constructorDecl && memberExprParentFunction->getParent() == fieldDecl->getParent()) { // if the field is touched from inside it's parent class constructor touchedFromConstructorSet.insert(fieldInfo); } else { if (memberExprParentFunction->getParent() == fieldDecl->getParent()) { touchedFromInsideSet.insert(fieldInfo); } else { touchedFromOutsideSet.insert(fieldInfo); } } } loplugin::Plugin::Registration< UnusedFields > X("unusedfields", false); } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */