diff --git a/Makefile b/Makefile index 0a06529ba46..d0607cde71b 100644 --- a/Makefile +++ b/Makefile @@ -156,7 +156,8 @@ MAN_SOURCE=man/cppcheck.1.xml ###### Object Files -LIBOBJ = $(libcppdir)/analyzerinfo.o \ +LIBOBJ = $(libcppdir)/addonutils.o \ + $(libcppdir)/analyzerinfo.o \ $(libcppdir)/astutils.o \ $(libcppdir)/check.o \ $(libcppdir)/check64bit.o \ @@ -218,6 +219,7 @@ CLIOBJ = cli/cmdlineparser.o \ TESTOBJ = test/options.o \ test/test64bit.o \ + test/testaddonutils.o \ test/testassert.o \ test/testastutils.o \ test/testautovariables.o \ @@ -393,6 +395,9 @@ validateRules: ###### Build +$(libcppdir)/addonutils.o: lib/addonutils.cpp externals/picojson.h lib/addonutils.h lib/config.h lib/errorlogger.h lib/path.h lib/suppressions.h lib/utils.h + $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CPPFILESDIR) $(CXXFLAGS) $(UNDEF_STRICT_ANSI) -c -o $(libcppdir)/addonutils.o $(libcppdir)/addonutils.cpp + $(libcppdir)/analyzerinfo.o: lib/analyzerinfo.cpp externals/tinyxml/tinyxml2.h lib/analyzerinfo.h lib/config.h lib/errorlogger.h lib/importproject.h lib/path.h lib/platform.h lib/suppressions.h lib/utils.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CPPFILESDIR) $(CXXFLAGS) $(UNDEF_STRICT_ANSI) -c -o $(libcppdir)/analyzerinfo.o $(libcppdir)/analyzerinfo.cpp @@ -477,7 +482,7 @@ $(libcppdir)/checkunusedvar.o: lib/checkunusedvar.cpp lib/astutils.h lib/check.h $(libcppdir)/checkvaarg.o: lib/checkvaarg.cpp lib/astutils.h lib/check.h lib/checkvaarg.h lib/config.h lib/errorlogger.h lib/importproject.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/symboldatabase.h lib/templatesimplifier.h lib/timer.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/valueflow.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CPPFILESDIR) $(CXXFLAGS) $(UNDEF_STRICT_ANSI) -c -o $(libcppdir)/checkvaarg.o $(libcppdir)/checkvaarg.cpp -$(libcppdir)/cppcheck.o: lib/cppcheck.cpp externals/picojson.h externals/simplecpp/simplecpp.h externals/tinyxml/tinyxml2.h lib/analyzerinfo.h lib/check.h lib/checkunusedfunctions.h lib/config.h lib/cppcheck.h lib/ctu.h lib/errorlogger.h lib/exprengine.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/preprocessor.h lib/settings.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/timer.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/valueflow.h lib/version.h +$(libcppdir)/cppcheck.o: lib/cppcheck.cpp externals/simplecpp/simplecpp.h externals/tinyxml/tinyxml2.h lib/addonutils.h lib/analyzerinfo.h lib/check.h lib/checkunusedfunctions.h lib/config.h lib/cppcheck.h lib/ctu.h lib/errorlogger.h lib/exprengine.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/preprocessor.h lib/settings.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/timer.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/valueflow.h lib/version.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CPPFILESDIR) $(CXXFLAGS) $(UNDEF_STRICT_ANSI) -c -o $(libcppdir)/cppcheck.o $(libcppdir)/cppcheck.cpp $(libcppdir)/ctu.o: lib/ctu.cpp externals/tinyxml/tinyxml2.h lib/astutils.h lib/check.h lib/config.h lib/ctu.h lib/errorlogger.h lib/importproject.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/symboldatabase.h lib/templatesimplifier.h lib/timer.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/valueflow.h @@ -564,6 +569,9 @@ test/options.o: test/options.cpp test/options.h test/test64bit.o: test/test64bit.cpp lib/check.h lib/check64bit.h lib/config.h lib/errorlogger.h lib/importproject.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/timer.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/valueflow.h test/testsuite.h $(CXX) ${INCLUDE_FOR_TEST} $(CPPFLAGS) $(CPPFILESDIR) $(CXXFLAGS) $(UNDEF_STRICT_ANSI) -c -o test/test64bit.o test/test64bit.cpp +test/testaddonutils.o: test/testaddonutils.cpp lib/addonutils.h lib/config.h lib/errorlogger.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/timer.h lib/utils.h test/testsuite.h + $(CXX) ${INCLUDE_FOR_TEST} $(CPPFLAGS) $(CPPFILESDIR) $(CXXFLAGS) $(UNDEF_STRICT_ANSI) -c -o test/testaddonutils.o test/testaddonutils.cpp + test/testassert.o: test/testassert.cpp lib/check.h lib/checkassert.h lib/config.h lib/errorlogger.h lib/importproject.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/timer.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/valueflow.h test/testsuite.h $(CXX) ${INCLUDE_FOR_TEST} $(CPPFLAGS) $(CPPFILESDIR) $(CXXFLAGS) $(UNDEF_STRICT_ANSI) -c -o test/testassert.o test/testassert.cpp diff --git a/gui/checkthread.cpp b/gui/checkthread.cpp index 31703812aa9..216e6e5d5ef 100644 --- a/gui/checkthread.cpp +++ b/gui/checkthread.cpp @@ -25,6 +25,8 @@ #include #include #include + +#include "addonutils.h" #include "checkthread.h" #include "erroritem.h" #include "threadresult.h" @@ -112,9 +114,13 @@ void CheckThread::run() void CheckThread::runAddonsAndTools(const ImportProject::FileSettings *fileSettings, const QString &fileName) { QString dumpFile; + auto disabled_addons = QStringList(); + + foreach (const QString addon_name, mAddonsAndTools) { + if (disabled_addons.contains(addon_name)) + continue; - foreach (const QString addon, mAddonsAndTools) { - if (addon == CLANG_ANALYZER || addon == CLANG_TIDY) { + if (addon_name == CLANG_ANALYZER || addon_name == CLANG_TIDY) { if (!fileSettings) continue; @@ -196,15 +202,15 @@ void CheckThread::runAddonsAndTools(const ImportProject::FileSettings *fileSetti const QByteArray &ba = process.readAllStandardOutput(); const quint16 chksum = qChecksum(ba.data(), ba.length()); - QFile f1(analyzerInfoFile + '.' + addon + "-E"); + QFile f1(analyzerInfoFile + '.' + addon_name + "-E"); if (f1.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream in1(&f1); const quint16 oldchksum = in1.readAll().toInt(); if (oldchksum == chksum) { - QFile f2(analyzerInfoFile + '.' + addon + "-results"); + QFile f2(analyzerInfoFile + '.' + addon_name + "-results"); if (f2.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream in2(&f2); - parseClangErrors(addon, fileName, in2.readAll()); + parseClangErrors(addon_name, fileName, in2.readAll()); continue; } } @@ -214,10 +220,10 @@ void CheckThread::runAddonsAndTools(const ImportProject::FileSettings *fileSetti QTextStream out1(&f1); out1 << chksum; - QFile::remove(analyzerInfoFile + '.' + addon + "-results"); + QFile::remove(analyzerInfoFile + '.' + addon_name + "-results"); } - if (addon == CLANG_ANALYZER) { + if (addon_name == CLANG_ANALYZER) { /* // Using clang args.insert(0,"--analyze"); @@ -247,7 +253,7 @@ void CheckThread::runAddonsAndTools(const ImportProject::FileSettings *fileSetti qDebug() << debug; if (!analyzerInfoFile.isEmpty()) { - QFile f(analyzerInfoFile + '.' + addon + "-cmd"); + QFile f(analyzerInfoFile + '.' + addon_name + "-cmd"); if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&f); out << debug; @@ -260,22 +266,32 @@ void CheckThread::runAddonsAndTools(const ImportProject::FileSettings *fileSetti process.waitForFinished(600*1000); const QString errout(process.readAllStandardOutput() + "\n\n\n" + process.readAllStandardError()); if (!analyzerInfoFile.isEmpty()) { - QFile f(analyzerInfoFile + '.' + addon + "-results"); + QFile f(analyzerInfoFile + '.' + addon_name + "-results"); if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&f); out << errout; } } - parseClangErrors(addon, fileName, errout); + parseClangErrors(addon_name, fileName, errout); } else { const QString python = CheckThread::pythonCmd(); - if (python.isEmpty()) + if (python.isEmpty()) { + emit showCheckError("python-error", + tr("Addon error").arg(addon_name), + tr("Python is not available.\nAddons will be disabled for current check.").arg(addon_name)); + disabled_addons.append(addon_name); continue; + } - const QString addonFilePath = CheckThread::getAddonFilePath(mDataDir, addon + ".py"); - if (addonFilePath.isEmpty()) + const QString addonFilePath = CheckThread::getAddonFilePath(mDataDir, addon_name + ".py"); + if (addonFilePath.isEmpty()) { + emit showCheckError(addon_name + "-error", + tr("Addon error"), + tr("Addon file %1 is not available.\n%2 addon will be disabled for current check.").arg(addonFilePath).arg(addon_name)); + disabled_addons.append(addon_name); continue; + } if (dumpFile.isEmpty()) { const std::string buildDir = mCppcheck.settings().buildDir; @@ -296,32 +312,27 @@ void CheckThread::runAddonsAndTools(const ImportProject::FileSettings *fileSetti mCppcheck.settings().buildDir = buildDir; } - QStringList args; - args << addonFilePath << "--cli" << dumpFile; - if (addon == "misra" && !mMisraFile.isEmpty() && QFileInfo(mMisraFile).exists()) { - if (mMisraFile.endsWith(".pdf", Qt::CaseInsensitive)) - args << "--misra-pdf=" + mMisraFile; - else - args << "--rule-texts=" + mMisraFile; - } - qDebug() << python << args; + try { + auto addon = Addon(addonFilePath.toStdString(), mCppcheck.settings().exename); + if (addon_name == "misra" && !mMisraFile.isEmpty() && QFileInfo(mMisraFile).exists()) + addon.appendArgs("--rule-texts=" + mMisraFile.toStdString()); - QProcess process; - QProcessEnvironment env = process.processEnvironment(); - if (!env.contains("PYTHONHOME") && !python.startsWith("python")) { - env.insert("PYTHONHOME", QFileInfo(python).canonicalPath()); - process.setProcessEnvironment(env); - } - process.start(python, args); - process.waitForFinished(); - const QString output(process.readAllStandardOutput()); - QFile f(dumpFile + '-' + addon + "-results"); - if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream out(&f); - out << output; - f.close(); + // Set appropriate PYTHONPATH if user was choice non-standard value in settings + QProcess process; + QProcessEnvironment env = process.processEnvironment(); + if (!env.contains("PYTHONHOME") && !python.startsWith("python")) + addon.setEnv("PYTHONHOME", QFileInfo(python).canonicalPath().toStdString()); + + auto results = QString::fromStdString(addon.execute(dumpFile.toStdString())); + parseAddonErrors(results, addon_name); + } catch (const InternalError &e) { + emit showCheckError(addon_name + "-error", + tr("Addon error"), + + tr("There is some problems with '%1' addon: %2.\nIt will be disabled for current check.").arg(QString::fromStdString(e.errorMessage))); + disabled_addons.append(addon_name); + continue; } - parseAddonErrors(output, addon); } } } @@ -332,7 +343,7 @@ void CheckThread::stop() mCppcheck.terminate(); } -void CheckThread::parseAddonErrors(QString err, const QString &tool) +void CheckThread::parseAddonErrors(QString &err, const QString &tool) { Q_UNUSED(tool); QTextStream in(&err, QIODevice::ReadOnly); diff --git a/gui/checkthread.h b/gui/checkthread.h index c92ff2d4af1..8258f5645cf 100644 --- a/gui/checkthread.h +++ b/gui/checkthread.h @@ -114,6 +114,10 @@ class CheckThread : public QThread { void done(); void fileChecked(const QString &file); + + /** @brief Show error occurred during check. */ + void showCheckError(const QString &id, const QString &title, const QString &text, bool once = true); + protected: /** @@ -144,7 +148,13 @@ class CheckThread : public QThread { private: void runAddonsAndTools(const ImportProject::FileSettings *fileSettings, const QString &fileName); - void parseAddonErrors(QString err, const QString &tool); + /** + * @brief Fill GUI representation with errors from addon output + * @param err Addon output + * @param tool Name of addon + */ + void parseAddonErrors(QString &err, const QString &tool); + void parseClangErrors(const QString &tool, const QString &file0, QString err); bool isSuppressed(const Suppressions::ErrorMessage &errorMessage) const; diff --git a/gui/threadhandler.cpp b/gui/threadhandler.cpp index 103da76315f..4a32305564a 100644 --- a/gui/threadhandler.cpp +++ b/gui/threadhandler.cpp @@ -131,6 +131,8 @@ void ThreadHandler::setThreadCount(const int count) this, &ThreadHandler::threadDone); connect(mThreads.last(), &CheckThread::fileChecked, &mResults, &ThreadResult::fileChecked); + connect(mThreads.last(), &CheckThread::showCheckError, + &mResults, &ThreadResult::showCheckError); } } @@ -143,6 +145,8 @@ void ThreadHandler::removeThreads() this, &ThreadHandler::threadDone); disconnect(mThreads[i], &CheckThread::fileChecked, &mResults, &ThreadResult::fileChecked); + disconnect(mThreads[i], &CheckThread::showCheckError, + &mResults, &ThreadResult::showCheckError); delete mThreads[i]; } diff --git a/gui/threadresult.cpp b/gui/threadresult.cpp index 0bc894f8081..943e0599416 100644 --- a/gui/threadresult.cpp +++ b/gui/threadresult.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include "common.h" #include "erroritem.h" #include "errorlogger.h" @@ -57,6 +59,14 @@ void ThreadResult::fileChecked(const QString &file) } } +void ThreadResult::showCheckError(const QString &id, const QString &title, const QString &text, bool once) +{ + if (!mShownErrorIds.contains(id) || !once) { + mShownErrorIds.insert(id); + QMessageBox::critical(NULL, title, text); + } +} + void ThreadResult::reportErr(const ErrorLogger::ErrorMessage &msg) { QMutexLocker locker(&mutex); diff --git a/gui/threadresult.h b/gui/threadresult.h index 693dd8cb229..5c78720373a 100644 --- a/gui/threadresult.h +++ b/gui/threadresult.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "errorlogger.h" #include "importproject.h" @@ -82,6 +83,16 @@ public slots: * @param file File that is checked */ void fileChecked(const QString &file); + + /** + * @brief Display an error occurred during check. + * @param id Unique error identifier + * @param title Error title + * @param text Error text + * @param once If true, show error with this identifier only once + */ + void showCheckError(const QString &id, const QString &title, const QString &text, bool once = true); + signals: /** * @brief Progress signal @@ -150,6 +161,11 @@ public slots: * */ unsigned long mTotalFiles; + + /** + * @brief Identifiers of errors occurred during check that has been shown. + */ + QSet mShownErrorIds; }; /// @} #endif // THREADRESULT_H diff --git a/lib/addonutils.cpp b/lib/addonutils.cpp new file mode 100644 index 00000000000..e361cd3c14d --- /dev/null +++ b/lib/addonutils.cpp @@ -0,0 +1,189 @@ +/* + * Cppcheck - A tool for static C/C++ code analysis + * Copyright (C) 2007-2019 Cppcheck team. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "addonutils.h" + +#include "path.h" +#include "utils.h" + +#define PICOJSON_USE_INT64 +#include +#include +#include +#include +#include + +static inline void throwAddonError(const std::string &msg) +{ + throw InternalError(nullptr, msg, InternalError::ADDON_ERROR); +} + +Addon::Addon(const std::string &addonFileName, const std::string &exename) + : mArgs("") +{ + getAddonInfo(addonFileName, exename); +} + +Addon::~Addon() {} + +void Addon::getAddonInfo(const std::string &fileName, const std::string &exename) +{ + if (fileName.find(".") == std::string::npos) + return getAddonInfo(fileName + ".py", exename); + + if (endsWith(fileName, ".py", 3)) { + mScriptFile = getFullPath(fileName, exename); + if (mScriptFile.empty()) + throwAddonError("Did not find addon " + fileName); + + std::string::size_type pos1 = mScriptFile.rfind("/"); + if (pos1 == std::string::npos) + pos1 = 0; + else + pos1++; + std::string::size_type pos2 = mScriptFile.rfind("."); + if (pos2 < pos1) + pos2 = std::string::npos; + mName = mScriptFile.substr(pos1, pos2 - pos1); + + return; + } else { + if (!endsWith(fileName, ".json", 5)) + throwAddonError("Failed to open addon " + fileName); + + std::ifstream fin(fileName); + if (!fin.is_open()) + throwAddonError("Failed to open " + fileName); + + picojson::value json; + fin >> json; + if (!json.is()) + throwAddonError("Loading " + fileName + " failed. Bad json."); + picojson::object obj = json.get(); + if (obj.count("args")) { + if (!obj["args"].is()) + throwAddonError("Loading " + fileName + " failed. args must be array."); + for (const picojson::value &v : obj["args"].get()) + mArgs += " " + v.get(); + } + + return getAddonInfo(obj["script"].get(), exename); + } +} + +void Addon::appendArgs(const std::string &arg) +{ + if (arg.empty()) + return; + + if (arg[0] != ' ') + mArgs.append(' ' + arg); + else + mArgs.append(arg); +} + +void Addon::setEnv(const std::string &variable, const std::string &value) +{ + if (variable.empty()) + return; + mEnv[variable] = value; +} + +void Addon::clearEnv() +{ + mEnv.clear(); +} + +std::string Addon::getFullPath(const std::string &fileName, const std::string &exename) const +{ + if (Path::fileExists(fileName)) + return fileName; + + const std::string exepath = Path::getPathFromFilename(exename); + if (Path::fileExists(exepath + fileName)) + return exepath + fileName; + if (Path::fileExists(exepath + "addons/" + fileName)) + return exepath + "addons/" + fileName; + +#ifdef FILESDIR + if (Path::fileExists(FILESDIR + ("/" + fileName))) + return FILESDIR + ("/" + fileName); + if (Path::fileExists(FILESDIR + ("/addons/" + fileName))) + return FILESDIR + ("/addons/" + fileName); +#endif + return ""; +} + +std::string Addon::getEnvString() const +{ + std::string result = ""; + for (auto const &var : mEnv) + result += (var.first + "=" + var.second + " "); + return result; +} + +std::string Addon::execute(const std::string &dumpFile) const +{ + const std::string cmd = getEnvString() + "python \"" + mScriptFile + "\" --cli" + mArgs + " \"" + dumpFile + "\""; + +#ifdef _WIN32 + std::unique_ptr pipe(_popen(cmd.c_str(), "r"), _pclose); +#else + std::unique_ptr pipe(popen(cmd.c_str(), "r"), pclose); +#endif + if (!pipe) + return ""; + char buffer[1024]; + std::string result; + while (fgets(buffer, sizeof(buffer), pipe.get()) != nullptr) { + result += buffer; + } + return result; +} + +std::unique_ptr Addon::getErrorMessage(const std::string &line) const +{ + if (line.compare(0,1,"{") != 0) + return nullptr; + + picojson::value res; + std::istringstream istr2(line); + istr2 >> res; + if (!res.is()) + return nullptr; + + picojson::object obj = res.get(); + + const std::string fileName = obj["file"].get(); + const int64_t lineNumber = obj["linenr"].get(); + const int64_t column = obj["column"].get(); + + std::unique_ptr errmsg(new(ErrorLogger::ErrorMessage)); + + errmsg->callStack.emplace_back(ErrorLogger::ErrorMessage::FileLocation(fileName, lineNumber, column)); + + errmsg->id = obj["addon"].get() + "-" + obj["errorId"].get(); + const std::string text = obj["message"].get(); + errmsg->setmsg(text); + const std::string severity = obj["severity"].get(); + errmsg->severity = Severity::fromString(severity); + if (errmsg->severity == Severity::SeverityType::none) + return nullptr; + errmsg->file0 = fileName; + + return errmsg; +} \ No newline at end of file diff --git a/lib/addonutils.h b/lib/addonutils.h new file mode 100644 index 00000000000..133107a5a07 --- /dev/null +++ b/lib/addonutils.h @@ -0,0 +1,115 @@ +/* + * Cppcheck - A tool for static C/C++ code analysis + * Copyright (C) 2007-2019 Cppcheck team. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ADDONUTILS_H +#define ADDONUTILS_H + +#include "errorlogger.h" +#include "path.h" + +#include +#include +#include + +/** + * @brief Class that represent Cppcheck addon -- external Python scipt that executed through Cppcheck. + * + * Addons can be configured through JSON files which contains path to script and additional arguments. + */ +class CPPCHECKLIB Addon { + + friend class TestAddonUtils; + +public: + /** + * @param addonFileName Path to addon script file with .py or .json extension + * @param exename Path to Cppcheck executable + */ + Addon(const std::string &addonFileName, const std::string &exename); + ~Addon(); + + /** + * @brief Execute addon. + * @param dumpFile Path to .dump file generated by cppcheck + * @return Addon output. + */ + std::string execute(const std::string &dumpFile) const; + + /** + * @brief Geneate ErrorMessage object from input line + * @param line Line generated due addon execution + * @return Pointer to ErrorMessage object if input contains error message, nullptr otherwise + * + * TODO: Replace with std::optional when switching to C++17. + */ + std::unique_ptr getErrorMessage(const std::string &line) const; + + /** + * @brief Add command line arguments to addon execution + * @param arg Argument to add + */ + void appendArgs(const std::string &arg); + + /** + * @brief Set value for environment variable used in addon execution + * @param variable environment variable + * @param value value to set + */ + void setEnv(const std::string &variable, const std::string &value); + + /** + * @brief Remove all environment settings + */ + void clearEnv(); + +private: + /** + * @brief Initialization helper used to set addon configuration properely. + * @param fileName Path to addon file. Both .py and .json addons are supported. + * @param exename Path to Cppcheck executable + */ + void getAddonInfo(const std::string &fileName, const std::string &exename); + + /** + * @brief Return absolute path to a addon file with respect of addons configuration. + * @param fileName Path to the file to check. + * @param exename Path to Cppcheck executable + * @return Absolute path to a addon file if success, empty string otherwise + */ + std::string getFullPath(const std::string &fileName, const std::string &exename) const; + + /** + * @brief Generate string with environment variables + * @return Space-separated string with variables in following format: "VAR1=21 VAR2=42" + */ + std::string getEnvString() const; + +private: + /** @brief Name of addon. */ + std::string mName; + + /** @brief Absolute path to addon script file */ + std::string mScriptFile; + + /** @brief Additional command line arguments */ + std::string mArgs; + + /** @brief Environment variables for addons execution. */ + std::map mEnv; +}; + +#endif // ADDONUTILS_H \ No newline at end of file diff --git a/lib/cppcheck.cpp b/lib/cppcheck.cpp index 7d4bce36e62..a0afb6a65e3 100644 --- a/lib/cppcheck.cpp +++ b/lib/cppcheck.cpp @@ -17,6 +17,7 @@ */ #include "cppcheck.h" +#include "addonutils.h" #include "check.h" #include "checkunusedfunctions.h" #include "ctu.h" @@ -34,8 +35,6 @@ #include "exprengine.h" -#define PICOJSON_USE_INT64 -#include #include #include #include @@ -60,95 +59,6 @@ static TimerResults S_timerResults; // CWE ids used static const CWE CWE398(398U); // Indicator of Poor Code Quality -namespace { - struct AddonInfo { - std::string name; - std::string scriptFile; - std::string args; - - static std::string getFullPath(const std::string &fileName, const std::string &exename) { - if (Path::fileExists(fileName)) - return fileName; - - const std::string exepath = Path::getPathFromFilename(exename); - if (Path::fileExists(exepath + fileName)) - return exepath + fileName; - if (Path::fileExists(exepath + "addons/" + fileName)) - return exepath + "addons/" + fileName; - -#ifdef FILESDIR - if (Path::fileExists(FILESDIR + ("/" + fileName))) - return FILESDIR + ("/" + fileName); - if (Path::fileExists(FILESDIR + ("/addons/" + fileName))) - return FILESDIR + ("/addons/" + fileName); -#endif - return ""; - } - - std::string getAddonInfo(const std::string &fileName, const std::string &exename) { - if (fileName.find(".") == std::string::npos) - return getAddonInfo(fileName + ".py", exename); - - if (endsWith(fileName, ".py", 3)) { - scriptFile = getFullPath(fileName, exename); - if (scriptFile.empty()) - return "Did not find addon " + fileName; - - std::string::size_type pos1 = scriptFile.rfind("/"); - if (pos1 == std::string::npos) - pos1 = 0; - else - pos1++; - std::string::size_type pos2 = scriptFile.rfind("."); - if (pos2 < pos1) - pos2 = std::string::npos; - name = scriptFile.substr(pos1, pos2 - pos1); - - return ""; - } - - if (!endsWith(fileName, ".json", 5)) - return "Failed to open addon " + fileName; - - std::ifstream fin(fileName); - if (!fin.is_open()) - return "Failed to open " + fileName; - picojson::value json; - fin >> json; - if (!json.is()) - return "Loading " + fileName + " failed. Bad json."; - picojson::object obj = json.get(); - if (obj.count("args")) { - if (!obj["args"].is()) - return "Loading " + fileName + " failed. args must be array."; - for (const picojson::value &v : obj["args"].get()) - args += " " + v.get(); - } - - return getAddonInfo(obj["script"].get(), exename); - } - }; -} - -static std::string executeAddon(const AddonInfo &addonInfo, const std::string &dumpFile) -{ - const std::string cmd = "python \"" + addonInfo.scriptFile + "\" --cli" + addonInfo.args + " \"" + dumpFile + "\""; - -#ifdef _WIN32 - std::unique_ptr pipe(_popen(cmd.c_str(), "r"), _pclose); -#else - std::unique_ptr pipe(popen(cmd.c_str(), "r"), pclose); -#endif - if (!pipe) - return ""; - char buffer[1024]; - std::string result; - while (fgets(buffer, sizeof(buffer), pipe.get()) != nullptr) { - result += buffer; - } - return result; -} - static std::vector split(const std::string &str, const std::string &sep) { std::vector ret; @@ -609,48 +519,21 @@ unsigned int CppCheck::checkFile(const std::string& filename, const std::string if (!mSettings.addons.empty()) { fdump.close(); - for (const std::string &addon : mSettings.addons) { - struct AddonInfo addonInfo; - const std::string &failedToGetAddonInfo = addonInfo.getAddonInfo(addon, mSettings.exename); - if (!failedToGetAddonInfo.empty()) { - reportOut(failedToGetAddonInfo); + for (const std::string &addon_file : mSettings.addons) { + try { + auto addon = Addon(addon_file, mSettings.exename); + const std::string results = addon.execute(dumpFile); + std::istringstream istr(results); + std::string line; + while (std::getline(istr, line)) { + auto errmsg = addon.getErrorMessage(line); + if (errmsg) + reportErr(*errmsg); + } + } catch (const InternalError &e) { + reportOut(e.errorMessage); continue; } - const std::string results = executeAddon(addonInfo, dumpFile); - std::istringstream istr(results); - std::string line; - - while (std::getline(istr, line)) { - if (line.compare(0,1,"{") != 0) - continue; - - picojson::value res; - std::istringstream istr2(line); - istr2 >> res; - if (!res.is()) - continue; - - picojson::object obj = res.get(); - - const std::string fileName = obj["file"].get(); - const int64_t lineNumber = obj["linenr"].get(); - const int64_t column = obj["column"].get(); - - ErrorLogger::ErrorMessage errmsg; - - errmsg.callStack.emplace_back(ErrorLogger::ErrorMessage::FileLocation(fileName, lineNumber, column)); - - errmsg.id = obj["addon"].get() + "-" + obj["errorId"].get(); - const std::string text = obj["message"].get(); - errmsg.setmsg(text); - const std::string severity = obj["severity"].get(); - errmsg.severity = Severity::fromString(severity); - if (errmsg.severity == Severity::SeverityType::none) - continue; - errmsg.file0 = fileName; - - reportErr(errmsg); - } } } diff --git a/lib/cppcheck.vcxproj b/lib/cppcheck.vcxproj index 470bcf4d1a5..46fba740703 100644 --- a/lib/cppcheck.vcxproj +++ b/lib/cppcheck.vcxproj @@ -38,6 +38,7 @@ + Create @@ -101,6 +102,7 @@ + diff --git a/lib/cppcheck.vcxproj.filters b/lib/cppcheck.vcxproj.filters index f41796ad145..8e88b435802 100644 --- a/lib/cppcheck.vcxproj.filters +++ b/lib/cppcheck.vcxproj.filters @@ -137,6 +137,9 @@ Source Files + + Source Files + Source Files @@ -301,6 +304,9 @@ Header Files + + Header Files + Header Files diff --git a/lib/errorlogger.cpp b/lib/errorlogger.cpp index 781f852b8f6..6b832d64c13 100644 --- a/lib/errorlogger.cpp +++ b/lib/errorlogger.cpp @@ -55,6 +55,9 @@ InternalError::InternalError(const Token *tok, const std::string &errorMsg, Type case INSTANTIATION: id = "instantiationError"; break; + case ADDON_ERROR: + id = "addonError"; + break; } } diff --git a/lib/errorlogger.h b/lib/errorlogger.h index c442c8620e5..2550505e7d5 100644 --- a/lib/errorlogger.h +++ b/lib/errorlogger.h @@ -54,7 +54,7 @@ namespace tinyxml2 { /** @brief Simple container to be thrown when internal error is detected. */ struct InternalError { - enum Type {AST, SYNTAX, UNKNOWN_MACRO, INTERNAL, LIMIT, INSTANTIATION}; + enum Type {AST, SYNTAX, UNKNOWN_MACRO, INTERNAL, LIMIT, INSTANTIATION, ADDON_ERROR}; InternalError(const Token *tok, const std::string &errorMsg, Type type = INTERNAL); const Token *token; std::string errorMessage; diff --git a/lib/lib.pri b/lib/lib.pri index f689e528996..cfe91f4cbe8 100644 --- a/lib/lib.pri +++ b/lib/lib.pri @@ -3,7 +3,8 @@ include($$PWD/pcrerules.pri) include($$PWD/../externals/externals.pri) INCLUDEPATH += $$PWD -HEADERS += $${PWD}/analyzerinfo.h \ +HEADERS += $${PWD}/addonutils.h \ + $${PWD}/analyzerinfo.h \ $${PWD}/astutils.h \ $${PWD}/check.h \ $${PWD}/check64bit.h \ @@ -54,7 +55,8 @@ HEADERS += $${PWD}/analyzerinfo.h \ $${PWD}/tokenlist.h \ $${PWD}/valueflow.h -SOURCES += $${PWD}/analyzerinfo.cpp \ +SOURCES += $${PWD}/addonutils.cpp \ + $${PWD}/analyzerinfo.cpp \ $${PWD}/astutils.cpp \ $${PWD}/check.cpp \ $${PWD}/check64bit.cpp \ diff --git a/test/testaddonutils.cpp b/test/testaddonutils.cpp new file mode 100644 index 00000000000..f56b67d5f61 --- /dev/null +++ b/test/testaddonutils.cpp @@ -0,0 +1,152 @@ +/* + * Cppcheck - A tool for static C/C++ code analysis + * Copyright (C) 2007-2019 Cppcheck team. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "addonutils.h" +#include "settings.h" +#include "testsuite.h" + +#ifdef _WIN32 +#include +#endif // _WIN32 + +#include +#include + +class TestAddonUtils : public TestFixture { +public: + TestAddonUtils() : TestFixture("TestAddonUtils") {} + +private: + Settings settings; + + void run() OVERRIDE { + settings.exename = "cppcheck"; +#ifdef _WIN32 + settings.exename += ".exe"; +#endif // _WIN32 + + TEST_CASE(testConstructors); + TEST_CASE(testConstructorsJson); + TEST_CASE(testBrokenJson); + TEST_CASE(testAppendArgs); + TEST_CASE(testEnv); + } + + void testConstructors() const + { + ASSERT_NO_THROW(Addon("misra.py", settings.exename)); + ASSERT_NO_THROW(Addon("naming.py", settings.exename)); + ASSERT_NO_THROW(Addon("y2038.py", settings.exename)); + ASSERT_NO_THROW(Addon("cert.py", settings.exename)); + + ASSERT_NO_THROW(Addon("misra", settings.exename)); + ASSERT_NO_THROW(Addon("naming", settings.exename)); + ASSERT_NO_THROW(Addon("y2038", settings.exename)); + ASSERT_NO_THROW(Addon("cert", settings.exename)); + + ASSERT_NO_THROW(Addon("naming.json", settings.exename)); + + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("misra.foo", settings.exename), InternalError); + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("misra.", settings.exename), InternalError); + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon(".py", settings.exename), InternalError); + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon(".", settings.exename), InternalError); + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("", settings.exename), InternalError); + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("/dev/null", settings.exename), InternalError); + } + + void testConstructorsJson() const + { + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("test_addon.json", settings.exename), InternalError); + + std::ofstream test_addon { "test_addon.json" }; + test_addon << "{ \"script\": \"addons/misra.py\" }" << std::endl; + ASSERT_NO_THROW(Addon("test_addon.json", settings.exename)); +#ifdef _WIN32 + DeleteFile("test_addon.json"); +#else + std::remove("test_addon.json"); +#endif // _WIN32 + + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("test_addon.json", settings.exename), InternalError); + } + + void testBrokenJson() const + { + { + std::ofstream test_addon { "test_addon_broken.json" }; + test_addon << "{ \"script\": \"addons/misra.py }" << std::endl; // missing quote + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("test_addon_broken.json", settings.exename), InternalError); +#ifdef _WIN32 + DeleteFile("test_addon_broken.json"); +#else + std::remove("test_addon_broken.json"); +#endif // _WIN32 + } + + { + std::ofstream test_addon { "test_addon_broken.json" }; + test_addon << "{ \"script\": \"addons/misra.py \"" << std::endl; // missing } + // cppcheck-suppress unusedScopedObject + ASSERT_THROW(Addon("test_addon_broken.json", settings.exename), InternalError); +#ifdef _WIN32 + DeleteFile("test_addon_broken.json"); +#else + std::remove("test_addon_broken.json"); +#endif // _WIN32 + } + } + + void testAppendArgs() const + { + auto addon = Addon("misra.py", settings.exename); + auto saved_args = addon.mArgs; + + addon.appendArgs(""); + ASSERT_EQUALS(saved_args, addon.mArgs); + + addon.appendArgs("--arg1"); + ASSERT_EQUALS(addon.mArgs[0], ' '); + } + + void testEnv() const + { + auto addon = Addon("misra.py", settings.exename); + + ASSERT_EQUALS(addon.getEnvString(), ""); + + addon.setEnv("VAR1", "21"); + ASSERT_EQUALS(addon.getEnvString(), "VAR1=21 "); + addon.setEnv("VAR2", "42"); + ASSERT_EQUALS(addon.getEnvString(), "VAR1=21 VAR2=42 "); + addon.setEnv("VAR1", "42"); + ASSERT_EQUALS(addon.getEnvString(), "VAR1=42 VAR2=42 "); + + addon.clearEnv(); + ASSERT_EQUALS(addon.getEnvString(), ""); + } +}; + +REGISTER_TEST(TestAddonUtils) diff --git a/test/testrunner.vcxproj b/test/testrunner.vcxproj index d37a615042d..9f987841002 100755 --- a/test/testrunner.vcxproj +++ b/test/testrunner.vcxproj @@ -1,4 +1,4 @@ - + @@ -33,6 +33,7 @@ + diff --git a/test/testrunner.vcxproj.filters b/test/testrunner.vcxproj.filters index 9c413e6e9af..4af6bd6b639 100644 --- a/test/testrunner.vcxproj.filters +++ b/test/testrunner.vcxproj.filters @@ -208,6 +208,9 @@ Source Files + + Source Files + Source Files