diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 842d597b0a4..74ad2085b5a 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -2233,6 +2233,8 @@ bool CmdLineParser::loadAddons(Settings& settings) result = false; continue; } + if (!settings.buildDir.empty()) + addonInfo.initContentHash(); settings.addonInfos.emplace_back(std::move(addonInfo)); } return result; diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 5ddb790b77b..143f0d8a8d7 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -1259,8 +1259,11 @@ bool MainWindow::getCppcheckSettings(Settings& settings, Suppressions& supprs) settings.severity.enable(Severity::portability); settings.severity.enable(Severity::information); settings.checks.enable(Checks::missingInclude); - if (!settings.buildDir.empty()) + if (!settings.buildDir.empty()) { settings.checks.enable(Checks::unusedFunction); + for (AddonInfo& addonInfo : settings.addonInfos) + addonInfo.initContentHash(); + } settings.debugwarnings = mSettings->value(SETTINGS_SHOW_DEBUG_WARNINGS, false).toBool(); settings.quiet = false; settings.verbose = true; diff --git a/lib/addoninfo.cpp b/lib/addoninfo.cpp index 388484916ea..c1368899543 100644 --- a/lib/addoninfo.cpp +++ b/lib/addoninfo.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "json.h" @@ -209,6 +210,7 @@ std::string AddonInfo::getAddonInfo(const std::string &fileName, const std::stri std::ifstream fin(fileName); if (!fin.is_open()) return "Failed to open " + fileName; + jsonFile = Path::fromNativeSeparators(fileName); if (name.empty()) { name = Path::fromNativeSeparators(fileName); if (name.find('/') != std::string::npos) @@ -218,3 +220,45 @@ std::string AddonInfo::getAddonInfo(const std::string &fileName, const std::stri fin >> json; return parseAddonInfo(*this, json, fileName, exename); } + +// split the args as the shell running the addon does +static std::vector splitArgs(const std::string &args) { +#ifdef _WIN32 + const std::string quotes = "\""; +#else + // popen() runs /bin/sh, which also removes single quotes + const std::string quotes = "\"\'"; +#endif + std::vector ret(1); + char quote = '\0'; + for (const char c : args) { + if (quote == '\0' && quotes.find(c) != std::string::npos) + quote = c; + else if (c == quote) + quote = '\0'; + else if (c == ' ' && quote == '\0') + ret.emplace_back(); + else + ret.back() += c; + } + return ret; +} + +void AddonInfo::initContentHash() { + contentHash.clear(); + if (!jsonFile.empty()) + contentHash += hashFileContent(jsonFile); + if (!scriptFile.empty()) + contentHash += hashFileContent(scriptFile); + if (!executable.empty()) + contentHash += hashFileContent(executable); + // files referenced as OPTION=FILE in args, e.g. --rule-texts=FILE + for (const std::string &arg : splitArgs(args)) { + const std::string::size_type pos = arg.find('='); + if (pos == std::string::npos) + continue; + const std::string path = arg.substr(pos + 1); + if (Path::isFile(path)) + contentHash += hashFileContent(path); + } +} diff --git a/lib/addoninfo.h b/lib/addoninfo.h index 902a26c2e12..f039a2dd376 100644 --- a/lib/addoninfo.h +++ b/lib/addoninfo.h @@ -26,6 +26,7 @@ struct CPPCHECKLIB AddonInfo { std::string name; + std::string jsonFile; // addon json file std::string scriptFile; // addon script std::string executable; // addon executable std::string args; // special extra arguments @@ -34,7 +35,10 @@ struct CPPCHECKLIB AddonInfo { std::string runScript; std::map checkers; // checker name and its requirement + std::string contentHash; // hash of the addon files + std::string getAddonInfo(const std::string &fileName, const std::string &exename, bool debug = false); + void initContentHash(); }; #endif // addonInfoH diff --git a/lib/cppcheck.cpp b/lib/cppcheck.cpp index c2c3060f03f..38cfaaf7400 100644 --- a/lib/cppcheck.cpp +++ b/lib/cppcheck.cpp @@ -888,6 +888,7 @@ std::size_t CppCheck::calculateHash(const Preprocessor& preprocessor, const std: for (const auto &a : mSettings.addonInfos) { toolinfo << a.name; toolinfo << a.args; + toolinfo << a.contentHash; } toolinfo << mSettings.premiumArgs; // TODO: do we need to add more options? diff --git a/lib/utils.cpp b/lib/utils.cpp index b840fc3194b..187adc376e3 100644 --- a/lib/utils.cpp +++ b/lib/utils.cpp @@ -20,6 +20,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -198,3 +202,13 @@ std::vector splitString(const std::string& str, char sep) } return l; } + +std::string hashFileContent(const std::string& path) +{ + std::ifstream fin(path, std::ios::binary); + if (!fin.is_open()) + return ""; + std::ostringstream ostr; + ostr << fin.rdbuf(); + return std::to_string(std::hash {}(ostr.str())); +} diff --git a/lib/utils.h b/lib/utils.h index 410bd7c6040..d09f7d4bc75 100644 --- a/lib/utils.h +++ b/lib/utils.h @@ -431,6 +431,13 @@ static inline T* empty_if_null(T* p) */ CPPCHECKLIB std::vector splitString(const std::string& str, char sep); +/** + * Hash the content of a file. + * @param path The file to read + * @return The hash, empty if the file cannot be opened. + */ +CPPCHECKLIB std::string hashFileContent(const std::string& path); + namespace utils { /** * Drop-in replacement for C++17's std::as_const diff --git a/test/cli/other_test.py b/test/cli/other_test.py index 8e13489e114..cd27efd05e1 100644 --- a/test/cli/other_test.py +++ b/test/cli/other_test.py @@ -2355,6 +2355,59 @@ def test_builddir_hash_check_level(tmp_path): # #13376 assert hash_1 != hash_2 +def test_builddir_hash_addon(tmp_path): + test_file = tmp_path / 'test.c' + with open(test_file, 'wt') as f: + f.write('void f() {}\n') + + addon_script = tmp_path / 'addon1.py' + with open(addon_script, 'wt') as f: + f.write('# addon1.py\n') + + rule_texts = tmp_path / 'rule texts.txt' + with open(rule_texts, 'wt') as f: + f.write('# rule texts.txt\n') + + extra_file = tmp_path / 'extra file.txt' + with open(extra_file, 'wt') as f: + f.write('# extra file.txt\n') + + addon_file = tmp_path / 'addon1.json' + with open(addon_file, 'wt') as f: + f.write(json.dumps({ + 'script': str(addon_script), + 'args': ['"--rule-texts={}"'.format(rule_texts), '--extra="{}"'.format(extra_file)] + })) + + build_dir = tmp_path / 'b1' + os.mkdir(build_dir) + + args = [ + '--addon={}'.format(addon_file), + '--cppcheck-build-dir={}'.format(build_dir), + str(test_file) + ] + + cache_file = build_dir / 'test.a1' + + def get_hash(): + exitcode, stdout, stderr = cppcheck(args) + assert exitcode == 0, stdout + assert stderr == '' + root = ElementTree.fromstring(cache_file.read_text()) + return root.findtext('hash') + + hash_1 = get_hash() + assert get_hash() == hash_1 + + for changed_file in (addon_script, rule_texts, extra_file, addon_file): + with open(changed_file, 'a') as f: + f.write('\n') + hash_2 = get_hash() + assert hash_2 != hash_1 + hash_1 = hash_2 + + def test_def_undef(tmp_path): test_file = tmp_path / 'test.c' with open(test_file, 'wt') as f: diff --git a/test/testcppcheck.cpp b/test/testcppcheck.cpp index d88daceb0d9..59e7aaef0a5 100644 --- a/test/testcppcheck.cpp +++ b/test/testcppcheck.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,13 @@ class TestCppcheck : public TestFixture { TEST_CASE(getDumpFileContentsLibrary); TEST_CASE(checkPlistOutput); TEST_CASE(premiumResultsCache); + TEST_CASE(addonScriptHash); + TEST_CASE(addonJsonHash); + TEST_CASE(addonArgsHash); + TEST_CASE(addonQuotedArgsHash); +#ifndef _WIN32 + TEST_CASE(addonSingleQuotedArgsHash); +#endif TEST_CASE(purgedConfiguration); TEST_CASE(recheckInclude); } @@ -612,6 +620,77 @@ class TestCppcheck : public TestFixture { ASSERT(hash1 != hash2); } + void addonHashInternal(AddonInfo addon, const ScopedFile& file) const + { + Settings settings; + Suppressions supprs; + ErrorLogger2 errorLogger; + + std::vector files; + + const char code[] = "void f();\nint x;\n"; + simplecpp::TokenList tokens(code, files, "m1.c"); + + Preprocessor preprocessor(tokens, settings, errorLogger, Standards::Language::C); + ASSERT(preprocessor.loadAllIncludes(files)); + + addon.name = "testaddon"; + settings.addons.insert(addon.name); + settings.addonInfos.push_back(std::move(addon)); + settings.addonInfos.back().initContentHash(); + + CppCheck check(settings, supprs, errorLogger, nullptr, false, {}); + const size_t hash1 = check.calculateHash(preprocessor); + + settings.addonInfos.back().initContentHash(); + ASSERT_EQUALS(hash1, check.calculateHash(preprocessor)); + + { + std::ofstream of(file.path()); + of << "changed content\n"; + } + settings.addonInfos.back().initContentHash(); + // cppcheck-suppress knownConditionTrueFalse + ASSERT(hash1 != check.calculateHash(preprocessor)); + } + + void addonScriptHash() const { + ScopedFile addon_script("testaddon.py", "# addon v1\n"); + AddonInfo addon; + addon.scriptFile = addon_script.path(); + addonHashInternal(addon, addon_script); + } + + void addonJsonHash() const { + ScopedFile addon_config("testaddon.json", "{ \"script\": \"x.py\" }\n"); + AddonInfo addon; + addon.jsonFile = addon_config.path(); + addonHashInternal(addon, addon_config); + } + + void addonArgsHash() const { + ScopedFile rule_texts("testrules.txt", "rule v1\n"); + AddonInfo addon; + addon.args = "--rule-texts=" + rule_texts.path(); + addonHashInternal(addon, rule_texts); + } + + void addonQuotedArgsHash() const { + ScopedFile rule_texts("test rules.txt", "rule v1\n"); + AddonInfo addon; + addon.args = "--rule-texts=\"" + rule_texts.path() + "\""; + addonHashInternal(addon, rule_texts); + } + +#ifndef _WIN32 + void addonSingleQuotedArgsHash() const { + ScopedFile rule_texts("test rules.txt", "rule v1\n"); + AddonInfo addon; + addon.args = "--rule-texts='" + rule_texts.path() + "'"; + addonHashInternal(addon, rule_texts); + } +#endif + void purgedConfiguration() const { ScopedFile test_file("test.cpp",