diff --git a/src/Clazy.cpp b/src/Clazy.cpp index 7f21afb..06ebdcd 100644 --- a/src/Clazy.cpp +++ b/src/Clazy.cpp @@ -1,418 +1,413 @@ /* This file is part of the clazy static checker. Copyright (C) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: SĂ©rgio Martins Copyright (C) 2015-2017 Sergio Martins This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Utils.h" #include "Clazy.h" #include "clazy_stl.h" #include "checkbase.h" #include "AccessSpecifierManager.h" #include "SourceCompatibilityHelpers.h" #include "FixItExporter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace clang; using namespace std; using namespace clang::ast_matchers; static void manuallyPopulateParentMap(ParentMap *map, Stmt *s) { if (!s) return; for (Stmt *child : s->children()) { llvm::errs() << "Patching " << child->getStmtClassName() << "\n"; map->setParent(child, s); manuallyPopulateParentMap(map, child); } } ClazyASTConsumer::ClazyASTConsumer(ClazyContext *context) : m_context(context) { #ifndef CLAZY_DISABLE_AST_MATCHERS m_matchFinder = new clang::ast_matchers::MatchFinder(); #endif } void ClazyASTConsumer::addCheck(const std::pair &check) { CheckBase *checkBase = check.first; #ifndef CLAZY_DISABLE_AST_MATCHERS checkBase->registerASTMatchers(*m_matchFinder); #endif //m_createdChecks.push_back(checkBase); const RegisteredCheck &rcheck = check.second; if (rcheck.options & RegisteredCheck::Option_VisitsStmts) m_checksToVisitStmts.push_back(checkBase); if (rcheck.options & RegisteredCheck::Option_VisitsDecls) m_checksToVisitDecls.push_back(checkBase); } ClazyASTConsumer::~ClazyASTConsumer() { #ifndef CLAZY_DISABLE_AST_MATCHERS delete m_matchFinder; #endif delete m_context; } bool ClazyASTConsumer::VisitDecl(Decl *decl) { if (AccessSpecifierManager *a = m_context->accessSpecifierManager) // Needs to visit system headers too (qobject.h for example) a->VisitDeclaration(decl); const SourceLocation locStart = clazy::getLocStart(decl); if (locStart.isInvalid() || m_context->sm.isInSystemHeader(locStart)) return true; const bool isFromIgnorableInclude = m_context->ignoresIncludedFiles() && !Utils::isMainFile(m_context->sm, locStart); m_context->lastDecl = decl; if (auto fdecl = dyn_cast(decl)) { m_context->lastFunctionDecl = fdecl; if (auto mdecl = dyn_cast(fdecl)) m_context->lastMethodDecl = mdecl; } for (CheckBase *check : m_checksToVisitDecls) { if (!(isFromIgnorableInclude && check->canIgnoreIncludes())) check->VisitDecl(decl); } return true; } bool ClazyASTConsumer::VisitStmt(Stmt *stm) { const SourceLocation locStart = clazy::getLocStart(stm); if (locStart.isInvalid() || m_context->sm.isInSystemHeader(locStart)) return true; if (!m_context->parentMap) { if (m_context->ci.getDiagnostics().hasUnrecoverableErrorOccurred()) return false; // ParentMap sometimes crashes when there were errors. Doesn't like a botched AST. m_context->parentMap = new ParentMap(stm); } ParentMap *parentMap = m_context->parentMap; // Workaround llvm bug: Crashes creating a parent map when encountering Catch Statements. if (lastStm && isa(lastStm) && !parentMap->hasParent(stm)) { parentMap->setParent(stm, lastStm); manuallyPopulateParentMap(parentMap, stm); } lastStm = stm; // clang::ParentMap takes a root statement, but there's no root statement in the AST, the root is a declaration // So add to parent map each time we go into a different hierarchy if (!parentMap->hasParent(stm)) parentMap->addStmt(stm); const bool isFromIgnorableInclude = m_context->ignoresIncludedFiles() && !Utils::isMainFile(m_context->sm, locStart); for (CheckBase *check : m_checksToVisitStmts) { if (!(isFromIgnorableInclude && check->canIgnoreIncludes())) check->VisitStmt(stm); } return true; } void ClazyASTConsumer::HandleTranslationUnit(ASTContext &ctx) { // FIXME: EndSourceFile() is called automatically, but not BeginsSourceFile() if (m_context->exporter) m_context->exporter->BeginSourceFile(clang::LangOptions()); if ((m_context->options & ClazyContext::ClazyOption_OnlyQt) && !m_context->isQt()) return; // Run our RecursiveAstVisitor based checks: TraverseDecl(ctx.getTranslationUnitDecl()); #ifndef CLAZY_DISABLE_AST_MATCHERS // Run our AstMatcher base checks: m_matchFinder->matchAST(ctx); #endif } static bool parseArgument(const string &arg, vector &args) { auto it = clazy::find(args, arg); if (it != args.end()) { args.erase(it); return true; } return false; } ClazyASTAction::ClazyASTAction() : PluginASTAction() , m_checkManager(CheckManager::instance()) { } std::unique_ptr ClazyASTAction::CreateASTConsumer(CompilerInstance &, llvm::StringRef) { // NOTE: This method needs to be kept reentrant (but not necessarily thread-safe) // Might be called from multiple threads via libclang, each thread operates on a different instance though std::lock_guard lock(CheckManager::lock()); auto astConsumer = std::unique_ptr(new ClazyASTConsumer(m_context)); auto createdChecks = m_checkManager->createChecks(m_checks, m_context); for (auto check : createdChecks) { astConsumer->addCheck(check); } return std::unique_ptr(astConsumer.release()); } static std::string getEnvVariable(const char *name) { const char *result = getenv(name); if (result) return result; else return std::string(); } bool ClazyASTAction::ParseArgs(const CompilerInstance &ci, const std::vector &args_) { // NOTE: This method needs to be kept reentrant (but not necessarily thread-safe) // Might be called from multiple threads via libclang, each thread operates on a different instance though std::vector args = args_; const string headerFilter = getEnvVariable("CLAZY_HEADER_FILTER"); const string ignoreDirs = getEnvVariable("CLAZY_IGNORE_DIRS"); std::string exportFixes; if (parseArgument("help", args)) { m_context = new ClazyContext(ci, headerFilter, ignoreDirs, exportFixes, ClazyContext::ClazyOption_None); PrintHelp(llvm::errs()); return true; } - if (parseArgument("no-inplace-fixits", args)) { - // Unit-tests don't use inplace fixits - m_options |= ClazyContext::ClazyOption_NoFixitsInplace; - } - if (parseArgument("enable-all-fixits", args)) { // This is useful for unit-tests, where we also want to run fixits. Don't use it otherwise. m_options |= ClazyContext::ClazyOption_AllFixitsEnabled; } if (parseArgument("no-autowrite-fixits", args)) m_options |= ClazyContext::ClazyOption_NoFixitsAutoWrite; if (parseArgument("qt4-compat", args)) m_options |= ClazyContext::ClazyOption_Qt4Compat; if (parseArgument("only-qt", args)) m_options |= ClazyContext::ClazyOption_OnlyQt; if (parseArgument("qt-developer", args)) m_options |= ClazyContext::ClazyOption_QtDeveloper; if (parseArgument("visit-implicit-code", args)) m_options |= ClazyContext::ClazyOption_VisitImplicitCode; if (parseArgument("ignore-included-files", args)) m_options |= ClazyContext::ClazyOption_IgnoreIncludedFiles; if (parseArgument("export-fixes", args)) exportFixes = args.at(0); m_context = new ClazyContext(ci, headerFilter, ignoreDirs, exportFixes, m_options); // This argument is for debugging purposes const bool dbgPrintRequestedChecks = parseArgument("print-requested-checks", args); { std::lock_guard lock(CheckManager::lock()); m_checks = m_checkManager->requestedChecks(m_context, args); } if (args.size() > 1) { // Too many arguments. llvm::errs() << "Too many arguments: "; for (const std::string &a : args) llvm::errs() << a << ' '; llvm::errs() << "\n"; PrintHelp(llvm::errs()); return false; } else if (args.size() == 1 && m_checks.empty()) { // Checks were specified but couldn't be found llvm::errs() << "Could not find checks in comma separated string " + args[0] + "\n"; PrintHelp(llvm::errs()); return false; } if (dbgPrintRequestedChecks) printRequestedChecks(); return true; } void ClazyASTAction::printRequestedChecks() const { llvm::errs() << "Requested checks: "; const unsigned int numChecks = m_checks.size(); for (unsigned int i = 0; i < numChecks; ++i) { llvm::errs() << m_checks.at(i).name; const bool isLast = i == numChecks - 1; if (!isLast) { llvm::errs() << ", "; } } llvm::errs() << "\n"; } void ClazyASTAction::PrintHelp(llvm::raw_ostream &ros) const { std::lock_guard lock(CheckManager::lock()); RegisteredCheck::List checks = m_checkManager->availableChecks(MaxCheckLevel); clazy::sort(checks, checkLessThanByLevel); ros << "Available checks and FixIts:\n\n"; int lastPrintedLevel = -1; const auto numChecks = checks.size(); for (unsigned int i = 0; i < numChecks; ++i) { const RegisteredCheck &check = checks[i]; const string levelStr = "level" + to_string(check.level); if (lastPrintedLevel < check.level) { lastPrintedLevel = check.level; if (check.level > 0) ros << "\n"; ros << "- Checks from " << levelStr << ":\n"; } const string relativeReadmePath = "src/checks/" + levelStr + "/README-" + check.name + ".md"; auto padded = check.name; padded.insert(padded.end(), 39 - padded.size(), ' '); ros << " - " << check.name;; auto fixits = m_checkManager->availableFixIts(check.name); if (!fixits.empty()) { ros << " ("; bool isFirst = true; for (const auto& fixit : fixits) { if (isFirst) { isFirst = false; } else { ros << ','; } ros << fixit.name; } ros << ')'; } ros << "\n"; } ros << "\nIf nothing is specified, all checks from level0 and level1 will be run.\n\n"; ros << "To specify which checks to enable set the CLAZY_CHECKS env variable, for example:\n"; ros << " export CLAZY_CHECKS=\"level0\"\n"; ros << " export CLAZY_CHECKS=\"level0,reserve-candidates,qstring-allocations\"\n"; ros << " export CLAZY_CHECKS=\"reserve-candidates\"\n\n"; ros << "or pass as compiler arguments, for example:\n"; ros << " -Xclang -plugin-arg-clazy -Xclang reserve-candidates,qstring-allocations\n"; ros << "\n"; ros << "To enable FixIts for a check, also set the env variable CLAZY_FIXIT, for example:\n"; ros << " export CLAZY_FIXIT=\"fix-qlatin1string-allocations\"\n\n"; ros << "FixIts are experimental and rewrite your code therefore only one FixIt is allowed per build.\nSpecifying a list of different FixIts is not supported.\nBackup your code before running them.\n"; } ClazyStandaloneASTAction::ClazyStandaloneASTAction(const string &checkList, const string &headerFilter, const string &ignoreDirs, const string &exportFixes, ClazyContext::ClazyOptions options) : clang::ASTFrontendAction() , m_checkList(checkList.empty() ? "level1" : checkList) , m_headerFilter(headerFilter.empty() ? getEnvVariable("CLAZY_HEADER_FILTER") : headerFilter) , m_ignoreDirs(ignoreDirs.empty() ? getEnvVariable("CLAZY_IGNORE_DIRS") : ignoreDirs) , m_exportFixes(exportFixes) , m_options(options) { } unique_ptr ClazyStandaloneASTAction::CreateASTConsumer(CompilerInstance &ci, llvm::StringRef) { auto context = new ClazyContext(ci, m_headerFilter, m_ignoreDirs, m_exportFixes, m_options); auto astConsumer = new ClazyASTConsumer(context); auto cm = CheckManager::instance(); vector checks; checks.push_back(m_checkList); const RegisteredCheck::List requestedChecks = cm->requestedChecks(context, checks); if (requestedChecks.size() == 0) { llvm::errs() << "No checks were requested!\n" << "\n"; return nullptr; } auto createdChecks = cm->createChecks(requestedChecks, context); for (const auto &check : createdChecks) { astConsumer->addCheck(check); } return unique_ptr(astConsumer); } volatile int ClazyPluginAnchorSource = 0; static FrontendPluginRegistry::Add X("clazy", "clang lazy plugin"); diff --git a/src/ClazyContext.cpp b/src/ClazyContext.cpp index 96bdf9f..fc23a68 100644 --- a/src/ClazyContext.cpp +++ b/src/ClazyContext.cpp @@ -1,145 +1,145 @@ /* This file is part of the clazy static checker. Copyright (C) 2017 Sergio Martins This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "AccessSpecifierManager.h" #include "ClazyContext.h" #include "FixItExporter.h" #include "PreProcessorVisitor.h" #include #include #include #include #include #include using namespace std; using namespace clang; class ClazyFixItOptions : public FixItOptions { public: ClazyFixItOptions(const ClazyFixItOptions &other) = delete; - ClazyFixItOptions(bool inplace) + ClazyFixItOptions() { if (const char *suffix = getenv("CLAZY_FIXIT_SUFFIX")) m_suffix = suffix; - InPlace = inplace && m_suffix.empty(); + InPlace = m_suffix.empty(); FixWhatYouCan = true; FixOnlyWarnings = true; Silent = false; } std::string RewriteFilename(const std::string &filename, int &fd) override { fd = -1; return InPlace ? filename : filename + m_suffix; } std::string m_suffix; }; ClazyContext::ClazyContext(const clang::CompilerInstance &compiler, const string &headerFilter, const string &ignoreDirs, const string &exportFixes, ClazyOptions opts) : ci(compiler) , astContext(ci.getASTContext()) , sm(ci.getSourceManager()) , m_noWerror(getenv("CLAZY_NO_WERROR") != nullptr) // Allows user to make clazy ignore -Werror , options(opts) , extraOptions(clazy::splitString(getenv("CLAZY_EXTRA_OPTIONS"), ',')) , exportFixes(exportFixes) { if (!headerFilter.empty()) headerFilterRegex = std::unique_ptr(new llvm::Regex(headerFilter)); if (!ignoreDirs.empty()) ignoreDirsRegex = std::unique_ptr(new llvm::Regex(ignoreDirs)); const char *fixitsEnv = getenv("CLAZY_FIXIT"); allFixitsEnabled = (options & ClazyOption_AllFixitsEnabled); if (!allFixitsEnabled && fixitsEnv) { const string fixitsEnvStr = clazy::unquoteString(fixitsEnv); if (fixitsEnvStr == "all_fixits") { allFixitsEnabled = true; } else { requestedFixitName = fixitsEnvStr; } } if (fixitsEnabled()) { if (exportFixesEnabled()) exporter = new FixItExporter(ci.getDiagnostics(), sm, ci.getLangOpts(), exportFixes); else if (!(options & ClazyOption_NoFixitsAutoWrite)) rewriter = new FixItRewriter(ci.getDiagnostics(), sm, - ci.getLangOpts(), new ClazyFixItOptions(fixitsAreInplace())); + ci.getLangOpts(), new ClazyFixItOptions()); } } ClazyContext::~ClazyContext() { //delete preprocessorVisitor; // we don't own it delete accessSpecifierManager; delete parentMap; if (exporter) { exporter->Export(); delete exporter; } if (rewriter) { rewriter->WriteFixedFiles(); delete rewriter; } preprocessorVisitor = nullptr; accessSpecifierManager = nullptr; parentMap = nullptr; } void ClazyContext::enableAccessSpecifierManager() { if (!accessSpecifierManager && !usingPreCompiledHeaders()) accessSpecifierManager = new AccessSpecifierManager(ci); } void ClazyContext::enablePreprocessorVisitor() { if (!preprocessorVisitor && !usingPreCompiledHeaders()) preprocessorVisitor = new PreProcessorVisitor(ci); } bool ClazyContext::isQt() const { static const bool s_isQt = [this] { for (auto s : ci.getPreprocessorOpts().Macros) { if (s.first == "QT_CORE_LIB") return true; } return false; } (); return s_isQt; } diff --git a/src/ClazyContext.h b/src/ClazyContext.h index ae997df..2fbcfae 100644 --- a/src/ClazyContext.h +++ b/src/ClazyContext.h @@ -1,202 +1,196 @@ /* This file is part of the clazy static checker. Copyright (C) 2017 Sergio Martins This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CLAZY_CONTEXT_H #define CLAZY_CONTEXT_H #include "SuppressionManager.h" #include "clazy_stl.h" #include #include #include #include #include #include #include #include #include #include #include // ClazyContext is just a struct to share data and code between all checks namespace clang { class CompilerInstance; class ASTContext; class ParentMap; class SourceManager; class FixItRewriter; class CXXMethodDecl; class Decl; } class AccessSpecifierManager; class PreProcessorVisitor; class FixItExporter; class ClazyContext { public: enum ClazyOption { ClazyOption_None = 0, - ClazyOption_NoFixitsInplace = 1, ClazyOption_NoFixitsAutoWrite = 2, // If enabled then fixits are reported, but not applied ClazyOption_AllFixitsEnabled = 4, ClazyOption_Qt4Compat = 8, ClazyOption_OnlyQt = 16, // Ignore non-Qt files. This is done by bailing out if QT_CORE_LIB is not set. ClazyOption_QtDeveloper = 32, // For running clazy on Qt itself, optional, but honours specific guidelines ClazyOption_VisitImplicitCode = 64, // Inspect compiler generated code aswell, useful for custom checks, if they need it ClazyOption_IgnoreIncludedFiles = 128 // Only warn for the current file being compiled, not on includes (useful for performance reasons) }; typedef int ClazyOptions; explicit ClazyContext(const clang::CompilerInstance &ci, const std::string &headerFilter, const std::string &ignoreDirs, const std::string &exportFixes, ClazyOptions = ClazyOption_None); ~ClazyContext(); bool usingPreCompiledHeaders() const { return !ci.getPreprocessorOpts().ImplicitPCHInclude.empty(); } bool userDisabledWError() const { return m_noWerror; } - bool fixitsAreInplace() const - { - return !(options & ClazyOption_NoFixitsInplace); - } - bool fixitsEnabled() const { return allFixitsEnabled || !requestedFixitName.empty(); } bool exportFixesEnabled() const { return !exportFixes.empty(); } bool isQtDeveloper() const { return options & ClazyOption_QtDeveloper; } bool ignoresIncludedFiles() const { return options & ClazyOption_IgnoreIncludedFiles; } bool isVisitImplicitCode() const { return options & ClazyContext::ClazyOption_VisitImplicitCode; } bool isOptionSet(const std::string &optionName) const { return clazy::contains(extraOptions, optionName); } bool fileMatchesLoc(const std::unique_ptr ®ex, clang::SourceLocation loc, const clang::FileEntry **file) const { if (!regex) return false; if (!(*file)) { clang::FileID fid = sm.getDecomposedExpansionLoc(loc).first; *file = sm.getFileEntryForID(fid); if (!(*file)) { return false; } } llvm::StringRef fileName((*file)->getName()); return regex->match(fileName); } bool shouldIgnoreFile(clang::SourceLocation loc) const { // 1. Process the regexp that excludes files const clang::FileEntry *file = nullptr; if (ignoreDirsRegex) { const bool matches = fileMatchesLoc(ignoreDirsRegex, loc, &file); if (matches) return true; } // 2. Process the regexp that includes files. Has lower priority. if (!headerFilterRegex || isMainFile(loc)) return false; const bool matches = fileMatchesLoc(headerFilterRegex, loc, &file); if (!file) return false; return !matches; } bool isMainFile(clang::SourceLocation loc) const { if (loc.isMacroID()) loc = sm.getExpansionLoc(loc); return sm.isInFileID(loc, sm.getMainFileID()); } /** * We only enable it if a check needs it, for performance reasons */ void enableAccessSpecifierManager(); void enablePreprocessorVisitor(); bool isQt() const; // TODO: More things will follow const clang::CompilerInstance &ci; clang::ASTContext &astContext; clang::SourceManager &sm; AccessSpecifierManager *accessSpecifierManager = nullptr; PreProcessorVisitor *preprocessorVisitor = nullptr; SuppressionManager suppressionManager; const bool m_noWerror; clang::ParentMap *parentMap = nullptr; const ClazyOptions options; const std::vector extraOptions; clang::FixItRewriter *rewriter = nullptr; FixItExporter *exporter = nullptr; bool allFixitsEnabled = false; std::string requestedFixitName; std::string exportFixes; clang::CXXMethodDecl *lastMethodDecl = nullptr; clang::FunctionDecl *lastFunctionDecl = nullptr; clang::Decl *lastDecl = nullptr; std::unique_ptr headerFilterRegex; std::unique_ptr ignoreDirsRegex; }; #endif diff --git a/src/ClazyStandaloneMain.cpp b/src/ClazyStandaloneMain.cpp index 3c0e3d8..93f0176 100644 --- a/src/ClazyStandaloneMain.cpp +++ b/src/ClazyStandaloneMain.cpp @@ -1,125 +1,120 @@ /* This file is part of the clazy static checker. Copyright (C) 2017 Sergio Martins This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // clazy:excludeall=non-pod-global-static #include "Clazy.h" #include "ClazyContext.h" #include #include #include #include #include #include namespace clang { class FrontendAction; } // namespace clang using namespace clang; using namespace clang::tooling; using namespace llvm; static llvm::cl::OptionCategory s_clazyCategory("clazy options"); static cl::opt s_checks("checks", cl::desc("Comma-separated list of clazy checks. Default is level1"), cl::init(""), cl::cat(s_clazyCategory)); -static cl::opt s_noInplaceFixits("no-inplace-fixits", cl::desc("Fixits will be applied to a separate file (for unit-test use only)"), - cl::init(false), cl::cat(s_clazyCategory)); - static cl::opt s_enableAllFixits("enable-all-fixits", cl::desc("Enables all fixits"), cl::init(false), cl::cat(s_clazyCategory)); static cl::opt s_exportFixes("export-fixes", cl::desc("YAML file to store suggested fixes in. The stored fixes can be applied to the input source code with clang-apply-replacements."), cl::init(""), cl::cat(s_clazyCategory)); static cl::opt s_qt4Compat("qt4-compat", cl::desc("Turns off checks not compatible with Qt 4"), cl::init(false), cl::cat(s_clazyCategory)); static cl::opt s_onlyQt("only-qt", cl::desc("Won't emit warnings for non-Qt files, or in other words, if -DQT_CORE_LIB is missing."), cl::init(false), cl::cat(s_clazyCategory)); static cl::opt s_qtDeveloper("qt-developer", cl::desc("For running clazy on Qt itself, optional, but honours specific guidelines"), cl::init(false), cl::cat(s_clazyCategory)); static cl::opt s_visitImplicitCode("visit-implicit-code", cl::desc("For visiting implicit code like compiler generated constructors. None of the built-in checks benefit from this, but can be useful for custom checks"), cl::init(false), cl::cat(s_clazyCategory)); static cl::opt s_ignoreIncludedFiles("ignore-included-files", cl::desc("Only emit warnings for the current file being compiled and ignore any includes. Useful for performance reasons."), cl::init(false), cl::cat(s_clazyCategory)); static cl::opt s_headerFilter("header-filter", cl::desc(R"(Regular expression matching the names of the headers to output diagnostics from. Diagnostics from the main file of each translation unit are always displayed.)"), cl::init(""), cl::cat(s_clazyCategory)); static cl::opt s_ignoreDirs("ignore-dirs", cl::desc(R"(Regular expression matching the names of the directories for which diagnostics should never be emitted. Useful for ignoring 3rdparty code.)"), cl::init(""), cl::cat(s_clazyCategory)); static cl::extrahelp s_commonHelp(CommonOptionsParser::HelpMessage); class ClazyToolActionFactory : public clang::tooling::FrontendActionFactory { public: ClazyToolActionFactory() : FrontendActionFactory() {} FrontendAction *create() override { ClazyContext::ClazyOptions options = ClazyContext::ClazyOption_None; - if (s_noInplaceFixits.getValue()) - options |= ClazyContext::ClazyOption_NoFixitsInplace; if (s_enableAllFixits.getValue()) options |= ClazyContext::ClazyOption_AllFixitsEnabled; if (s_qt4Compat.getValue()) options |= ClazyContext::ClazyOption_Qt4Compat; if (s_qtDeveloper.getValue()) options |= ClazyContext::ClazyOption_QtDeveloper; if (s_onlyQt.getValue()) options |= ClazyContext::ClazyOption_OnlyQt; if (s_visitImplicitCode.getValue()) options |= ClazyContext::ClazyOption_VisitImplicitCode; if (s_ignoreIncludedFiles.getValue()) options |= ClazyContext::ClazyOption_IgnoreIncludedFiles; // TODO: We need to agregate the fixes with previous run return new ClazyStandaloneASTAction(s_checks.getValue(), s_headerFilter.getValue(), s_ignoreDirs.getValue(), s_exportFixes.getValue(), options); } }; int main(int argc, const char **argv) { CommonOptionsParser optionsParser(argc, argv, s_clazyCategory); ClangTool tool(optionsParser.getCompilations(), optionsParser.getSourcePathList()); return tool.run(new ClazyToolActionFactory()); } diff --git a/tests/run_tests.py b/tests/run_tests.py index 9562440..2efacad 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1,614 +1,614 @@ #!/usr/bin/env python2 import sys, os, subprocess, string, re, json, threading, multiprocessing, argparse from threading import Thread from sys import platform as _platform _verbose = False def isWindows(): return _platform == 'win32' class QtInstallation: def __init__(self): self.int_version = 000 self.qmake_header_path = "/usr/include/qt/" self.qmake_lib_path = "/usr/lib" def compiler_flags(self): return "-isystem " + self.qmake_header_path + ("" if isWindows() else " -fPIC") + " -L " + self.qmake_lib_path class Test: def __init__(self, check): self.filenames = [] self.minimum_qt_version = 500 self.maximum_qt_version = 59999 self.minimum_clang_version = 380 self.compare_everything = False self.isFixedFile = False self.link = False # If true we also call the linker self.check = check self.expects_failure = False self.qt_major_version = 5 # Tests use Qt 5 by default self.env = os.environ self.checks = [] self.flags = "" self.must_fail = False self.blacklist_platforms = [] self.qt4compat = False self.only_qt = False self.qt_developer = False self.header_filter = "" self.ignore_dirs = "" def filename(self): if len(self.filenames) == 1: return self.filenames[0] return "" def isScript(self): return self.filename().endswith(".sh") def setQtMajorVersion(self, major_version): if major_version == 4: self.qt_major_version = 4 if self.minimum_qt_version >= 500: self.minimum_qt_version = 400 def envString(self): result = "" for key in self.env: result += key + '="' + self.env[key] + '" ' return result def setEnv(self, e): self.env = os.environ.copy() for key in e: key_str = key.encode('ascii', 'ignore') self.env[key_str] = e[key].encode('ascii', 'ignore') class Check: def __init__(self, name): self.name = name self.minimum_clang_version = 380 # clang 3.8.0 self.minimum_qt_version = 500 self.maximum_qt_version = 59999 self.enabled = True self.clazy_standalone_only = False self.tests = [] #------------------------------------------------------------------------------- # utility functions #1 def get_command_output(cmd, test_env = os.environ): try: if _verbose: print cmd output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True, env=test_env) except subprocess.CalledProcessError, e: return e.output,False return output,True def load_json(check_name): check = Check(check_name) filename = check_name + "/config.json" if not os.path.exists(filename): # Ignore this directory return check f = open(filename, 'r') contents = f.read() f.close() decoded = json.loads(contents) check_blacklist_platforms = [] if 'minimum_clang_version' in decoded: check.minimum_clang_version = decoded['minimum_clang_version'] if 'minimum_qt_version' in decoded: check.minimum_qt_version = decoded['minimum_qt_version'] if 'maximum_qt_version' in decoded: check.maximum_qt_version = decoded['maximum_qt_version'] if 'enabled' in decoded: check.enabled = decoded['enabled'] if 'clazy_standalone_only' in decoded: check.clazy_standalone_only = decoded['clazy_standalone_only'] if 'blacklist_platforms' in decoded: check_blacklist_platforms = decoded['blacklist_platforms'] if 'tests' in decoded: for t in decoded['tests']: test = Test(check) test.blacklist_platforms = check_blacklist_platforms if 'filename' in t: test.filenames.append(t['filename']) if 'filenames' in t: test.filenames += t['filenames'] if 'minimum_qt_version' in t: test.minimum_qt_version = t['minimum_qt_version'] else: test.minimum_qt_version = check.minimum_qt_version if 'maximum_qt_version' in t: test.maximum_qt_version = t['maximum_qt_version'] else: test.maximum_qt_version = check.maximum_qt_version if 'minimum_clang_version' in t: test.minimum_clang_version = t['minimum_clang_version'] else: test.minimum_clang_version = check.minimum_clang_version if 'blacklist_platforms' in t: test.blacklist_platforms = t['blacklist_platforms'] if 'compare_everything' in t: test.compare_everything = t['compare_everything'] if 'isFixedFile' in t: test.isFixedFile = t['isFixedFile'] if 'link' in t: test.link = t['link'] if 'qt_major_version' in t: test.setQtMajorVersion(t['qt_major_version']) if 'env' in t: test.setEnv(t['env']) if 'checks' in t: test.checks = t['checks'] if 'flags' in t: test.flags = t['flags'] if 'must_fail' in t: test.must_fail = t['must_fail'] if 'expects_failure' in t: test.expects_failure = t['expects_failure'] if 'qt4compat' in t: test.qt4compat = t['qt4compat'] if 'only_qt' in t: test.only_qt = t['only_qt'] if 'qt_developer' in t: test.qt_developer = t['qt_developer'] if 'header_filter' in t: test.header_filter = t['header_filter'] if 'ignore_dirs' in t: test.ignore_dirs = t['ignore_dirs'] if not test.checks: test.checks.append(test.check.name) check.tests.append(test) if test.isFixedFile: fileToDelete = check_name + "/" + test.filename() if os.path.exists(fileToDelete): os.remove(fileToDelete) return check def find_qt_installation(major_version, qmakes): installation = QtInstallation() for qmake in qmakes: qmake_version_str,success = get_command_output(qmake + " -query QT_VERSION") if success and qmake_version_str.startswith(str(major_version) + "."): qmake_header_path = get_command_output(qmake + " -query QT_INSTALL_HEADERS")[0].strip() qmake_lib_path = get_command_output(qmake + " -query QT_INSTALL_LIBS")[0].strip() if qmake_header_path: installation.qmake_header_path = qmake_header_path if qmake_lib_path: installation.qmake_lib_path = qmake_lib_path ver = qmake_version_str.split('.') installation.int_version = int(ver[0]) * 10000 + int(ver[1]) * 100 + int(ver[2]) if _verbose: print "Found Qt " + str(installation.int_version) + " using qmake " + qmake break if installation.int_version == 0 and major_version >= 5: # Don't warn for missing Qt4 headers print "Error: Couldn't find a Qt" + str(major_version) + " installation" return installation def libraryName(): if _platform == 'win32': return 'ClazyPlugin.dll' elif _platform == 'darwin': return 'ClazyPlugin.dylib' else: return 'ClazyPlugin.so' def link_flags(): flags = "-lQt5Core -lQt5Gui -lQt5Widgets" if _platform.startswith('linux'): flags += " -lstdc++" return flags def clazy_cpp_args(): return "-Wno-unused-value -Qunused-arguments -std=c++14 " def more_clazy_args(): - return " -Xclang -plugin-arg-clazy -Xclang no-inplace-fixits " + clazy_cpp_args() + return " " + clazy_cpp_args() def clazy_standalone_binary(): if 'CLAZYSTANDALONE_CXX' in os.environ: # in case we want to use "clazy.AppImage --standalone" instead return os.environ['CLAZYSTANDALONE_CXX'] return 'clazy-standalone' def clazy_standalone_command(test, qt): result = " -- " + clazy_cpp_args() + qt.compiler_flags() + " " + test.flags - result = " -no-inplace-fixits -checks=" + string.join(test.checks, ',') + " " + result + result = " -export-fixes=" + test.check.name + "/fixes.yaml -checks=" + string.join(test.checks, ',') + " " + result if not test.isFixedFile: result = " -enable-all-fixits " + result if test.qt4compat: result = " -qt4-compat " + result if test.only_qt: result = " -only-qt " + result if test.qt_developer: result = " -qt-developer " + result if test.header_filter: result = " -header-filter " + test.header_filter + " " + result if test.ignore_dirs: result = " -ignore-dirs " + test.ignore_dirs + " " + result return result def clazy_command(qt, test, filename): if test.isScript(): return "./" + filename if 'CLAZY_CXX' in os.environ: # In case we want to use clazy.bat result = os.environ['CLAZY_CXX'] + more_clazy_args() + qt.compiler_flags() else: clang = os.getenv('CLANGXX', 'clang') result = clang + " -Xclang -load -Xclang " + libraryName() + " -Xclang -add-plugin -Xclang clazy " + more_clazy_args() + qt.compiler_flags() if test.qt4compat: result = result + " -Xclang -plugin-arg-clazy -Xclang qt4-compat " if test.only_qt: result = result + " -Xclang -plugin-arg-clazy -Xclang only-qt " if test.qt_developer: result = result + " -Xclang -plugin-arg-clazy -Xclang qt-developer " if test.link and _platform.startswith('linux'): # Linking on one platform is enough. Won't waste time on macOS and Windows. result = result + " " + link_flags() else: result = result + " -c " result = result + test.flags + " -Xclang -plugin-arg-clazy -Xclang " + string.join(test.checks, ',') + " " if not test.isFixedFile: # When compiling the already fixed file disable fixit, we don't want to fix twice result += _enable_fixits_argument + " " result += filename return result def dump_ast_command(test): return "clang -std=c++14 -fsyntax-only -Xclang -ast-dump -fno-color-diagnostics -c " + qt_installation(test.qt_major_version).compiler_flags() + " " + test.flags + " " + test.filename() def compiler_name(): if 'CLAZY_CXX' in os.environ: return os.environ['CLAZY_CXX'] # so we can set clazy.bat instead return os.getenv('CLANGXX', 'clang') #------------------------------------------------------------------------------- # Setup argparse parser = argparse.ArgumentParser() parser.add_argument("-v", "--verbose", action='store_true') parser.add_argument("--no-standalone", action='store_true', help="Don\'t run clazy-standalone") parser.add_argument("--only-standalone", action='store_true', help='Only run clazy-standalone') parser.add_argument("--dump-ast", action='store_true', help='Dump a unit-test AST to file') parser.add_argument("--exclude", help='Comma separated list of checks to ignore') parser.add_argument("check_names", nargs='*', help="The name of the check who's unit-tests will be run. Defaults to running all checks.") args = parser.parse_args() if args.only_standalone and args.no_standalone: print "Error: --only-standalone is incompatible with --no-standalone" sys.exit(1) #------------------------------------------------------------------------------- # Global variables _enable_fixits_argument = "-Xclang -plugin-arg-clazy -Xclang enable-all-fixits" _dump_ast = args.dump_ast _verbose = args.verbose _no_standalone = args.no_standalone _only_standalone = args.only_standalone _num_threads = multiprocessing.cpu_count() _lock = threading.Lock() _was_successful = True _qt5_installation = find_qt_installation(5, ["QT_SELECT=5 qmake", "qmake-qt5", "qmake"]) _qt4_installation = find_qt_installation(4, ["QT_SELECT=4 qmake", "qmake-qt4", "qmake"]) _excluded_checks = args.exclude.split(',') if args.exclude is not None else [] #------------------------------------------------------------------------------- # utility functions #2 version,success = get_command_output(compiler_name() + ' --version') match = re.search('clang version (.*?)[ -]', version) try: version = match.group(1) except: # Now try the Clazy.AppImage way match = re.search('clang version: (.*)', version) try: version = match.group(1) except: print "Could not determine clang version, is it in PATH?" sys.exit(-1) if _verbose: print 'Found clang version: ' + str(version) CLANG_VERSION = int(version.replace('.', '')) def qt_installation(major_version): if major_version == 5: return _qt5_installation elif major_version == 4: return _qt4_installation return None def run_command(cmd, output_file = "", test_env = os.environ): lines,success = get_command_output(cmd, test_env) lines = lines.replace("std::_Container_base0", "std::_Vector_base") # Hack for Windows, we have std::_Vector_base in the expected data lines = lines.replace("std::__1::__vector_base_common", "std::_Vector_base") # Hack for macOS lines = lines.replace("std::_Vector_alloc", "std::_Vector_base") if not success and not output_file: print lines return False if _verbose: print "Running: " + cmd print "output_file=" + output_file lines = lines.replace('\r\n', '\n') if output_file: f = open(output_file, 'w') f.writelines(lines) f.close() else: print lines return success def files_are_equal(file1, file2): try: f = open(file1, 'r') lines1 = f.readlines() f.close() f = open(file2, 'r') lines2 = f.readlines() f.close() return lines1 == lines2 except: return False def get_check_names(): return filter(lambda entry: os.path.isdir(entry), os.listdir(".")) # Returns all files with .cpp_fixed extension. These were rewritten by clang. def get_fixed_files(): return filter(lambda entry: entry.endswith('.cpp_fixed.cpp'), os.listdir(".")) def print_differences(file1, file2): # Returns true if the the files are equal return run_command("diff -Naur {} {}".format(file1, file2)) def normalizedCwd(): return os.getcwd().replace('\\', '/') def extract_word(word, in_file, out_file): in_f = open(in_file, 'r') out_f = open(out_file, 'w') for line in in_f: if word in line: line = line.replace('\\', '/') line = line.replace(normalizedCwd() + '/', "") # clazy-standalone prints the complete cpp file path for some reason. Normalize it so it compares OK with the expected output. out_f.write(line) in_f.close() out_f.close() def print_file(filename): f = open(filename, 'r') print f.read() f.close() def file_contains(filename, text): f = open(filename, 'r') contents = f.read() f.close() return text in contents def run_unit_test(test, is_standalone): if test.check.clazy_standalone_only and not is_standalone: return True qt = qt_installation(test.qt_major_version) if _verbose: print print "Qt version: " + str(qt.int_version) print "Qt headers: " + qt.qmake_header_path if qt.int_version < test.minimum_qt_version or qt.int_version > test.maximum_qt_version or CLANG_VERSION < test.minimum_clang_version: if (_verbose): print "Skipping " + test.check.name + " because required version is not available" return True if _platform in test.blacklist_platforms: if (_verbose): print "Skipping " + test.check.name + " because it is blacklisted for this platform" return True checkname = test.check.name filename = checkname + "/" + test.filename() output_file = filename + ".out" result_file = filename + ".result" expected_file = filename + ".expected" if is_standalone and test.isScript(): return True if is_standalone: cmd_to_run = clazy_standalone_binary() + " " + filename + " " + clazy_standalone_command(test, qt) else: cmd_to_run = clazy_command(qt, test, filename) if test.compare_everything: result_file = output_file if test.isFixedFile: result_file = filename must_fail = test.must_fail cmd_success = run_command(cmd_to_run, output_file, test.env) if file_contains(output_file, 'Invalid check: '): return True if (not cmd_success and not must_fail) or (cmd_success and must_fail): print "[FAIL] " + checkname + " (Failed to build test. Check " + output_file + " for details)" print "-------------------" print "Contents of %s:" % output_file print_file(output_file) print "-------------------" print return False if not test.compare_everything and not test.isFixedFile: word_to_grep = "warning:" if not must_fail else "error:" extract_word(word_to_grep, output_file, result_file) printableName = checkname if len(test.check.tests) > 1: printableName += "/" + test.filename() if is_standalone: printableName += " (standalone)" success = files_are_equal(expected_file, result_file) if test.expects_failure: if success: print "[XOK] " + printableName return False else: print "[XFAIL] " + printableName print_differences(expected_file, result_file) else: if success: print "[OK] " + printableName else: print "[FAIL] " + printableName print_differences(expected_file, result_file) return False return True def run_unit_tests(tests): result = True for test in tests: if not _only_standalone: result = result and run_unit_test(test, False) if not _no_standalone: result = result and run_unit_test(test, True) global _was_successful, _lock with _lock: _was_successful = _was_successful and result def dump_ast(check): for test in check.tests: ast_filename = test.filename() + ".ast" run_command(dump_ast_command(test) + " > " + ast_filename) print "Dumped AST to " + os.getcwd() + "/" + ast_filename #------------------------------------------------------------------------------- def load_checks(all_check_names): checks = [] for name in all_check_names: try: check = load_json(name) if check.enabled: checks.append(check) except: print "Error while loading " + name raise sys.exit(-1) return checks #------------------------------------------------------------------------------- # main if 'CLAZY_NO_WERROR' in os.environ: del os.environ['CLAZY_NO_WERROR'] os.environ['CLAZY_CHECKS'] = '' os.environ['CLAZY_FIXIT_SUFFIX'] = '_fixed.cpp' all_check_names = get_check_names() all_checks = load_checks(all_check_names) requested_check_names = args.check_names requested_check_names = map(lambda x: x.strip("/\\"), requested_check_names) for check_name in requested_check_names: if check_name not in all_check_names: print "Unknown check: " + check_name print sys.exit(-1) if not requested_check_names: requested_check_names = all_check_names requested_checks = filter(lambda check: check.name in requested_check_names and check.name not in _excluded_checks, all_checks) requested_checks = filter(lambda check: check.minimum_clang_version <= CLANG_VERSION, requested_checks) threads = [] if _dump_ast: for check in requested_checks: os.chdir(check.name) dump_ast(check) os.chdir("..") else: list_of_chunks = [[] for x in range(_num_threads)] # Each list is a list of Test to be worked on by a thread i = _num_threads for check in requested_checks: for test in check.tests: if not test.isFixedFile: i = (i + 1) % _num_threads list_of_chunks[i].append(test) for tests in list_of_chunks: if not tests: continue; t = Thread(target=run_unit_tests, args=(tests,)) t.start() threads.append(t) for thread in threads: thread.join() if _was_successful: print "SUCCESS" sys.exit(0) else: print "FAIL" sys.exit(-1)