enhance unusedfields plugin to find readonly fields

Change-Id: I4da97443fc7eb14fd94959a026ab45a9256c055f
Reviewed-on: https://gerrit.libreoffice.org/40158
Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
Tested-by: Noel Grandin <noel.grandin@collabora.co.uk>
This commit is contained in:
Noel Grandin
2017-07-18 20:09:31 +02:00
parent 2303d4f1a2
commit 32878b6857
7 changed files with 2482 additions and 51 deletions

View File

@@ -15,6 +15,7 @@
#include <algorithm>
#include "plugin.hxx"
#include "compat.hxx"
#include "check.hxx"
/**
This performs two analyses:
@@ -62,6 +63,7 @@ bool operator < (const MyFieldInfo &lhs, const MyFieldInfo &rhs)
static std::set<MyFieldInfo> touchedFromInsideSet;
static std::set<MyFieldInfo> touchedFromOutsideSet;
static std::set<MyFieldInfo> readFromSet;
static std::set<MyFieldInfo> writeToSet;
static std::set<MyFieldInfo> definitionSet;
@@ -79,15 +81,25 @@ public:
bool VisitFieldDecl( const FieldDecl* );
bool VisitMemberExpr( const MemberExpr* );
bool VisitDeclRefExpr( const DeclRefExpr* );
bool VisitCXXConstructorDecl( const CXXConstructorDecl* );
bool VisitVarDecl( const VarDecl* );
bool TraverseCXXConstructorDecl( CXXConstructorDecl* );
bool TraverseCXXMethodDecl( CXXMethodDecl* );
bool TraverseFunctionDecl( FunctionDecl* );
private:
MyFieldInfo niceName(const FieldDecl*);
void checkTouchedFromOutside(const FieldDecl* fieldDecl, const Expr* memberExpr);
void checkWriteOnly(const FieldDecl* fieldDecl, const Expr* memberExpr);
RecordDecl * insideMoveOrCopyDeclParent;
RecordDecl * insideStreamOutputOperator;
void checkReadOnly(const FieldDecl* fieldDecl, const Expr* memberExpr);
bool isSomeKindOfZero(const Expr* arg);
bool IsPassedByNonConstRef(const Stmt * child, const CallExpr * callExpr, const FunctionDecl * calleeFunctionDecl, bool bParamsAndArgsOffset);
RecordDecl * insideMoveOrCopyDeclParent;
RecordDecl * insideStreamOutputOperator;
// For reasons I do not understand, parentFunctionDecl() is not reliable, so
// we store the parent function on the way down the AST.
FunctionDecl * insideFunctionDecl;
};
void UnusedFields::run()
@@ -105,10 +117,10 @@ void UnusedFields::run()
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 : writeToSet)
output += "write:\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;
@@ -117,13 +129,17 @@ void UnusedFields::run()
else
{
for (const MyFieldInfo & s : readFromSet)
{
report(
DiagnosticsEngine::Warning,
"read %0",
s.parentRecord->getLocStart())
<< s.fieldName;
}
for (const MyFieldInfo & s : writeToSet)
report(
DiagnosticsEngine::Warning,
"write %0",
s.parentRecord->getLocStart())
<< s.fieldName;
}
}
@@ -182,10 +198,91 @@ bool UnusedFields::VisitFieldDecl( const FieldDecl* fieldDecl )
return true;
}
if (fieldDecl->getInClassInitializer() && !isSomeKindOfZero(fieldDecl->getInClassInitializer())) {
writeToSet.insert(niceName(fieldDecl));
}
definitionSet.insert(niceName(fieldDecl));
return true;
}
/**
Does the expression being used to initialise a field value evaluate to
the same as a default value?
*/
bool UnusedFields::isSomeKindOfZero(const Expr* arg)
{
assert(arg);
arg = arg->IgnoreParenCasts();
if (isa<CXXDefaultArgExpr>(arg)) {
arg = dyn_cast<CXXDefaultArgExpr>(arg)->getExpr();
}
arg = arg->IgnoreParenCasts();
// ignore this, it seems to trigger an infinite recursion
if (isa<UnaryExprOrTypeTraitExpr>(arg)) {
return false;
}
if (auto cxxConstructExpr = dyn_cast<CXXConstructExpr>(arg)) {
return cxxConstructExpr->getConstructor()->isDefaultConstructor();
}
APSInt x1;
if (arg->EvaluateAsInt(x1, compiler.getASTContext()))
{
return x1 == 0;
}
if (isa<CXXNullPtrLiteralExpr>(arg)) {
return true;
}
if (isa<MaterializeTemporaryExpr>(arg))
{
const CXXBindTemporaryExpr* strippedArg = dyn_cast_or_null<CXXBindTemporaryExpr>(arg->IgnoreParenCasts());
if (strippedArg)
{
auto temp = dyn_cast<CXXTemporaryObjectExpr>(strippedArg->getSubExpr());
if (temp->getNumArgs() == 0)
{
if (loplugin::TypeCheck(temp->getType()).Class("OUString").Namespace("rtl").GlobalNamespace()) {
return true;
}
if (loplugin::TypeCheck(temp->getType()).Class("OString").Namespace("rtl").GlobalNamespace()) {
return true;
}
return false;
}
}
}
// Get the expression contents.
// This helps us find params which are always initialised with something like "OUString()".
SourceManager& SM = compiler.getSourceManager();
SourceLocation startLoc = arg->getLocStart();
SourceLocation endLoc = arg->getLocEnd();
const char *p1 = SM.getCharacterData( startLoc );
const char *p2 = SM.getCharacterData( endLoc );
if (!p1 || !p2 || (p2 - p1) < 0 || (p2 - p1) > 40) {
return false;
}
unsigned n = Lexer::MeasureTokenLength( endLoc, SM, compiler.getLangOpts());
std::string s( p1, p2 - p1 + n);
// strip linefeed and tab characters so they don't interfere with the parsing of the log file
std::replace( s.begin(), s.end(), '\r', ' ');
std::replace( s.begin(), s.end(), '\n', ' ');
std::replace( s.begin(), s.end(), '\t', ' ');
// now normalize the value. For some params, like OUString, we can pass it as OUString() or "" and they are the same thing
if (s == "OUString()")
return true;
else if (s == "OString()")
return true;
else if (s == "aEmptyOUStr") //sw
return true;
else if (s == "EMPTY_OUSTRING")//sc
return true;
else if (s == "GetEmptyOUString()") //sc
return true;
return false;
}
static char easytolower(char in)
{
if (in<='Z' && in>='A')
@@ -217,8 +314,10 @@ bool UnusedFields::TraverseCXXMethodDecl(CXXMethodDecl* cxxMethodDecl)
if (cxxMethodDecl->isCopyAssignmentOperator() || cxxMethodDecl->isMoveAssignmentOperator())
insideMoveOrCopyDeclParent = cxxMethodDecl->getParent();
}
insideFunctionDecl = cxxMethodDecl;
bool ret = RecursiveASTVisitor::TraverseCXXMethodDecl(cxxMethodDecl);
insideMoveOrCopyDeclParent = nullptr;
insideFunctionDecl = nullptr;
return ret;
}
@@ -233,8 +332,10 @@ bool UnusedFields::TraverseFunctionDecl(FunctionDecl* functionDecl)
insideStreamOutputOperator = qt.getNonReferenceType().getUnqualifiedType()->getAsCXXRecordDecl();
}
}
insideFunctionDecl = functionDecl;
bool ret = RecursiveASTVisitor::TraverseFunctionDecl(functionDecl);
insideStreamOutputOperator = nullptr;
insideFunctionDecl = nullptr;
return ret;
}
@@ -258,6 +359,8 @@ bool UnusedFields::VisitMemberExpr( const MemberExpr* memberExpr )
checkWriteOnly(fieldDecl, memberExpr);
checkReadOnly(fieldDecl, memberExpr);
return true;
}
@@ -450,6 +553,292 @@ void UnusedFields::checkWriteOnly(const FieldDecl* fieldDecl, const Expr* member
readFromSet.insert(fieldInfo);
}
void UnusedFields::checkReadOnly(const FieldDecl* fieldDecl, const Expr* memberExpr)
{
if (insideMoveOrCopyDeclParent)
{
RecordDecl const * cxxRecordDecl1 = fieldDecl->getParent();
// we don't care about writes to a field when inside the copy/move constructor/operator= for that field
if (cxxRecordDecl1 && (cxxRecordDecl1 == insideMoveOrCopyDeclParent))
return;
}
auto parentsRange = compiler.getASTContext().getParents(*memberExpr);
const Stmt* child = memberExpr;
const Stmt* parent = parentsRange.begin() == parentsRange.end() ? nullptr : parentsRange.begin()->get<Stmt>();
// walk up the tree until we find something interesting
bool bPotentiallyWrittenTo = false;
bool bDump = false;
auto walkupUp = [&]() {
child = parent;
auto parentsRange = compiler.getASTContext().getParents(*parent);
parent = parentsRange.begin() == parentsRange.end() ? nullptr : parentsRange.begin()->get<Stmt>();
};
do
{
if (!parent)
{
break;
}
if (isa<CXXReinterpretCastExpr>(parent))
{
// once we see one of these, there is not much useful we can know
bPotentiallyWrittenTo = true;
break;
}
else if (isa<CastExpr>(parent) || isa<MemberExpr>(parent) || isa<ParenExpr>(parent) || isa<ParenListExpr>(parent)
#if CLANG_VERSION >= 40000
|| isa<ArrayInitLoopExpr>(parent)
#endif
|| isa<ExprWithCleanups>(parent))
{
walkupUp();
}
else if (auto unaryOperator = dyn_cast<UnaryOperator>(parent))
{
UnaryOperator::Opcode op = unaryOperator->getOpcode();
if (op == UO_AddrOf || op == UO_PostInc || op == UO_PostDec || op == UO_PreInc || op == UO_PreDec)
{
bPotentiallyWrittenTo = true;
break;
}
walkupUp();
}
else if (auto arraySubscriptExpr = dyn_cast<ArraySubscriptExpr>(parent))
{
if (arraySubscriptExpr->getIdx() == child)
break;
walkupUp();
}
else if (auto operatorCallExpr = dyn_cast<CXXOperatorCallExpr>(parent))
{
const FunctionDecl * calleeFunctionDecl = operatorCallExpr->getDirectCallee();
if (calleeFunctionDecl)
{
// if calling a non-const operator on the field
auto calleeMethodDecl = dyn_cast<CXXMethodDecl>(calleeFunctionDecl);
if (calleeMethodDecl
&& operatorCallExpr->getArg(0) == child && !calleeMethodDecl->isConst())
{
bPotentiallyWrittenTo = true;
break;
}
bool bParamsAndArgsOffset = calleeMethodDecl != nullptr;
if (IsPassedByNonConstRef(child, operatorCallExpr, calleeFunctionDecl, bParamsAndArgsOffset))
bPotentiallyWrittenTo = true;
}
else
bPotentiallyWrittenTo = true; // conservative, could improve
break;
}
else if (auto cxxMemberCallExpr = dyn_cast<CXXMemberCallExpr>(parent))
{
const CXXMethodDecl * calleeMethodDecl = cxxMemberCallExpr->getMethodDecl();
if (calleeMethodDecl)
{
// if calling a non-const method on the field
const Expr* tmp = dyn_cast<Expr>(child);
if (tmp->isBoundMemberFunction(compiler.getASTContext())) {
tmp = dyn_cast<MemberExpr>(tmp)->getBase();
}
if (cxxMemberCallExpr->getImplicitObjectArgument() == tmp
&& !calleeMethodDecl->isConst())
{
bPotentiallyWrittenTo = true;
break;
}
// check for being passed as parameter by non-const-reference
if (IsPassedByNonConstRef(child, cxxMemberCallExpr, calleeMethodDecl, false/*bParamsAndArgsOffset*/))
bPotentiallyWrittenTo = true;
}
else
bPotentiallyWrittenTo = true; // can happen in templates
break;
}
else if (auto cxxConstructExpr = dyn_cast<CXXConstructExpr>(parent))
{
const CXXConstructorDecl * cxxConstructorDecl = cxxConstructExpr->getConstructor();
// check for being passed as parameter by non-const-reference
unsigned len = std::min(cxxConstructExpr->getNumArgs(),
cxxConstructorDecl->getNumParams());
for (unsigned i = 0; i < len; ++i)
if (cxxConstructExpr->getArg(i) == child)
if (loplugin::TypeCheck(cxxConstructorDecl->getParamDecl(i)->getType()).NonConst().LvalueReference())
{
bPotentiallyWrittenTo = true;
break;
}
break;
}
else if (auto callExpr = dyn_cast<CallExpr>(parent))
{
const FunctionDecl * calleeFunctionDecl = callExpr->getDirectCallee();
if (calleeFunctionDecl) {
if (IsPassedByNonConstRef(child, callExpr, calleeFunctionDecl, false/*bParamsAndArgsOffset*/))
bPotentiallyWrittenTo = true;
} else
bPotentiallyWrittenTo = true; // conservative, could improve
break;
}
else if (auto binaryOp = dyn_cast<BinaryOperator>(parent))
{
BinaryOperator::Opcode op = binaryOp->getOpcode();
const bool assignmentOp = op == BO_Assign || op == BO_MulAssign
|| op == BO_DivAssign || op == BO_RemAssign || op == BO_AddAssign
|| op == BO_SubAssign || op == BO_ShlAssign || op == BO_ShrAssign
|| op == BO_AndAssign || op == BO_XorAssign || op == BO_OrAssign;
if (binaryOp->getLHS() == child && assignmentOp) {
bPotentiallyWrittenTo = true;
}
break;
}
else if (isa<ReturnStmt>(parent))
{
if (insideFunctionDecl && loplugin::TypeCheck(insideFunctionDecl->getReturnType()).NonConst().LvalueReference()) {
bPotentiallyWrittenTo = true;
}
break;
}
else if (isa<ConditionalOperator>(parent)
|| isa<SwitchStmt>(parent)
|| isa<DeclStmt>(parent)
|| isa<WhileStmt>(parent)
|| isa<CXXNewExpr>(parent)
|| isa<ForStmt>(parent)
|| isa<InitListExpr>(parent)
|| isa<CXXDependentScopeMemberExpr>(parent)
|| isa<UnresolvedMemberExpr>(parent)
|| isa<MaterializeTemporaryExpr>(parent)
|| isa<IfStmt>(parent)
|| isa<DoStmt>(parent)
|| isa<CXXDeleteExpr>(parent)
|| isa<UnaryExprOrTypeTraitExpr>(parent)
|| isa<CXXUnresolvedConstructExpr>(parent)
|| isa<CompoundStmt>(parent)
|| isa<LabelStmt>(parent)
|| isa<CXXForRangeStmt>(parent)
|| isa<CXXTypeidExpr>(parent)
|| isa<DefaultStmt>(parent))
{
break;
}
else
{
bPotentiallyWrittenTo = true;
bDump = true;
break;
}
} while (true);
if (bDump)
{
report(
DiagnosticsEngine::Warning,
"oh dear, what can the matter be? writtenTo=%0",
memberExpr->getLocStart())
<< bPotentiallyWrittenTo
<< memberExpr->getSourceRange();
if (parent)
{
report(
DiagnosticsEngine::Note,
"parent over here",
parent->getLocStart())
<< parent->getSourceRange();
parent->dump();
}
memberExpr->dump();
}
MyFieldInfo fieldInfo = niceName(fieldDecl);
if (bPotentiallyWrittenTo)
writeToSet.insert(fieldInfo);
}
bool UnusedFields::IsPassedByNonConstRef(const Stmt * child, const CallExpr * callExpr,
const FunctionDecl * calleeFunctionDecl,
bool bParamsAndArgsOffset)
{
unsigned len = std::min(callExpr->getNumArgs() + (bParamsAndArgsOffset ? 1 : 0),
calleeFunctionDecl->getNumParams());
for (unsigned i = 0; i < len; ++i)
if (callExpr->getArg(i + (bParamsAndArgsOffset ? 1 : 0)) == child)
if (loplugin::TypeCheck(calleeFunctionDecl->getParamDecl(i)->getType()).NonConst().LvalueReference())
return true;
return false;
}
// fields that are assigned via member initialisers do not get visited in VisitDeclRef, so
// have to do it here
bool UnusedFields::VisitCXXConstructorDecl( const CXXConstructorDecl* cxxConstructorDecl )
{
if (ignoreLocation( cxxConstructorDecl )) {
return true;
}
// ignore stuff that forms part of the stable URE interface
if (isInUnoIncludeFile(compiler.getSourceManager().getSpellingLoc(cxxConstructorDecl->getLocation()))) {
return true;
}
// templates make EvaluateAsInt crash inside clang
if (cxxConstructorDecl->isDependentContext())
return true;
// we don't care about writes to a field when inside the copy/move constructor/operator= for that field
if (insideMoveOrCopyDeclParent && cxxConstructorDecl->getParent() == insideMoveOrCopyDeclParent)
return true;
for(auto it = cxxConstructorDecl->init_begin(); it != cxxConstructorDecl->init_end(); ++it)
{
const CXXCtorInitializer* init = *it;
const FieldDecl* fieldDecl = init->getMember();
if (fieldDecl && init->getInit() && !isSomeKindOfZero(init->getInit()))
{
MyFieldInfo fieldInfo = niceName(fieldDecl);
writeToSet.insert(fieldInfo);
}
}
return true;
}
// Fields that are assigned via init-list-expr do not get visited in VisitDeclRef, so
// have to do it here.
// TODO could be more precise here about which fields are actually being written to
bool UnusedFields::VisitVarDecl( const VarDecl* varDecl)
{
if (!varDecl->getLocation().isValid() || ignoreLocation( varDecl ))
return true;
// ignore stuff that forms part of the stable URE interface
if (isInUnoIncludeFile(compiler.getSourceManager().getSpellingLoc(varDecl->getLocation())))
return true;
if (!varDecl->hasInit())
return true;
auto initListExpr = dyn_cast<InitListExpr>(varDecl->getInit()->IgnoreImplicit());
if (!initListExpr)
return true;
// If this is an array, navigate down until we hit a record.
// It appears to be somewhat painful to navigate down an array type structure reliably.
QualType varType = varDecl->getType().getDesugaredType(compiler.getASTContext());
while (varType->isArrayType() || varType->isConstantArrayType()
|| varType->isIncompleteArrayType() || varType->isVariableArrayType()
|| varType->isDependentSizedArrayType())
varType = varType->getAsArrayTypeUnsafe()->getElementType().getDesugaredType(compiler.getASTContext());
auto recordType = varType->getAs<RecordType>();
if (!recordType)
return true;
auto recordDecl = recordType->getDecl();
for (auto it = recordDecl->field_begin(); it != recordDecl->field_end(); ++it)
{
MyFieldInfo fieldInfo = niceName(*it);
writeToSet.insert(fieldInfo);
}
return true;
}
bool UnusedFields::VisitDeclRefExpr( const DeclRefExpr* declRefExpr )
{
const Decl* decl = declRefExpr->getDecl();