Improved unused private function check:

- Fixed #3628
- Added support for friend
Improved symbol database:
- friend scopes are now set
- Added findScopeByName function
Refactorizations:
- Removed some unnecessary "virtual" keywords
- Removed unnecessary _filename member variable, pass it as argument instead
- Made CppCheck::replaceAll static, since it is independant from a specific CppCheck instance, Pass string to be modified by reference
This commit is contained in:
PKEuS 2012-02-24 20:45:56 +01:00
parent 9f42ce91a1
commit 9a5f66030c
10 changed files with 188 additions and 94 deletions

View File

@ -57,7 +57,7 @@ public:
* given value is returned instead of default 0. * given value is returned instead of default 0.
* If no errors are found, 0 is returned. * If no errors are found, 0 is returned.
*/ */
virtual int check(int argc, const char* const argv[]); int check(int argc, const char* const argv[]);
/** /**
* Information about progress is directed here. This should be * Information about progress is directed here. This should be
@ -88,7 +88,7 @@ protected:
* Helper function to print out errors. Appends a line change. * Helper function to print out errors. Appends a line change.
* @param errmsg String printed to error stream * @param errmsg String printed to error stream
*/ */
virtual void reportErr(const std::string &errmsg); void reportErr(const std::string &errmsg);
/** /**
* @brief Parse command line args and get settings and file lists * @brief Parse command line args and get settings and file lists

View File

@ -508,6 +508,52 @@ void CheckClass::operatorEqVarError(const Token *tok, const std::string &classna
// ClassCheck: Unused private functions // ClassCheck: Unused private functions
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
static bool checkBaseFunctionsRec(const Scope* scope, std::list<const Token*>& FuncList, bool& whole)
{
for (std::vector<Scope::BaseInfo>::const_iterator i = scope->derivedFrom.begin(); i != scope->derivedFrom.end(); ++i) {
if (!i->scope || !i->scope->classStart) {
whole = false; // We need to see the complete definition of the class
return false;
}
for (std::list<Function>::const_iterator func = i->scope->functionList.begin(); func != i->scope->functionList.end(); ++func) {
if (func->isVirtual) { // Only virtual functions can be overriden
// Remove function from FuncList. TODO: Handle overloads
bool found = false;
std::list<const Token*>::iterator it = FuncList.begin();
while (it != FuncList.end()) {
if (func->token->str() == (*it)->str()) {
FuncList.erase(it++);
found = true;
} else
++it;
}
if (found)
return false;
}
}
if (!checkBaseFunctionsRec(i->scope, FuncList, whole))
return false;
}
return true;
}
static bool checkFunctionUsage(const std::string& name, const Scope* scope)
{
if (!scope)
return true; // Assume its used, if scope is not seen
for (std::list<Function>::const_iterator func = scope->functionList.begin(); func != scope->functionList.end(); ++func) {
if (func->start) {
for (const Token *ftok = func->start; ftok != func->start->link(); ftok = ftok->next()) {
if (ftok->str() == name && ftok->next()->str() == "(") // Function called. TODO: Handle overloads
return true;
}
}
}
return false; // Unused in this scope
}
void CheckClass::privateFunctions() void CheckClass::privateFunctions()
{ {
if (!_settings->isEnabled("style")) if (!_settings->isEnabled("style"))
@ -518,17 +564,11 @@ void CheckClass::privateFunctions()
if (_tokenizer->codeWithTemplates()) if (_tokenizer->codeWithTemplates())
return; return;
std::list<Scope>::const_iterator scope; for (std::list<Scope>::const_iterator scope = symbolDatabase->scopeList.begin(); scope != symbolDatabase->scopeList.end(); ++scope) {
for (scope = symbolDatabase->scopeList.begin(); scope != symbolDatabase->scopeList.end(); ++scope) {
// only check classes and structures // only check classes and structures
if (!scope->isClassOrStruct()) if (!scope->isClassOrStruct())
continue; continue;
// skip checking if there are friends
if (!scope->friendList.empty())
continue;
// dont check borland classes with properties.. // dont check borland classes with properties..
if (Token::findsimplematch(scope->classStart, "; __property ;", scope->classEnd)) if (Token::findsimplematch(scope->classStart, "; __property ;", scope->classEnd))
continue; continue;
@ -561,63 +601,36 @@ void CheckClass::privateFunctions()
} }
// Bailout for overriden virtual functions of base classes // Bailout for overriden virtual functions of base classes
if (!scope->derivedFrom.empty()) { if (!scope->derivedFrom.empty())
for (std::vector<Scope::BaseInfo>::const_iterator i = scope->derivedFrom.begin(); i != scope->derivedFrom.end(); ++i) { checkBaseFunctionsRec(&*scope, FuncList, whole);
if (!i->scope || !i->scope->classStart) {
whole = false; // We need to see the complete definition of the class
break;
}
for (std::list<Function>::const_iterator func = i->scope->functionList.begin(); func != i->scope->functionList.end(); ++func) {
if (func->isVirtual) { // Only virtual functions can be overriden
// Remove function from FuncList. TODO: Handle overloads
std::list<const Token *>::iterator it = FuncList.begin();
while (it != FuncList.end()) {
if (func->token->str() == (*it)->str())
FuncList.erase(it++);
else
++it;
}
}
}
}
}
if (!whole) if (!whole)
continue; continue;
// Check that all private functions are used.. while (!FuncList.empty()) {
for (std::list<Function>::const_iterator func = scope->functionList.begin(); func != scope->functionList.end(); ++func) { // Check that all private functions are used
if (func->start) { bool used = checkFunctionUsage(FuncList.front()->str(), &*scope); // Usage in this class
for (const Token *ftok = func->start; ftok != func->start->link(); ftok = ftok->next()) { // Check in friend classes
if (Token::Match(ftok, "%var% (")) { for (std::list<Scope::FriendInfo>::const_iterator i = scope->friendList.begin(); !used && i != scope->friendList.end(); ++i)
// Remove function from FuncList. TODO: Handle overloads used = checkFunctionUsage(FuncList.front()->str(), i->scope);
std::list<const Token *>::iterator it = FuncList.begin();
while (it != FuncList.end()) { if (!used) {
if (ftok->str() == (*it)->str()) // Final check; check if the function pointer is used somewhere..
FuncList.erase(it++); const std::string _pattern("return|throw|(|)|,|= &|" + FuncList.front()->str());
else
++it; // or if the function address is used somewhere...
} // eg. sigc::mem_fun(this, &className::classFunction)
} const std::string _pattern2("& " + scope->className + " :: " + FuncList.front()->str());
const std::string methodAsArgument("(|, " + scope->className + " :: " + FuncList.front()->str() + " ,|)");
const std::string methodAssigned("%var% = &| " + scope->className + " :: " + FuncList.front()->str());
if (!Token::findmatch(_tokenizer->tokens(), _pattern.c_str()) &&
!Token::findmatch(_tokenizer->tokens(), _pattern2.c_str()) &&
!Token::findmatch(_tokenizer->tokens(), methodAsArgument.c_str()) &&
!Token::findmatch(_tokenizer->tokens(), methodAssigned.c_str())) {
unusedPrivateFunctionError(FuncList.front(), scope->className, FuncList.front()->str());
} }
} }
}
while (!FuncList.empty()) {
// Final check; check if the function pointer is used somewhere..
const std::string _pattern("return|throw|(|)|,|= &|" + FuncList.front()->str());
// or if the function address is used somewhere...
// eg. sigc::mem_fun(this, &className::classFunction)
const std::string _pattern2("& " + scope->className + " :: " + FuncList.front()->str());
const std::string methodAsArgument("(|, " + scope->className + " :: " + FuncList.front()->str() + " ,|)");
const std::string methodAssigned("%var% = &| " + scope->className + " :: " + FuncList.front()->str());
if (!Token::findmatch(_tokenizer->tokens(), _pattern.c_str()) &&
!Token::findmatch(_tokenizer->tokens(), _pattern2.c_str()) &&
!Token::findmatch(_tokenizer->tokens(), methodAsArgument.c_str()) &&
!Token::findmatch(_tokenizer->tokens(), methodAssigned.c_str())) {
unusedPrivateFunctionError(FuncList.front(), scope->className, FuncList.front()->str());
}
FuncList.pop_front(); FuncList.pop_front();
} }
} }

View File

@ -67,28 +67,24 @@ const char * CppCheck::extraVersion()
unsigned int CppCheck::check(const std::string &path) unsigned int CppCheck::check(const std::string &path)
{ {
_filename = path; return processFile(path);
return processFile();
} }
unsigned int CppCheck::check(const std::string &path, const std::string &content) unsigned int CppCheck::check(const std::string &path, const std::string &content)
{ {
_filename = path;
_fileContent = content; _fileContent = content;
const unsigned int retval = processFile(); const unsigned int retval = processFile(path);
_fileContent.clear(); _fileContent.clear();
return retval; return retval;
} }
std::string CppCheck::replaceAll(std::string code, const std::string &from, const std::string &to) const void CppCheck::replaceAll(std::string& code, const std::string &from, const std::string &to)
{ {
size_t pos = 0; size_t pos = 0;
while ((pos = code.find(from, pos)) != std::string::npos) { while ((pos = code.find(from, pos)) != std::string::npos) {
code.replace(pos, from.length(), to); code.replace(pos, from.length(), to);
pos += to.length(); pos += to.length();
} }
return code;
} }
bool CppCheck::findError(std::string code, const char FileName[]) bool CppCheck::findError(std::string code, const char FileName[])
@ -126,8 +122,8 @@ bool CppCheck::findError(std::string code, const char FileName[])
// Add '\n' so that "\n#file" on first line would be found // Add '\n' so that "\n#file" on first line would be found
code = "// " + error + "\n" + code; code = "// " + error + "\n" + code;
code = replaceAll(code, "\n#file", "\n// #file"); replaceAll(code, "\n#file", "\n// #file");
code = replaceAll(code, "\n#endfile", "\n// #endfile"); replaceAll(code, "\n#endfile", "\n// #endfile");
// We have reduced the code as much as we can. Print out // We have reduced the code as much as we can. Print out
// the code and quit. // the code and quit.
@ -138,23 +134,23 @@ bool CppCheck::findError(std::string code, const char FileName[])
return true; return true;
} }
unsigned int CppCheck::processFile() unsigned int CppCheck::processFile(const std::string& filename)
{ {
exitcode = 0; exitcode = 0;
// only show debug warnings for C/C++ source files (don't fix // only show debug warnings for C/C++ source files (don't fix
// debug warnings for java/c#/etc files) // debug warnings for java/c#/etc files)
if (!Path::acceptFile(_filename)) if (!Path::acceptFile(filename))
_settings.debugwarnings = false; _settings.debugwarnings = false;
// TODO: Should this be moved out to its own function so all the files can be // TODO: Should this be moved out to its own function so all the files can be
// analysed before any files are checked? // analysed before any files are checked?
if (_settings.test_2_pass && _settings._jobs == 1) { if (_settings.test_2_pass && _settings._jobs == 1) {
const std::string printname = Path::toNativeSeparators(_filename); const std::string printname = Path::toNativeSeparators(filename);
reportOut("Analysing " + printname + "..."); reportOut("Analysing " + printname + "...");
std::ifstream f(_filename.c_str()); std::ifstream f(filename.c_str());
analyseFile(f, _filename); analyseFile(f, filename);
} }
_errout.str(""); _errout.str("");
@ -163,7 +159,7 @@ unsigned int CppCheck::processFile()
return exitcode; return exitcode;
if (_settings._errorsOnly == false) { if (_settings._errorsOnly == false) {
std::string fixedpath(_filename); std::string fixedpath(filename);
fixedpath = Path::simplifyPath(fixedpath.c_str()); fixedpath = Path::simplifyPath(fixedpath.c_str());
fixedpath = Path::toNativeSeparators(fixedpath); fixedpath = Path::toNativeSeparators(fixedpath);
_errorLogger.reportOut(std::string("Checking ") + fixedpath + std::string("...")); _errorLogger.reportOut(std::string("Checking ") + fixedpath + std::string("..."));
@ -177,12 +173,12 @@ unsigned int CppCheck::processFile()
if (!_fileContent.empty()) { if (!_fileContent.empty()) {
// File content was given as a string // File content was given as a string
std::istringstream iss(_fileContent); std::istringstream iss(_fileContent);
preprocessor.preprocess(iss, filedata, configurations, _filename, _settings._includePaths); preprocessor.preprocess(iss, filedata, configurations, filename, _settings._includePaths);
} else { } else {
// Only file name was given, read the content from file // Only file name was given, read the content from file
std::ifstream fin(_filename.c_str()); std::ifstream fin(filename.c_str());
Timer t("Preprocessor::preprocess", _settings._showtime, &S_timerResults); Timer t("Preprocessor::preprocess", _settings._showtime, &S_timerResults);
preprocessor.preprocess(fin, filedata, configurations, _filename, _settings._includePaths); preprocessor.preprocess(fin, filedata, configurations, filename, _settings._includePaths);
} }
if (_settings.checkConfiguration) { if (_settings.checkConfiguration) {
@ -200,7 +196,7 @@ unsigned int CppCheck::processFile()
// was used. // was used.
if (!_settings._force && checkCount >= _settings._maxConfigs) { if (!_settings._force && checkCount >= _settings._maxConfigs) {
const std::string fixedpath = Path::toNativeSeparators(_filename); const std::string fixedpath = Path::toNativeSeparators(filename);
ErrorLogger::ErrorMessage::FileLocation location; ErrorLogger::ErrorMessage::FileLocation location;
location.setfile(fixedpath); location.setfile(fixedpath);
std::list<ErrorLogger::ErrorMessage::FileLocation> loclist; std::list<ErrorLogger::ErrorMessage::FileLocation> loclist;
@ -225,12 +221,12 @@ unsigned int CppCheck::processFile()
cfg = *it; cfg = *it;
Timer t("Preprocessor::getcode", _settings._showtime, &S_timerResults); Timer t("Preprocessor::getcode", _settings._showtime, &S_timerResults);
const std::string codeWithoutCfg = preprocessor.getcode(filedata, *it, _filename); const std::string codeWithoutCfg = preprocessor.getcode(filedata, *it, filename);
t.Stop(); t.Stop();
// If only errors are printed, print filename after the check // If only errors are printed, print filename after the check
if (_settings._errorsOnly == false && it != configurations.begin()) { if (_settings._errorsOnly == false && it != configurations.begin()) {
std::string fixedpath = Path::simplifyPath(_filename.c_str()); std::string fixedpath = Path::simplifyPath(filename.c_str());
fixedpath = Path::toNativeSeparators(fixedpath); fixedpath = Path::toNativeSeparators(fixedpath);
_errorLogger.reportOut(std::string("Checking ") + fixedpath + ": " + cfg + std::string("...")); _errorLogger.reportOut(std::string("Checking ") + fixedpath + ": " + cfg + std::string("..."));
} }
@ -238,23 +234,23 @@ unsigned int CppCheck::processFile()
const std::string &appendCode = _settings.append(); const std::string &appendCode = _settings.append();
if (_settings.debugFalsePositive) { if (_settings.debugFalsePositive) {
if (findError(codeWithoutCfg + appendCode, _filename.c_str())) { if (findError(codeWithoutCfg + appendCode, filename.c_str())) {
return exitcode; return exitcode;
} }
} else { } else {
checkFile(codeWithoutCfg + appendCode, _filename.c_str()); checkFile(codeWithoutCfg + appendCode, filename.c_str());
} }
++checkCount; ++checkCount;
} }
} catch (std::runtime_error &e) { } catch (std::runtime_error &e) {
// Exception was thrown when checking this file.. // Exception was thrown when checking this file..
const std::string fixedpath = Path::toNativeSeparators(_filename); const std::string fixedpath = Path::toNativeSeparators(filename);
_errorLogger.reportOut("Bailing out from checking " + fixedpath + ": " + e.what()); _errorLogger.reportOut("Bailing out from checking " + fixedpath + ": " + e.what());
} }
if (!_settings._errorsOnly) if (!_settings._errorsOnly)
reportUnmatchedSuppressions(_settings.nomsg.getUnmatchedLocalSuppressions(_filename)); reportUnmatchedSuppressions(_settings.nomsg.getUnmatchedLocalSuppressions(filename));
_errorList.clear(); _errorList.clear();
return exitcode; return exitcode;

View File

@ -141,7 +141,7 @@ public:
private: private:
/** @brief Process one file. */ /** @brief Process one file. */
unsigned int processFile(); unsigned int processFile(const std::string& filename);
/** @brief Check file */ /** @brief Check file */
void checkFile(const std::string &code, const char FileName[]); void checkFile(const std::string &code, const char FileName[]);
@ -173,14 +173,13 @@ private:
* @brief Replace "from" strings with "to" strings in "code" * @brief Replace "from" strings with "to" strings in "code"
* and return it. * and return it.
*/ */
std::string replaceAll(std::string code, const std::string &from, const std::string &to) const; static void replaceAll(std::string& code, const std::string &from, const std::string &to);
unsigned int exitcode; unsigned int exitcode;
std::list<std::string> _errorList; std::list<std::string> _errorList;
std::ostringstream _errout; std::ostringstream _errout;
Settings _settings; Settings _settings;
bool _useGlobalSuppressions; bool _useGlobalSuppressions;
std::string _filename;
std::string _fileContent; std::string _fileContent;
std::set<std::string> _dependencies; std::set<std::string> _dependencies;

View File

@ -363,7 +363,7 @@ SymbolDatabase::SymbolDatabase(const Tokenizer *tokenizer, const Settings *setti
Scope::FriendInfo friendInfo; Scope::FriendInfo friendInfo;
friendInfo.name = tok->strAt(1) == "class" ? tok->strAt(2) : tok->strAt(1); friendInfo.name = tok->strAt(1) == "class" ? tok->strAt(2) : tok->strAt(1);
/** @todo fill this in later after parsing is complete */ // fill this in after parsing is complete
friendInfo.scope = 0; friendInfo.scope = 0;
scope->friendList.push_back(friendInfo); scope->friendList.push_back(friendInfo);
@ -546,6 +546,23 @@ SymbolDatabase::SymbolDatabase(const Tokenizer *tokenizer, const Settings *setti
} }
} }
// fill in friend info
for (it = scopeList.begin(); it != scopeList.end(); ++it) {
for (std::list<Scope::FriendInfo>::iterator i = it->friendList.begin(); i != it->friendList.end(); ++i) {
for (std::list<Scope>::iterator j = scopeList.begin(); j != scopeList.end(); ++j) {
// check scope for match
const Scope *scope = j->findQualifiedScope(i->name);
// found match?
if (scope && scope->isClassOrStruct()) {
// set found scope
i->scope = const_cast<Scope *>(scope);
break;
}
}
}
}
// fill in variable info // fill in variable info
for (it = scopeList.begin(); it != scopeList.end(); ++it) { for (it = scopeList.begin(); it != scopeList.end(); ++it) {
scope = &(*it); scope = &(*it);
@ -1651,8 +1668,6 @@ AccessControl Scope::defaultAccess() const
default: default:
return Local; return Local;
} }
return Public;
} }
// Get variable list.. // Get variable list..
@ -2032,6 +2047,17 @@ const Function *SymbolDatabase::findFunctionByToken(const Token *tok) const
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
const Scope* SymbolDatabase::findScopeByName(const std::string& name) const
{
for (std::list<Scope>::const_iterator it = scopeList.begin(); it != scopeList.end(); ++it) {
if (it->className == name)
return &*it;
}
return 0;
}
//---------------------------------------------------------------------------
Scope * Scope::findInNestedList(const std::string & name) Scope * Scope::findInNestedList(const std::string & name)
{ {
std::list<Scope *>::iterator it; std::list<Scope *>::iterator it;

View File

@ -552,6 +552,8 @@ public:
const Function *findFunctionByToken(const Token *tok) const; const Function *findFunctionByToken(const Token *tok) const;
const Scope* findScopeByName(const std::string& name) const;
bool argsMatch(const Scope *info, const Token *first, const Token *second, const std::string &path, unsigned int depth) const; bool argsMatch(const Scope *info, const Token *first, const Token *second, const std::string &path, unsigned int depth) const;
bool isClassOrStruct(const std::string &type) const { bool isClassOrStruct(const std::string &type) const {

View File

@ -39,7 +39,7 @@ class Settings;
class TemplateSimplifier { class TemplateSimplifier {
public: public:
TemplateSimplifier(); TemplateSimplifier();
virtual ~TemplateSimplifier(); ~TemplateSimplifier();
/** /**
* Used after simplifyTemplates to perform a little cleanup. * Used after simplifyTemplates to perform a little cleanup.

View File

@ -47,7 +47,7 @@ private:
public: public:
Tokenizer(); Tokenizer();
Tokenizer(const Settings * settings, ErrorLogger *errorLogger); Tokenizer(const Settings * settings, ErrorLogger *errorLogger);
virtual ~Tokenizer(); ~Tokenizer();
/** Returns the source file path. e.g. "file.cpp" */ /** Returns the source file path. e.g. "file.cpp" */
std::string getSourceFilePath() const; std::string getSourceFilePath() const;
@ -204,7 +204,7 @@ public:
/** /**
* get error messages that the tokenizer generate * get error messages that the tokenizer generate
*/ */
virtual void getErrorMessages(ErrorLogger *errorLogger, const Settings *settings) const; void getErrorMessages(ErrorLogger *errorLogger, const Settings *settings) const;
/** Simplify assignment in function call "f(x=g());" => "x=g();f(x);" /** Simplify assignment in function call "f(x=g());" => "x=g();f(x);"
*/ */

View File

@ -113,6 +113,8 @@ private:
TEST_CASE(hasMissingInlineClassFunctionReturningFunctionPointer); TEST_CASE(hasMissingInlineClassFunctionReturningFunctionPointer);
TEST_CASE(hasClassFunctionReturningFunctionPointer); TEST_CASE(hasClassFunctionReturningFunctionPointer);
TEST_CASE(classWithFriend);
TEST_CASE(parseFunctionCorrect); TEST_CASE(parseFunctionCorrect);
TEST_CASE(hasGlobalVariables1); TEST_CASE(hasGlobalVariables1);
@ -631,6 +633,25 @@ private:
} }
} }
void classWithFriend() {
GET_SYMBOL_DB("class Foo {}; class Bar1 { friend class Foo; }; class Bar2 { friend Foo; };")
// 3 scopes: Global, 3 classes
ASSERT(db && db->scopeList.size() == 4);
if (db) {
const Scope* foo = db->findScopeByName("Foo");
ASSERT(foo != 0);
const Scope* bar1 = db->findScopeByName("Bar1");
ASSERT(bar1 != 0);
const Scope* bar2 = db->findScopeByName("Bar2");
ASSERT(bar2 != 0);
if (foo && bar1 && bar2) {
ASSERT(bar1->friendList.size() == 1 && bar1->friendList.front().name == "Foo" && bar1->friendList.front().scope == foo);
ASSERT(bar2->friendList.size() == 1 && bar2->friendList.front().name == "Foo" && bar2->friendList.front().scope == foo);
}
}
}
void parseFunctionCorrect() { void parseFunctionCorrect() {
// ticket 3188 - "if" statement parsed as function // ticket 3188 - "if" statement parsed as function
GET_SYMBOL_DB("void func(i) int i; { if (i == 1) return; }\n") GET_SYMBOL_DB("void func(i) int i; { if (i == 1) return; }\n")

View File

@ -423,16 +423,53 @@ private:
" void bar() {}\n" // Don't skip if no function is overriden " void bar() {}\n" // Don't skip if no function is overriden
"};"); "};");
ASSERT_EQUALS("[test.cpp:9]: (style) Unused private function 'derived::bar'\n", errout.str()); ASSERT_EQUALS("[test.cpp:9]: (style) Unused private function 'derived::bar'\n", errout.str());
check("class Base {\n"
"private:\n"
" virtual void func() = 0;\n"
"public:\n"
" void public_func() {\n"
" func();\n"
" };\n"
"};\n"
"\n"
"class Derived1: public Base {\n"
"private:\n"
" void func() {}\n"
"};\n"
"class Derived2: public Derived1 {\n"
"private:\n"
" void func() {}\n"
"};");
ASSERT_EQUALS("", errout.str());
} }
void friendClass() { void friendClass() {
// ticket #2459 - friend class // ticket #2459 - friend class
check("class Foo {\n" check("class Foo {\n"
"private:\n"
" friend Bar;\n" // Unknown friend class
" void f() { }\n"
"};");
ASSERT_EQUALS("", errout.str());
check("struct Bar {\n"
" void g() { f(); }\n" // Friend class seen, but f() used in it
"};\n"
"class Foo {\n"
"private:\n" "private:\n"
" friend Bar;\n" " friend Bar;\n"
" void f() { }\n" " void f() { }\n"
"};"); "};");
ASSERT_EQUALS("", errout.str()); ASSERT_EQUALS("", errout.str());
check("class Bar {\n" // Friend class seen, f() not used in it
"};\n"
"class Foo {\n"
" friend Bar;\n"
" void f() { }\n"
"};");
ASSERT_EQUALS("[test.cpp:5]: (style) Unused private function 'Foo::f'\n", errout.str());
} }
void borland() { void borland() {