From 42f3717765ab78c72de6fe12649c70df6d7b6565 Mon Sep 17 00:00:00 2001 From: glank Date: Sun, 6 Jul 2025 02:41:20 +0200 Subject: [PATCH 01/35] Rework PathMatch --- lib/importproject.cpp | 45 +++++++-------- lib/pathmatch.cpp | 128 +++++++++++++++++++++++------------------ lib/pathmatch.h | 46 ++++++++++----- test/testpathmatch.cpp | 52 ++++++++++------- 4 files changed, 157 insertions(+), 114 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index d37ad71c2c9..0a4c7e6b05b 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -19,6 +19,7 @@ #include "importproject.h" #include "path.h" +#include "pathmatch.h" #include "settings.h" #include "standards.h" #include "suppressions.h" @@ -42,30 +43,11 @@ #include "json.h" -// TODO: align the exclusion logic with PathMatch -// TODO: PathMatch lacks glob support void ImportProject::ignorePaths(const std::vector &ipaths, bool debug) { + PathMatch matcher(ipaths); for (auto it = fileSettings.cbegin(); it != fileSettings.cend();) { - bool ignore = false; - for (std::string i : ipaths) { - if (it->filename().size() > i.size() && it->filename().compare(0,i.size(),i)==0) { - ignore = true; - break; - } - if (isValidGlobPattern(i) && matchglob(i, it->filename())) { - ignore = true; - break; - } - if (!Path::isAbsolute(i)) { - i = mPath + i; - if (it->filename().size() > i.size() && it->filename().compare(0,i.size(),i)==0) { - ignore = true; - break; - } - } - } - if (ignore) { + if (matcher.match(it->filename())) { if (debug) std::cout << "ignored path: " << it->filename() << std::endl; it = fileSettings.erase(it); @@ -1274,6 +1256,23 @@ static std::list readXmlStringList(const tinyxml2::XMLElement *node return ret; } +static std::list readXmlPathMatchList(const tinyxml2::XMLElement *node, const std::string &path, const char name[], const char attribute[]) +{ + std::list ret; + for (const tinyxml2::XMLElement *child = node->FirstChildElement(); child; child = child->NextSiblingElement()) { + if (strcmp(child->Name(), name) != 0) + continue; + const char *attr = attribute ? child->Attribute(attribute) : child->GetText(); + if (attr) { + if (attr[0] == '.') + ret.push_back(joinRelativePath(path, attr)); + else + ret.emplace_back(attr); + } + } + return ret; +} + static std::string join(const std::list &strlist, const char *sep) { std::string ret; @@ -1339,13 +1338,13 @@ bool ImportProject::importCppcheckGuiProject(std::istream &istr, Settings &setti else if (strcmp(name, CppcheckXml::PathsElementName) == 0) paths = readXmlStringList(node, path, CppcheckXml::PathName, CppcheckXml::PathNameAttrib); else if (strcmp(name, CppcheckXml::ExcludeElementName) == 0) - guiProject.excludedPaths = readXmlStringList(node, "", CppcheckXml::ExcludePathName, CppcheckXml::ExcludePathNameAttrib); // TODO: append instead of overwrite + guiProject.excludedPaths = readXmlPathMatchList(node, path, CppcheckXml::ExcludePathName, CppcheckXml::ExcludePathNameAttrib); // TODO: append instead of overwrite else if (strcmp(name, CppcheckXml::FunctionContracts) == 0) ; else if (strcmp(name, CppcheckXml::VariableContractsElementName) == 0) ; else if (strcmp(name, CppcheckXml::IgnoreElementName) == 0) - guiProject.excludedPaths = readXmlStringList(node, "", CppcheckXml::IgnorePathName, CppcheckXml::IgnorePathNameAttrib); // TODO: append instead of overwrite + guiProject.excludedPaths = readXmlPathMatchList(node, path, CppcheckXml::IgnorePathName, CppcheckXml::IgnorePathNameAttrib); // TODO: append instead of overwrite else if (strcmp(name, CppcheckXml::LibrariesElementName) == 0) guiProject.libraries = readXmlStringList(node, "", CppcheckXml::LibraryElementName, nullptr); // TODO: append instead of overwrite else if (strcmp(name, CppcheckXml::SuppressionsElementName) == 0) { diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 638c2bce005..9b48df4c076 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -19,74 +19,88 @@ #include "pathmatch.h" #include "path.h" -#include "utils.h" #include -#include +#include +#include +#include -PathMatch::PathMatch(std::vector paths, bool caseSensitive) - : mPaths(std::move(paths)), mCaseSensitive(caseSensitive) +/* Escape regex special chars and translate globs to equivalent regex */ +static std::string translate(const std::string &s) { - for (std::string& p : mPaths) - { - p = Path::fromNativeSeparators(p); - if (!mCaseSensitive) - strTolower(p); + std::string r; + std::size_t i = 0; + + while (i != s.size()) { + int c = s[i++]; + + if (std::strchr("\\[](){}+^$|", c) != nullptr) { + r.push_back('\\'); + r.push_back(c); + } else if (c == '*') { + if (i != s.size() && s[i] == '*') { + r.append(".*"); + i++; + } + else { + r.append("[^/]*"); + } + } else if (c == '?') { + r.append("[^/]"); + } else { + r.push_back(c); + } } - // TODO: also make lowercase? - mWorkingDirectory.push_back(Path::fromNativeSeparators(Path::getCurrentPath())); + + return r; } -bool PathMatch::match(const std::string &path) const +PathMatch::PathMatch(const std::vector &paths, bool caseSensitive) { - if (path.empty()) - return false; - - std::string findpath = Path::fromNativeSeparators(path); - if (!mCaseSensitive) - strTolower(findpath); - std::string finddir; - if (!endsWith(findpath,'/')) - finddir = removeFilename(findpath); - else - finddir = findpath; - - const bool is_absolute = Path::isAbsolute(path); - - // TODO: align the match logic with ImportProject::ignorePaths() - for (auto i = mPaths.cbegin(); i != mPaths.cend(); ++i) { - const std::string pathToMatch((!is_absolute && Path::isAbsolute(*i)) ? Path::getRelativePath(*i, mWorkingDirectory) : *i); - - // Filtering directory name - if (endsWith(pathToMatch,'/')) { - if (pathToMatch.length() > finddir.length()) - continue; - // Match relative paths starting with mask - // -isrc matches src/foo.cpp - if (finddir.compare(0, pathToMatch.size(), pathToMatch) == 0) - return true; - // Match only full directory name in middle or end of the path - // -isrc matches myproject/src/ but does not match - // myproject/srcfiles/ or myproject/mysrc/ - if (finddir.find("/" + pathToMatch) != std::string::npos) - return true; - } - // Filtering filename - else { - if (pathToMatch.length() > findpath.length()) - continue; - // Check if path ends with mask - // -ifoo.cpp matches (./)foo.c, src/foo.cpp and proj/src/foo.cpp - // -isrc/file.cpp matches src/foo.cpp and proj/src/foo.cpp - if (findpath.compare(findpath.size() - pathToMatch.size(), findpath.size(), pathToMatch) == 0) - return true; + std::string regex_string; + + for (auto p : paths) { + if (p.empty()) + continue; + + if (!regex_string.empty()) + regex_string.push_back('|'); + + p = Path::fromNativeSeparators(p); + + if (p.front() == '.') + p = Path::getCurrentPath() + "/" + p; + + if (Path::isAbsolute(p)) { + p = Path::simplifyPath(p); + + if (p.back() == '/') + regex_string.append("^" + translate(p)); + else + regex_string.append("^" + translate(p) + "$"); + } else { + if (p.back() == '/') + regex_string.append("/" + translate(p)); + else + regex_string.append("/" + translate(p) + "$"); } } - return false; + + if (caseSensitive) + mRegex = std::regex(regex_string, std::regex_constants::extended); + else + mRegex = std::regex(regex_string, std::regex_constants::extended | std::regex_constants::icase); } -std::string PathMatch::removeFilename(const std::string &path) +bool PathMatch::match(const std::string &path) const { - const std::size_t ind = path.find_last_of('/'); - return path.substr(0, ind + 1); + std::string p; + std::smatch m; + + if (Path::isAbsolute(path)) + p = Path::simplifyPath(path); + else + p = Path::simplifyPath(Path::getCurrentPath() + "/" + path); + + return std::regex_search(p, m, mRegex, std::regex_constants::match_any | std::regex_constants::match_not_null); } diff --git a/lib/pathmatch.h b/lib/pathmatch.h index f0ace4fbc94..25a99e9ed57 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -21,12 +21,43 @@ #include "config.h" +#include #include #include /// @addtogroup CLI /// @{ +/** + * Path matching rules: + * - If a rule looks like an absolute path (e.g. starts with '/', but varies by platform): + * - The rule will be simplified (path separators vary by platform): + * - '/./' => '/' + * - '/dir/../' => '/' + * - '//' => '/' + * - If the rule ends with a path separator, match all files where the rule matches the start of the file's + * simplified absolute path. Globs are allowed in the rule. + * - Otherwise, match all files where the rule matches the file's simplified absolute path. + * Globs are allowed in the rule. + * - If a rule starts with '.': + * - The rule is interpreted as a path relative to the execution directory (when passed to the CLI), + * or the directory containing the project file (when imported), and then converted to an absolute path and + * treatesd as such according to the above procedure. + * - Otherwise: + * - No simplification is done to the rule. + * - If the rule ends with a path separator: + * - Match all files where the rule matches any part of the file's simplified absolute path, and the matching + * part directly follows a path separator. Globs are allowed in the rule. + * - Otherwise: + * - Match all files where the rules matches the end of the file's simplified absolute path, and the matching + * part directly follows a path separator. Globs are allowed in the rule. + * + * - Glob rules: + * - '**' matches any number of characters including path separators. + * - '*' matches any number of characters except path separators. + * - '?' matches any single character except path separators. + **/ + /** * @brief Simple path matching for ignoring paths in CLI. */ @@ -42,7 +73,7 @@ class CPPCHECKLIB PathMatch { * @param caseSensitive Match the case of the characters when * matching paths? */ - explicit PathMatch(std::vector paths, bool caseSensitive = true); + explicit PathMatch(const std::vector &paths, bool caseSensitive = true); /** * @brief Match path against list of masks. @@ -54,19 +85,8 @@ class CPPCHECKLIB PathMatch { */ bool match(const std::string &path) const; -protected: - - /** - * @brief Remove filename part from the path. - * @param path Path to edit. - * @return path without filename part. - */ - static std::string removeFilename(const std::string &path); - private: - std::vector mPaths; - bool mCaseSensitive; - std::vector mWorkingDirectory; + std::regex mRegex; }; /// @} diff --git a/test/testpathmatch.cpp b/test/testpathmatch.cpp index 45fbde54d58..bc94067e72f 100644 --- a/test/testpathmatch.cpp +++ b/test/testpathmatch.cpp @@ -20,7 +20,6 @@ #include "fixture.h" #include -#include #include @@ -29,10 +28,10 @@ class TestPathMatch : public TestFixture { TestPathMatch() : TestFixture("TestPathMatch") {} private: - const PathMatch emptyMatcher{std::vector()}; - const PathMatch srcMatcher{std::vector(1, "src/")}; - const PathMatch fooCppMatcher{std::vector(1, "foo.cpp")}; - const PathMatch srcFooCppMatcher{std::vector(1, "src/foo.cpp")}; + const PathMatch emptyMatcher{{}}; + const PathMatch srcMatcher{{"src/"}}; + const PathMatch fooCppMatcher{{"foo.cpp"}}; + const PathMatch srcFooCppMatcher{{"src/foo.cpp"}}; void run() override { TEST_CASE(emptymaskemptyfile); @@ -67,6 +66,8 @@ class TestPathMatch : public TestFixture { TEST_CASE(filemaskpath3); TEST_CASE(filemaskpath4); TEST_CASE(mixedallmatch); + TEST_CASE(globstar1); + TEST_CASE(globstar2); } // Test empty PathMatch @@ -97,13 +98,12 @@ class TestPathMatch : public TestFixture { } void onemasksamepathdifferentslash() const { - const PathMatch srcMatcher2{std::vector(1, "src\\")}; + PathMatch srcMatcher2({"src\\"}); ASSERT(srcMatcher2.match("src/")); } void onemasksamepathdifferentcase() const { - std::vector masks(1, "sRc/"); - PathMatch match(std::move(masks), false); + PathMatch match({"sRc/"}, false); ASSERT(match.match("srC/")); } @@ -115,7 +115,7 @@ class TestPathMatch : public TestFixture { const std::string longerExclude("longersrc/"); const std::string shorterToMatch("src/"); ASSERT(shorterToMatch.length() < longerExclude.length()); - PathMatch match(std::vector(1, longerExclude)); + PathMatch match({longerExclude}); ASSERT(match.match(longerExclude)); ASSERT(!match.match(shorterToMatch)); } @@ -154,26 +154,22 @@ class TestPathMatch : public TestFixture { } void twomasklongerpath1() const { - std::vector masks = { "src/", "module/" }; - PathMatch match(std::move(masks)); + PathMatch match({ "src/", "module/" }); ASSERT(!match.match("project/")); } void twomasklongerpath2() const { - std::vector masks = { "src/", "module/" }; - PathMatch match(std::move(masks)); + PathMatch match({ "src/", "module/" }); ASSERT(match.match("project/src/")); } void twomasklongerpath3() const { - std::vector masks = { "src/", "module/" }; - PathMatch match(std::move(masks)); + PathMatch match({ "src/", "module/" }); ASSERT(match.match("project/module/")); } void twomasklongerpath4() const { - std::vector masks = { "src/", "module/" }; - PathMatch match(std::move(masks)); + PathMatch match({ "src/", "module/" }); ASSERT(match.match("project/src/module/")); } @@ -183,8 +179,7 @@ class TestPathMatch : public TestFixture { } void filemaskdifferentcase() const { - std::vector masks(1, "foo.cPp"); - PathMatch match(std::move(masks), false); + PathMatch match({"foo.cPp"}, false); ASSERT(match.match("fOo.cpp")); } @@ -219,11 +214,26 @@ class TestPathMatch : public TestFixture { void mixedallmatch() const { // #13570 // when trying to match a directory against a directory entry it erroneously modified a local variable also used for file matching - std::vector masks = { "tests/", "file.c" }; - PathMatch match(std::move(masks)); + PathMatch match({ "tests/", "file.c" }); ASSERT(match.match("tests/")); ASSERT(match.match("lib/file.c")); } + + void globstar1() const { + PathMatch match({"src/**/foo.c"}); + ASSERT(match.match("src/lib/foo/foo.c")); + ASSERT(match.match("src/lib/foo/bar/foo.c")); + ASSERT(!match.match("src/lib/foo/foo.cpp")); + ASSERT(!match.match("src/lib/foo/bar/foo.cpp")); + } + + void globstar2() const { + PathMatch match({"./src/**/foo.c"}); + ASSERT(match.match("src/lib/foo/foo.c")); + ASSERT(match.match("src/lib/foo/bar/foo.c")); + ASSERT(!match.match("src/lib/foo/foo.cpp")); + ASSERT(!match.match("src/lib/foo/bar/foo.cpp")); + } }; REGISTER_TEST(TestPathMatch) From 9521baf523c6df5be673a42eee1accb1315fefbc Mon Sep 17 00:00:00 2001 From: glank Date: Sun, 6 Jul 2025 03:13:42 +0200 Subject: [PATCH 02/35] Fix gui testcase --- test/testimportproject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testimportproject.cpp b/test/testimportproject.cpp index 71b1ba5c6dc..6dc725f0793 100644 --- a/test/testimportproject.cpp +++ b/test/testimportproject.cpp @@ -446,7 +446,7 @@ class TestImportProject : public TestFixture { project.fileSettings = {std::move(fs1), std::move(fs2)}; project.ignorePaths({"*foo", "bar*"}); - ASSERT_EQUALS(2, project.fileSettings.size()); + ASSERT_EQUALS(1, project.fileSettings.size()); project.ignorePaths({"foo/*"}); ASSERT_EQUALS(1, project.fileSettings.size()); From 525bc6f88a0105659d1c249e3ff19ef214ca2e04 Mon Sep 17 00:00:00 2001 From: glank Date: Sun, 6 Jul 2025 03:13:50 +0200 Subject: [PATCH 03/35] Run dmake --- Makefile | 4 ++-- oss-fuzz/Makefile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 188529d82a9..15475971f8c 100644 --- a/Makefile +++ b/Makefile @@ -585,7 +585,7 @@ $(libcppdir)/forwardanalyzer.o: lib/forwardanalyzer.cpp lib/addoninfo.h lib/anal $(libcppdir)/fwdanalysis.o: lib/fwdanalysis.cpp lib/addoninfo.h lib/astutils.h lib/checkers.h lib/config.h lib/errortypes.h lib/fwdanalysis.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/smallvector.h lib/sourcelocation.h lib/standards.h lib/symboldatabase.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/vfvalue.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/fwdanalysis.cpp -$(libcppdir)/importproject.o: lib/importproject.cpp externals/picojson/picojson.h externals/tinyxml2/tinyxml2.h lib/addoninfo.h lib/checkers.h lib/config.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/token.h lib/tokenlist.h lib/utils.h lib/vfvalue.h lib/xml.h +$(libcppdir)/importproject.o: lib/importproject.cpp externals/picojson/picojson.h externals/tinyxml2/tinyxml2.h lib/addoninfo.h lib/checkers.h lib/config.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/token.h lib/tokenlist.h lib/utils.h lib/vfvalue.h lib/xml.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/importproject.cpp $(libcppdir)/infer.o: lib/infer.cpp lib/calculate.h lib/config.h lib/errortypes.h lib/infer.h lib/mathlib.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/valueptr.h lib/vfvalue.h @@ -606,7 +606,7 @@ $(libcppdir)/path.o: lib/path.cpp externals/simplecpp/simplecpp.h lib/config.h l $(libcppdir)/pathanalysis.o: lib/pathanalysis.cpp lib/astutils.h lib/config.h lib/errortypes.h lib/library.h lib/mathlib.h lib/pathanalysis.h lib/smallvector.h lib/sourcelocation.h lib/standards.h lib/symboldatabase.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/vfvalue.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/pathanalysis.cpp -$(libcppdir)/pathmatch.o: lib/pathmatch.cpp lib/config.h lib/path.h lib/pathmatch.h lib/standards.h lib/utils.h +$(libcppdir)/pathmatch.o: lib/pathmatch.cpp lib/config.h lib/path.h lib/pathmatch.h lib/standards.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/pathmatch.cpp $(libcppdir)/platform.o: lib/platform.cpp externals/tinyxml2/tinyxml2.h lib/config.h lib/mathlib.h lib/path.h lib/platform.h lib/standards.h lib/xml.h diff --git a/oss-fuzz/Makefile b/oss-fuzz/Makefile index 88449d09051..0092fcce8f1 100644 --- a/oss-fuzz/Makefile +++ b/oss-fuzz/Makefile @@ -273,7 +273,7 @@ $(libcppdir)/forwardanalyzer.o: ../lib/forwardanalyzer.cpp ../lib/addoninfo.h .. $(libcppdir)/fwdanalysis.o: ../lib/fwdanalysis.cpp ../lib/addoninfo.h ../lib/astutils.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/fwdanalysis.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/settings.h ../lib/smallvector.h ../lib/sourcelocation.h ../lib/standards.h ../lib/symboldatabase.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/vfvalue.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/fwdanalysis.cpp -$(libcppdir)/importproject.o: ../lib/importproject.cpp ../externals/picojson/picojson.h ../externals/tinyxml2/tinyxml2.h ../lib/addoninfo.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/filesettings.h ../lib/importproject.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/platform.h ../lib/settings.h ../lib/standards.h ../lib/suppressions.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h ../lib/xml.h +$(libcppdir)/importproject.o: ../lib/importproject.cpp ../externals/picojson/picojson.h ../externals/tinyxml2/tinyxml2.h ../lib/addoninfo.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/filesettings.h ../lib/importproject.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/pathmatch.h ../lib/platform.h ../lib/settings.h ../lib/standards.h ../lib/suppressions.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h ../lib/xml.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/importproject.cpp $(libcppdir)/infer.o: ../lib/infer.cpp ../lib/calculate.h ../lib/config.h ../lib/errortypes.h ../lib/infer.h ../lib/mathlib.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/valueptr.h ../lib/vfvalue.h @@ -294,7 +294,7 @@ $(libcppdir)/path.o: ../lib/path.cpp ../externals/simplecpp/simplecpp.h ../lib/c $(libcppdir)/pathanalysis.o: ../lib/pathanalysis.cpp ../lib/astutils.h ../lib/config.h ../lib/errortypes.h ../lib/library.h ../lib/mathlib.h ../lib/pathanalysis.h ../lib/smallvector.h ../lib/sourcelocation.h ../lib/standards.h ../lib/symboldatabase.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/vfvalue.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/pathanalysis.cpp -$(libcppdir)/pathmatch.o: ../lib/pathmatch.cpp ../lib/config.h ../lib/path.h ../lib/pathmatch.h ../lib/standards.h ../lib/utils.h +$(libcppdir)/pathmatch.o: ../lib/pathmatch.cpp ../lib/config.h ../lib/path.h ../lib/pathmatch.h ../lib/standards.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/pathmatch.cpp $(libcppdir)/platform.o: ../lib/platform.cpp ../externals/tinyxml2/tinyxml2.h ../lib/config.h ../lib/mathlib.h ../lib/path.h ../lib/platform.h ../lib/standards.h ../lib/xml.h From dd76d658c41698477bb08d566f24445106dcf2ff Mon Sep 17 00:00:00 2001 From: glank Date: Sun, 6 Jul 2025 04:11:00 +0200 Subject: [PATCH 04/35] Update test cases --- test/cli/more-projects_test.py | 1 - test/cli/other_test.py | 20 -------------------- test/cli/proj2_test.py | 4 ++-- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/test/cli/more-projects_test.py b/test/cli/more-projects_test.py index 57f400fa197..512ad9fdede 100644 --- a/test/cli/more-projects_test.py +++ b/test/cli/more-projects_test.py @@ -705,7 +705,6 @@ def test_project_file_ignore_3(tmpdir): assert_cppcheck(args, ec_exp=1, err_exp=[], out_exp=out_lines) -@pytest.mark.xfail(strict=True) def test_json_file_ignore(tmpdir): test_file = os.path.join(tmpdir, 'test.cpp') with open(test_file, 'wt') as f: diff --git a/test/cli/other_test.py b/test/cli/other_test.py index e430dc3fd06..ff35eb0574f 100644 --- a/test/cli/other_test.py +++ b/test/cli/other_test.py @@ -1764,17 +1764,14 @@ def test_ignore_file_append(tmpdir): __test_ignore_file(tmpdir, 'test.cpp', append=True) -@pytest.mark.xfail(strict=True) # TODO: glob syntax is not supported? def test_ignore_file_wildcard_back(tmpdir): __test_ignore_file(tmpdir, 'test.c*') -@pytest.mark.xfail(strict=True) # TODO: glob syntax is not supported? def test_ignore_file_wildcard_front(tmpdir): __test_ignore_file(tmpdir, '*test.cpp') -@pytest.mark.xfail(strict=True) # TODO: glob syntax is not supported? def test_ignore_file_placeholder(tmpdir): __test_ignore_file(tmpdir, 't?st.cpp') @@ -1787,12 +1784,10 @@ def test_ignore_file_relative_backslash(tmpdir): __test_ignore_file(tmpdir, 'src\\test.cpp') -@pytest.mark.xfail(strict=True) # TODO: glob syntax is not supported? def test_ignore_file_relative_wildcard(tmpdir): __test_ignore_file(tmpdir, 'src/test.c*') -@pytest.mark.xfail(strict=True) # TODO: glob syntax is not supported? def test_ignore_file_relative_wildcard_backslash(tmpdir): __test_ignore_file(tmpdir, 'src\\test.c*') @@ -1805,12 +1800,10 @@ def test_ignore_path_relative_backslash(tmpdir): __test_ignore_file(tmpdir, 'src\\') -@pytest.mark.xfail(strict=True) # TODO: glob syntax is not supported? def test_ignore_path_relative_wildcard(tmpdir): __test_ignore_file(tmpdir, 'src*/') -@pytest.mark.xfail(strict=True) # TODO: glob syntax is not supported? def test_ignore_path_relative_wildcard_backslash(tmpdir): __test_ignore_file(tmpdir, 'src*\\') @@ -1880,17 +1873,14 @@ def test_ignore_project_file_cli_append(tmpdir): __test_ignore_project(tmpdir, ign_proj='test2.cpp', ign_cli='test.cpp', append_cli=True) -@pytest.mark.xfail(strict=True) # TODO: ? def test_ignore_project_file_wildcard_back(tmpdir): __test_ignore_project(tmpdir, 'test.c*') -@pytest.mark.xfail(strict=True) # TODO: ? def test_ignore_project_file_wildcard_front(tmpdir): __test_ignore_project(tmpdir, '*test.cpp') -@pytest.mark.xfail(strict=True) # TODO: ? def test_ignore_project_file_placeholder(tmpdir): __test_ignore_project(tmpdir, 't?st.cpp') @@ -1959,18 +1949,15 @@ def __test_ignore_project_2(tmpdir, extra_args, append=False, inject_path=False) assert stdout.splitlines() == lines_exp -@pytest.mark.xfail(strict=True) # TODO: -i appears to be ignored def test_ignore_project_2_file(tmpdir): __test_ignore_project_2(tmpdir, ['-itest.cpp']) -@pytest.mark.xfail(strict=True) # TODO: -i appears to be ignored def test_ignore_project_2_file_append(tmpdir): # make sure it also matches when specified after project __test_ignore_project_2(tmpdir, ['-itest.cpp'], append=True) -@pytest.mark.xfail(strict=True) # TODO: PathMatch lacks wildcard support / -i appears to be ignored def test_ignore_project_2_file_wildcard_back(tmpdir): __test_ignore_project_2(tmpdir, ['-itest.c*']) @@ -1979,27 +1966,22 @@ def test_ignore_project_2_file_wildcard_front(tmpdir): __test_ignore_project_2(tmpdir, ['-i*test.cpp']) -@pytest.mark.xfail(strict=True) # TODO: PathMatch lacks wildcard support / -i appears to be ignored def test_ignore_project_2_file_placeholder(tmpdir): __test_ignore_project_2(tmpdir, ['-it?st.cpp']) -@pytest.mark.xfail(strict=True) # TODO: -i appears to be ignored def test_ignore_project_2_file_relative(tmpdir): __test_ignore_project_2(tmpdir, ['-isrc/test.cpp']) -@pytest.mark.xfail(strict=True) # TODO: -i appears to be ignored def test_ignore_project_2_file_relative_backslash(tmpdir): __test_ignore_project_2(tmpdir, ['-isrc\\test.cpp']) -@pytest.mark.xfail(strict=True) # TODO: PathMatch lacks wildcard support / -i appears to be ignored def test_ignore_project_2_file_relative_wildcard(tmpdir): __test_ignore_project_2(tmpdir, ['-isrc/test.c*']) -@pytest.mark.xfail(strict=True) # TODO: PathMatch lacks wildcard support / -i appears to be ignored def test_ignore_project_2_file_relative_wildcard_backslash(tmpdir): __test_ignore_project_2(tmpdir, ['-isrc\\test.c*']) @@ -2012,12 +1994,10 @@ def test_ignore_project_2_path_relative_backslash(tmpdir): __test_ignore_project_2(tmpdir, ['-isrc\\']) -@pytest.mark.xfail(strict=True) # TODO: PathMatch lacks wildcard support def test_ignore_project_2_path_relative_wildcard(tmpdir): __test_ignore_project_2(tmpdir, ['-isrc*/']) -@pytest.mark.xfail(strict=True) # TODO: PathMatch lacks wildcard support def test_ignore_project_2_path_relative_wildcard_backslash(tmpdir): __test_ignore_project_2(tmpdir, ['-isrc*\\']) diff --git a/test/cli/proj2_test.py b/test/cli/proj2_test.py index c9516d9ddbf..1059c475198 100644 --- a/test/cli/proj2_test.py +++ b/test/cli/proj2_test.py @@ -100,7 +100,7 @@ def test_gui_project_loads_compile_commands_2(tmp_path): proj_dir = tmp_path / 'proj2' shutil.copytree(__proj_dir, proj_dir) __create_compile_commands(proj_dir) - exclude_path_1 = 'proj2/b' + exclude_path_1 = 'proj2/b/' create_gui_project_file(os.path.join(proj_dir, 'test.cppcheck'), import_project='compile_commands.json', exclude_paths=[exclude_path_1]) @@ -157,7 +157,7 @@ def test_gui_project_loads_relative_vs_solution_2(tmp_path): def test_gui_project_loads_relative_vs_solution_with_exclude(tmp_path): proj_dir = tmp_path / 'proj2' shutil.copytree(__proj_dir, proj_dir) - create_gui_project_file(os.path.join(tmp_path, 'test.cppcheck'), root_path='proj2', import_project='proj2/proj2.sln', exclude_paths=['b']) + create_gui_project_file(os.path.join(tmp_path, 'test.cppcheck'), root_path='proj2', import_project='proj2/proj2.sln', exclude_paths=['b/']) ret, stdout, stderr = cppcheck(['--project=test.cppcheck'], cwd=tmp_path) assert ret == 0, stdout assert stderr == __ERR_A From 7f0b04ea6bd1578513db7e7c340e708904dd6177 Mon Sep 17 00:00:00 2001 From: glank Date: Sun, 6 Jul 2025 04:15:21 +0200 Subject: [PATCH 05/35] Fix empty regex --- lib/pathmatch.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 9b48df4c076..6dea52deda8 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -86,6 +86,9 @@ PathMatch::PathMatch(const std::vector &paths, bool caseSensitive) } } + if (regex_string.empty()) + regex_string = "^$"; + if (caseSensitive) mRegex = std::regex(regex_string, std::regex_constants::extended); else From 8c31bc278693f7fafd0749ecac904b1e32ee9bbe Mon Sep 17 00:00:00 2001 From: glank Date: Sun, 6 Jul 2025 08:01:26 +0200 Subject: [PATCH 06/35] Fix clang-tidy issues --- test/testfilelister.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/testfilelister.cpp b/test/testfilelister.cpp index 048c2e4c17a..9cae2b29c1f 100644 --- a/test/testfilelister.cpp +++ b/test/testfilelister.cpp @@ -27,8 +27,6 @@ #include #include #include -#include -#include class TestFileLister : public TestFixture { public: @@ -62,8 +60,7 @@ class TestFileLister : public TestFixture { // Recursively add add files.. std::list files; - std::vector masks; - PathMatch matcher(std::move(masks)); + PathMatch matcher({}); std::string err = FileLister::recursiveAddFiles(files, adddir, {}, matcher); ASSERT_EQUALS("", err); @@ -118,8 +115,7 @@ class TestFileLister : public TestFixture { const std::string basedir = findBaseDir(); std::list files; - std::vector ignored{"lib/token.cpp"}; - PathMatch matcher(ignored); + PathMatch matcher({"lib/token.cpp"}); std::string err = FileLister::recursiveAddFiles(files, basedir + "lib/token.cpp", {}, matcher); ASSERT_EQUALS("", err); ASSERT(files.empty()); @@ -129,8 +125,7 @@ class TestFileLister : public TestFixture { const std::string basedir = findBaseDir(); std::list files; - std::vector ignored; - PathMatch matcher(ignored); + PathMatch matcher({}); std::string err = FileLister::recursiveAddFiles(files, basedir + "lib/token.cpp", {}, matcher); ASSERT_EQUALS("", err); ASSERT_EQUALS(1, files.size()); @@ -141,8 +136,7 @@ class TestFileLister : public TestFixture { const std::string basedir = findBaseDir() + "."; std::list files; - std::vector ignored{"lib/"}; // needs to end with slash so it matches directories - added by CmdLineParser - PathMatch matcher(ignored); + PathMatch matcher({"lib/"}); // needs to end with slash so it matches directories - added by CmdLineParser std::string err = FileLister::recursiveAddFiles(files, basedir, {}, matcher); ASSERT_EQUALS("", err); ASSERT(!files.empty()); From 1b270da1539beceb9a82a63a712d4e40b8adcc66 Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 8 Jul 2025 14:57:39 +0200 Subject: [PATCH 07/35] Some pathmatch and matchglob refactoring, more tests --- cli/cmdlineparser.cpp | 20 ++--- gui/filelist.cpp | 6 +- lib/importproject.cpp | 11 ++- lib/pathmatch.cpp | 34 ++++++--- lib/pathmatch.h | 30 ++++++-- lib/suppressions.cpp | 3 +- lib/utils.cpp | 4 - test/cli/more-projects_test.py | 128 +++++++++++++++++++++++++++++++ test/testpathmatch.cpp | 133 ++++++++++++++++++--------------- 9 files changed, 263 insertions(+), 106 deletions(-) diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 2c879daa904..df67c642d37 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -209,8 +209,9 @@ bool CmdLineParser::fillSettingsFromArgs(int argc, const char* const argv[]) std::list fileSettings; if (!mSettings.fileFilters.empty()) { // filter only for the selected filenames from all project files + PathMatch filtermatcher(mSettings.fileFilters); std::copy_if(fileSettingsRef.cbegin(), fileSettingsRef.cend(), std::back_inserter(fileSettings), [&](const FileSettings &fs) { - return matchglobs(mSettings.fileFilters, fs.filename()); + return filtermatcher.match(fs.filename()); }); if (fileSettings.empty()) { mLogger.printError("could not find any files matching the filter."); @@ -242,16 +243,9 @@ bool CmdLineParser::fillSettingsFromArgs(int argc, const char* const argv[]) if (!pathnamesRef.empty()) { std::list filesResolved; - // TODO: this needs to be inlined into PathMatch as it depends on the underlying filesystem -#if defined(_WIN32) - // For Windows we want case-insensitive path matching - const bool caseSensitive = false; -#else - const bool caseSensitive = true; -#endif // Execute recursiveAddFiles() to each given file parameter // TODO: verbose log which files were ignored? - const PathMatch matcher(ignored, caseSensitive); + const PathMatch matcher(ignored); for (const std::string &pathname : pathnamesRef) { const std::string err = FileLister::recursiveAddFiles(filesResolved, Path::toNativeSeparators(pathname), mSettings.library.markupExtensions(), matcher, mSettings.debugignore); if (!err.empty()) { @@ -2160,13 +2154,9 @@ bool CmdLineParser::loadCppcheckCfg() std::list CmdLineParser::filterFiles(const std::vector& fileFilters, const std::list& filesResolved) { std::list files; -#ifdef _WIN32 - constexpr bool caseInsensitive = true; -#else - constexpr bool caseInsensitive = false; -#endif + PathMatch filtermatcher(fileFilters); std::copy_if(filesResolved.cbegin(), filesResolved.cend(), std::inserter(files, files.end()), [&](const FileWithDetails& entry) { - return matchglobs(fileFilters, entry.path(), caseInsensitive) || matchglobs(fileFilters, entry.spath(), caseInsensitive); + return filtermatcher.match(entry.path()) || filtermatcher.match(entry.spath()); }); return files; } diff --git a/gui/filelist.cpp b/gui/filelist.cpp index 3115967c451..43d44550ab0 100644 --- a/gui/filelist.cpp +++ b/gui/filelist.cpp @@ -119,11 +119,7 @@ static std::vector toStdStringList(const QStringList &stringList) QStringList FileList::applyExcludeList() const { -#ifdef _WIN32 - const PathMatch pathMatch(toStdStringList(mExcludedPaths), true); -#else - const PathMatch pathMatch(toStdStringList(mExcludedPaths), false); -#endif + const PathMatch pathMatch(toStdStringList(mExcludedPaths)); QStringList paths; for (const QFileInfo& item : mFileList) { diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 0a4c7e6b05b..0fd8e9ebcd4 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -45,7 +45,7 @@ void ImportProject::ignorePaths(const std::vector &ipaths, bool debug) { - PathMatch matcher(ipaths); + PathMatch matcher(ipaths, mPath); for (auto it = fileSettings.cbegin(); it != fileSettings.cend();) { if (matcher.match(it->filename())) { if (debug) @@ -840,8 +840,9 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X } // Project files + PathMatch filtermatcher(fileFilters); for (const std::string &cfilename : compileList) { - if (!fileFilters.empty() && !matchglobs(fileFilters, cfilename)) + if (!fileFilters.empty() && !filtermatcher.match(cfilename)) continue; for (const ProjectConfiguration &p : projectConfigurationList) { @@ -919,6 +920,8 @@ ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::strin SharedItemsProject result; result.pathToProjectFile = filename; + PathMatch filtermatcher(fileFilters); + tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); if (error != tinyxml2::XML_SUCCESS) { @@ -939,8 +942,8 @@ ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::strin std::string file(include); findAndReplace(file, "$(MSBuildThisFileDirectory)", "./"); - // Don't include file if it matches the filter - if (!fileFilters.empty() && !matchglobs(fileFilters, file)) + // Skip file if it doesn't match the filter + if (!fileFilters.empty() && !filtermatcher.match(file)) continue; result.sourceFiles.emplace_back(file); diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 6dea52deda8..9952cfa740d 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -55,8 +55,16 @@ static std::string translate(const std::string &s) return r; } -PathMatch::PathMatch(const std::vector &paths, bool caseSensitive) +PathMatch::PathMatch(const std::vector &paths, const std::string &basepath, Mode mode) { + if (mode == Mode::platform) { +#ifdef _WIN32 + mode = Mode::icase; +#else + mode = Mode::scase; +#endif + } + std::string regex_string; for (auto p : paths) { @@ -66,10 +74,14 @@ PathMatch::PathMatch(const std::vector &paths, bool caseSensitive) if (!regex_string.empty()) regex_string.push_back('|'); - p = Path::fromNativeSeparators(p); + if (p.front() == '.') { + if (Path::isAbsolute(basepath)) + p = basepath + "/" + p; + else + p = Path::getCurrentPath() + "/" + basepath + "/" + p; + } - if (p.front() == '.') - p = Path::getCurrentPath() + "/" + p; + p = Path::fromNativeSeparators(p); if (Path::isAbsolute(p)) { p = Path::simplifyPath(p); @@ -89,21 +101,23 @@ PathMatch::PathMatch(const std::vector &paths, bool caseSensitive) if (regex_string.empty()) regex_string = "^$"; - if (caseSensitive) - mRegex = std::regex(regex_string, std::regex_constants::extended); - else + if (mode == Mode::icase) mRegex = std::regex(regex_string, std::regex_constants::extended | std::regex_constants::icase); + else + mRegex = std::regex(regex_string, std::regex_constants::extended); } -bool PathMatch::match(const std::string &path) const +bool PathMatch::match(const std::string &path, const std::string &basepath) const { std::string p; std::smatch m; if (Path::isAbsolute(path)) - p = Path::simplifyPath(path); + p = Path::fromNativeSeparators(Path::simplifyPath(path)); + else if (Path::isAbsolute(basepath)) + p = Path::fromNativeSeparators(Path::simplifyPath(basepath + "/" + path)); else - p = Path::simplifyPath(Path::getCurrentPath() + "/" + path); + p = Path::fromNativeSeparators(Path::simplifyPath(Path::getCurrentPath() + "/" + basepath + "/" + path)); return std::regex_search(p, m, mRegex, std::regex_constants::match_any | std::regex_constants::match_not_null); } diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 25a99e9ed57..03925586a8f 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -40,16 +40,16 @@ * - Otherwise, match all files where the rule matches the file's simplified absolute path. * Globs are allowed in the rule. * - If a rule starts with '.': - * - The rule is interpreted as a path relative to the execution directory (when passed to the CLI), - * or the directory containing the project file (when imported), and then converted to an absolute path and - * treatesd as such according to the above procedure. + * - The rule is interpreted as a path relative to `basepath`, which should be the execution directory for rules + * passed to the CLI, or the directory containing the project file when imported, and then converted to an + * absolute path and treated as such according to the above procedure. * - Otherwise: * - No simplification is done to the rule. * - If the rule ends with a path separator: * - Match all files where the rule matches any part of the file's simplified absolute path, and the matching * part directly follows a path separator. Globs are allowed in the rule. * - Otherwise: - * - Match all files where the rules matches the end of the file's simplified absolute path, and the matching + * - Match all files where the rule matches the end of the file's simplified absolute path, and the matching * part directly follows a path separator. Globs are allowed in the rule. * * - Glob rules: @@ -64,16 +64,30 @@ class CPPCHECKLIB PathMatch { public: + /** + * @brief Case sensitivity mode. + * + * platform: Use the platform default. + * scase: Case sensitive. + * icase: Case insensitive. + **/ + enum class Mode { + platform, + scase, + icase, + }; + /** * The constructor. * * If a path is a directory it needs to end with a file separator. * * @param paths List of masks. - * @param caseSensitive Match the case of the characters when - * matching paths? + * @param basepath Path to which matched paths are relative, when applicable. Can be relative, in which case it is + * appended to Path::getCurrentPath(). + * @param mode Case sensitivity mode. */ - explicit PathMatch(const std::vector &paths, bool caseSensitive = true); + explicit PathMatch(const std::vector &paths, const std::string &basepath = std::string(), Mode mode = Mode::platform); /** * @brief Match path against list of masks. @@ -83,7 +97,7 @@ class CPPCHECKLIB PathMatch { * @param path Path to match. * @return true if any of the masks match the path, false otherwise. */ - bool match(const std::string &path) const; + bool match(const std::string &path, const std::string &basepath = std::string()) const; private: std::regex mRegex; diff --git a/lib/suppressions.cpp b/lib/suppressions.cpp index 6255927cf75..82d80eb2c30 100644 --- a/lib/suppressions.cpp +++ b/lib/suppressions.cpp @@ -22,6 +22,7 @@ #include "errortypes.h" #include "filesettings.h" #include "path.h" +#include "pathmatch.h" #include "utils.h" #include "token.h" #include "tokenize.h" @@ -396,7 +397,7 @@ SuppressionList::Suppression::Result SuppressionList::Suppression::isSuppressed( if (!errorId.empty() && !matchglob(errorId, errmsg.errorId)) return Result::Checked; } else { - if (!fileName.empty() && !matchglob(fileName, errmsg.getFileName())) + if (!fileName.empty() && !PathMatch({fileName}).match(errmsg.getFileName())) return Result::None; if ((SuppressionList::Type::unique == type) && (lineNumber != NO_LINE) && (lineNumber != errmsg.lineNumber)) { if (!thisAndNextLine || lineNumber + 1 != errmsg.lineNumber) diff --git a/lib/utils.cpp b/lib/utils.cpp index 11661556968..5ed025c8d5c 100644 --- a/lib/utils.cpp +++ b/lib/utils.cpp @@ -85,10 +85,6 @@ bool matchglob(const std::string& pattern, const std::string& name, bool caseIns n++; } else if (caseInsensitive && tolower(*n) == tolower(*p)) { n++; - } else if (*n == '\\' && *p == '/') { - n++; - } else if (*n == '/' && *p == '\\') { - n++; } else { matching = false; } diff --git a/test/cli/more-projects_test.py b/test/cli/more-projects_test.py index 512ad9fdede..ffa27463b72 100644 --- a/test/cli/more-projects_test.py +++ b/test/cli/more-projects_test.py @@ -372,6 +372,134 @@ def test_project_file_filter_3(tmpdir): assert_cppcheck(args, ec_exp=0, err_exp=[], out_exp=out_lines) +def test_project_relpath_file_filter_abspath(tmpdir): + """ + relative paths in project file, absolute path in file filter + """ + test_file_cpp = os.path.join(tmpdir, 'test.cpp') + with open(test_file_cpp, 'wt') as f: + pass + test_file_c = os.path.join(tmpdir, 'test.c') + with open(test_file_c, 'wt') as f: + pass + + project_file = os.path.join(tmpdir, 'test.cppcheck') + with open(project_file, 'wt') as f: + f.write( + """ + + + + + +""") + + out_lines = [ + 'Checking test.c ...' + ] + + args = ['--file-filter={}'.format(test_file_c), '--project=test.cppcheck'] + assert_cppcheck(args, ec_exp=0, err_exp=[], out_exp=out_lines, cwd=tmpdir) + + +def test_project_abspath_file_filter_relpath(tmpdir): + """ + absolute paths in project file, relative path in file filter + """ + test_file_cpp = os.path.join(tmpdir, 'test.cpp') + with open(test_file_cpp, 'wt') as f: + pass + test_file_c = os.path.join(tmpdir, 'test.c') + with open(test_file_c, 'wt') as f: + pass + + project_file = os.path.join(tmpdir, 'test.cppcheck') + with open(project_file, 'wt') as f: + f.write( + """ + + + + + +""".format(test_file_c, test_file_cpp)) + + out_lines = [ + 'Checking {} ...'.format(test_file_c) + ] + + args = ['--file-filter=test.c', '--project=test.cppcheck'] + assert_cppcheck(args, ec_exp=0, err_exp=[], out_exp=out_lines, cwd=tmpdir) + + +def test_project_pathmatch_other_cwd(tmpdir): + """ + mixed relative and absolute paths in project file and on command line, executed in a different directory + """ + test_root = tmpdir + test_cwd = os.path.join(test_root, 'cwd') + test_dir_1 = os.path.join(test_root, 'a') + test_dir_2 = os.path.join(test_root, 'b') + test_dir_3 = os.path.join(test_cwd, 'b') + + os.mkdir(test_cwd) + os.mkdir(test_dir_1) + os.mkdir(test_dir_2) + os.mkdir(test_dir_3) + + test_file_1 = os.path.join(test_dir_1, 'a-abs.c') + with open(test_file_1, 'wt') as f: + pass + + test_file_2 = os.path.join(test_dir_1, 'a-rel.c') + with open(test_file_2, 'wt') as f: + pass + + test_file_3 = os.path.join(test_dir_2, 'b-abs.c') + with open(test_file_3, 'wt') as f: + pass + + test_file_4 = os.path.join(test_dir_2, 'b-rel.c') + with open(test_file_4, 'wt') as f: + pass + + test_file_5 = os.path.join(test_dir_3, 'b-abs.c') + with open(test_file_5, 'wt') as f: + pass + + test_file_6 = os.path.join(test_dir_3, 'b-rel.c') + with open(test_file_6, 'wt') as f: + pass + + project_file = os.path.join(test_root, 'test.cppcheck') + with open(project_file, 'wt') as f: + f.write( + """ + + + + + + + + + + + + +""".format(test_file_1, test_file_3, test_file_5)) + + out_lines = [ + 'Checking {} ...'.format(test_file_5), + '1/2 files checked 0% done', + 'Checking ../cwd/b/b-rel.c ...', + '2/2 files checked 0% done' + ] + + args = ['--file-filter={}/*/?/**.c*'.format(test_root), '--project=../test.cppcheck'] + assert_cppcheck(args, ec_exp=0, err_exp=[], out_exp=out_lines, cwd=test_cwd) + + def test_project_file_filter_no_match(tmpdir): test_file = os.path.join(tmpdir, 'test.cpp') with open(test_file, 'wt') as f: diff --git a/test/testpathmatch.cpp b/test/testpathmatch.cpp index bc94067e72f..9a8e09b6e65 100644 --- a/test/testpathmatch.cpp +++ b/test/testpathmatch.cpp @@ -28,10 +28,15 @@ class TestPathMatch : public TestFixture { TestPathMatch() : TestFixture("TestPathMatch") {} private: - const PathMatch emptyMatcher{{}}; - const PathMatch srcMatcher{{"src/"}}; - const PathMatch fooCppMatcher{{"foo.cpp"}}; - const PathMatch srcFooCppMatcher{{"src/foo.cpp"}}; +#ifdef _WIN32 + const std::string basepath{"C:\\test"}; +#else + const std::string basepath{"/test"}; +#endif + const PathMatch emptyMatcher{{}, basepath}; + const PathMatch srcMatcher{{"src/"}, basepath}; + const PathMatch fooCppMatcher{{"foo.cpp"}, basepath}; + const PathMatch srcFooCppMatcher{{"src/foo.cpp"}, basepath}; void run() override { TEST_CASE(emptymaskemptyfile); @@ -66,173 +71,183 @@ class TestPathMatch : public TestFixture { TEST_CASE(filemaskpath3); TEST_CASE(filemaskpath4); TEST_CASE(mixedallmatch); + TEST_CASE(glob); TEST_CASE(globstar1); TEST_CASE(globstar2); } // Test empty PathMatch void emptymaskemptyfile() const { - ASSERT(!emptyMatcher.match("")); + ASSERT(!emptyMatcher.match("", basepath)); } void emptymaskpath1() const { - ASSERT(!emptyMatcher.match("src/")); + ASSERT(!emptyMatcher.match("src/", basepath)); } void emptymaskpath2() const { - ASSERT(!emptyMatcher.match("../src/")); + ASSERT(!emptyMatcher.match("../src/", basepath)); } void emptymaskpath3() const { - ASSERT(!emptyMatcher.match("/home/user/code/src/")); - ASSERT(!emptyMatcher.match("d:/home/user/code/src/")); + ASSERT(!emptyMatcher.match("/home/user/code/src/", basepath)); + ASSERT(!emptyMatcher.match("d:/home/user/code/src/", basepath)); } // Test PathMatch containing "src/" void onemaskemptypath() const { - ASSERT(!srcMatcher.match("")); + ASSERT(!srcMatcher.match("", basepath)); } void onemasksamepath() const { - ASSERT(srcMatcher.match("src/")); + ASSERT(srcMatcher.match("src/", basepath)); } void onemasksamepathdifferentslash() const { - PathMatch srcMatcher2({"src\\"}); - ASSERT(srcMatcher2.match("src/")); + PathMatch srcMatcher2({"src\\"}, basepath); + ASSERT(srcMatcher2.match("src/", basepath)); } void onemasksamepathdifferentcase() const { - PathMatch match({"sRc/"}, false); - ASSERT(match.match("srC/")); + PathMatch match({"sRc/"}, basepath, PathMatch::Mode::icase); + ASSERT(match.match("srC/", basepath)); } void onemasksamepathwithfile() const { - ASSERT(srcMatcher.match("src/file.txt")); + ASSERT(srcMatcher.match("src/file.txt", basepath)); } void onemaskshorterpath() const { const std::string longerExclude("longersrc/"); const std::string shorterToMatch("src/"); ASSERT(shorterToMatch.length() < longerExclude.length()); - PathMatch match({longerExclude}); - ASSERT(match.match(longerExclude)); - ASSERT(!match.match(shorterToMatch)); + PathMatch match({longerExclude}, basepath); + ASSERT(match.match(longerExclude, basepath)); + ASSERT(!match.match(shorterToMatch, basepath)); } void onemaskdifferentdir1() const { - ASSERT(!srcMatcher.match("srcfiles/file.txt")); + ASSERT(!srcMatcher.match("srcfiles/file.txt", basepath)); } void onemaskdifferentdir2() const { - ASSERT(!srcMatcher.match("proj/srcfiles/file.txt")); + ASSERT(!srcMatcher.match("proj/srcfiles/file.txt", basepath)); } void onemaskdifferentdir3() const { - ASSERT(!srcMatcher.match("proj/mysrc/file.txt")); + ASSERT(!srcMatcher.match("proj/mysrc/file.txt", basepath)); } void onemaskdifferentdir4() const { - ASSERT(!srcMatcher.match("proj/mysrcfiles/file.txt")); + ASSERT(!srcMatcher.match("proj/mysrcfiles/file.txt", basepath)); } void onemasklongerpath1() const { - ASSERT(srcMatcher.match("/tmp/src/")); - ASSERT(srcMatcher.match("d:/tmp/src/")); + ASSERT(srcMatcher.match("/tmp/src/", basepath)); + ASSERT(srcMatcher.match("d:/tmp/src/", basepath)); } void onemasklongerpath2() const { - ASSERT(srcMatcher.match("src/module/")); + ASSERT(srcMatcher.match("src/module/", basepath)); } void onemasklongerpath3() const { - ASSERT(srcMatcher.match("project/src/module/")); + ASSERT(srcMatcher.match("project/src/module/", basepath)); } void onemaskcwd() const { - ASSERT(!srcMatcher.match("./src")); + ASSERT(!srcMatcher.match("./src", basepath)); } void twomasklongerpath1() const { - PathMatch match({ "src/", "module/" }); - ASSERT(!match.match("project/")); + PathMatch match({ "src/", "module/" }, basepath); + ASSERT(!match.match("project/", basepath)); } void twomasklongerpath2() const { - PathMatch match({ "src/", "module/" }); - ASSERT(match.match("project/src/")); + PathMatch match({ "src/", "module/" }, basepath); + ASSERT(match.match("project/src/", basepath)); } void twomasklongerpath3() const { - PathMatch match({ "src/", "module/" }); - ASSERT(match.match("project/module/")); + PathMatch match({ "src/", "module/" }, basepath); + ASSERT(match.match("project/module/", basepath)); } void twomasklongerpath4() const { - PathMatch match({ "src/", "module/" }); - ASSERT(match.match("project/src/module/")); + PathMatch match({ "src/", "module/" }, basepath); + ASSERT(match.match("project/src/module/", basepath)); } // Test PathMatch containing "foo.cpp" void filemask1() const { - ASSERT(fooCppMatcher.match("foo.cpp")); + ASSERT(fooCppMatcher.match("foo.cpp", basepath)); } void filemaskdifferentcase() const { - PathMatch match({"foo.cPp"}, false); - ASSERT(match.match("fOo.cpp")); + PathMatch match({"foo.cPp"}, basepath, PathMatch::Mode::icase); + ASSERT(match.match("fOo.cpp", basepath)); } void filemask2() const { - ASSERT(fooCppMatcher.match("../foo.cpp")); + ASSERT(fooCppMatcher.match("../foo.cpp", basepath)); } void filemask3() const { - ASSERT(fooCppMatcher.match("src/foo.cpp")); + ASSERT(fooCppMatcher.match("src/foo.cpp", basepath)); } void filemaskcwd() const { - ASSERT(fooCppMatcher.match("./lib/foo.cpp")); + ASSERT(fooCppMatcher.match("./lib/foo.cpp", basepath)); } // Test PathMatch containing "src/foo.cpp" void filemaskpath1() const { - ASSERT(srcFooCppMatcher.match("src/foo.cpp")); + ASSERT(srcFooCppMatcher.match("src/foo.cpp", basepath)); } void filemaskpath2() const { - ASSERT(srcFooCppMatcher.match("proj/src/foo.cpp")); + ASSERT(srcFooCppMatcher.match("proj/src/foo.cpp", basepath)); } void filemaskpath3() const { - ASSERT(!srcFooCppMatcher.match("foo.cpp")); + ASSERT(!srcFooCppMatcher.match("foo.cpp", basepath)); } void filemaskpath4() const { - ASSERT(!srcFooCppMatcher.match("bar/foo.cpp")); + ASSERT(!srcFooCppMatcher.match("bar/foo.cpp", basepath)); } void mixedallmatch() const { // #13570 // when trying to match a directory against a directory entry it erroneously modified a local variable also used for file matching - PathMatch match({ "tests/", "file.c" }); - ASSERT(match.match("tests/")); - ASSERT(match.match("lib/file.c")); + PathMatch match({ "tests/", "file.c" }, basepath); + ASSERT(match.match("tests/", basepath)); + ASSERT(match.match("lib/file.c", basepath)); + } + + void glob() const { + PathMatch match({"test?.cpp"}, basepath); + ASSERT(match.match("test1.cpp", basepath)); + ASSERT(match.match("src/test1.cpp", basepath)); + ASSERT(!match.match("test1.c", basepath)); + ASSERT(!match.match("test.cpp", basepath)); + ASSERT(!match.match("test1.cpp/src", basepath)); } void globstar1() const { - PathMatch match({"src/**/foo.c"}); - ASSERT(match.match("src/lib/foo/foo.c")); - ASSERT(match.match("src/lib/foo/bar/foo.c")); - ASSERT(!match.match("src/lib/foo/foo.cpp")); - ASSERT(!match.match("src/lib/foo/bar/foo.cpp")); + PathMatch match({"src/**/foo.c"}, basepath); + ASSERT(match.match("src/lib/foo/foo.c", basepath)); + ASSERT(match.match("src/lib/foo/bar/foo.c", basepath)); + ASSERT(!match.match("src/lib/foo/foo.cpp", basepath)); + ASSERT(!match.match("src/lib/foo/bar/foo.cpp", basepath)); } void globstar2() const { - PathMatch match({"./src/**/foo.c"}); - ASSERT(match.match("src/lib/foo/foo.c")); - ASSERT(match.match("src/lib/foo/bar/foo.c")); - ASSERT(!match.match("src/lib/foo/foo.cpp")); - ASSERT(!match.match("src/lib/foo/bar/foo.cpp")); + PathMatch match({"./src/**/foo.c"}, basepath); + ASSERT(match.match("src/lib/foo/foo.c", basepath)); + ASSERT(match.match("src/lib/foo/bar/foo.c", basepath)); + ASSERT(!match.match("src/lib/foo/foo.cpp", basepath)); + ASSERT(!match.match("src/lib/foo/bar/foo.cpp", basepath)); } }; From 9743d82ca0c367ed29bf796118f6dfc557550815 Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 8 Jul 2025 15:03:32 +0200 Subject: [PATCH 08/35] Run dmake --- Makefile | 2 +- oss-fuzz/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 15475971f8c..54267989b43 100644 --- a/Makefile +++ b/Makefile @@ -630,7 +630,7 @@ $(libcppdir)/standards.o: lib/standards.cpp externals/simplecpp/simplecpp.h lib/ $(libcppdir)/summaries.o: lib/summaries.cpp lib/addoninfo.h lib/analyzerinfo.h lib/checkers.h lib/config.h lib/errortypes.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/sourcelocation.h lib/standards.h lib/summaries.h lib/symboldatabase.h lib/templatesimplifier.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/vfvalue.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/summaries.cpp -$(libcppdir)/suppressions.o: lib/suppressions.cpp externals/tinyxml2/tinyxml2.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/mathlib.h lib/path.h lib/platform.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/vfvalue.h lib/xml.h +$(libcppdir)/suppressions.o: lib/suppressions.cpp externals/tinyxml2/tinyxml2.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/standards.h lib/suppressions.h lib/templatesimplifier.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/vfvalue.h lib/xml.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/suppressions.cpp $(libcppdir)/templatesimplifier.o: lib/templatesimplifier.cpp lib/addoninfo.h lib/checkers.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/standards.h lib/templatesimplifier.h lib/token.h lib/tokenize.h lib/tokenlist.h lib/utils.h lib/vfvalue.h diff --git a/oss-fuzz/Makefile b/oss-fuzz/Makefile index 0092fcce8f1..4d218a111a7 100644 --- a/oss-fuzz/Makefile +++ b/oss-fuzz/Makefile @@ -318,7 +318,7 @@ $(libcppdir)/standards.o: ../lib/standards.cpp ../externals/simplecpp/simplecpp. $(libcppdir)/summaries.o: ../lib/summaries.cpp ../lib/addoninfo.h ../lib/analyzerinfo.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/settings.h ../lib/sourcelocation.h ../lib/standards.h ../lib/summaries.h ../lib/symboldatabase.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenize.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/summaries.cpp -$(libcppdir)/suppressions.o: ../lib/suppressions.cpp ../externals/tinyxml2/tinyxml2.h ../lib/config.h ../lib/errorlogger.h ../lib/errortypes.h ../lib/filesettings.h ../lib/mathlib.h ../lib/path.h ../lib/platform.h ../lib/standards.h ../lib/suppressions.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenize.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h ../lib/xml.h +$(libcppdir)/suppressions.o: ../lib/suppressions.cpp ../externals/tinyxml2/tinyxml2.h ../lib/config.h ../lib/errorlogger.h ../lib/errortypes.h ../lib/filesettings.h ../lib/mathlib.h ../lib/path.h ../lib/pathmatch.h ../lib/platform.h ../lib/standards.h ../lib/suppressions.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenize.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h ../lib/xml.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/suppressions.cpp $(libcppdir)/templatesimplifier.o: ../lib/templatesimplifier.cpp ../lib/addoninfo.h ../lib/checkers.h ../lib/config.h ../lib/errorlogger.h ../lib/errortypes.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/settings.h ../lib/standards.h ../lib/templatesimplifier.h ../lib/token.h ../lib/tokenize.h ../lib/tokenlist.h ../lib/utils.h ../lib/vfvalue.h From 2dcceca336759b55b986c37c737d3760fb80d65d Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 8 Jul 2025 15:27:00 +0200 Subject: [PATCH 09/35] Make basepath a class member --- lib/importproject.cpp | 2 +- lib/pathmatch.cpp | 21 ++++---- lib/pathmatch.h | 13 ++--- test/testpathmatch.cpp | 116 ++++++++++++++++++++--------------------- 4 files changed, 77 insertions(+), 75 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index 0fd8e9ebcd4..ccf9958087e 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -45,7 +45,7 @@ void ImportProject::ignorePaths(const std::vector &ipaths, bool debug) { - PathMatch matcher(ipaths, mPath); + PathMatch matcher(ipaths); for (auto it = fileSettings.cbegin(); it != fileSettings.cend();) { if (matcher.match(it->filename())) { if (debug) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 9952cfa740d..f44bc748e35 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -57,6 +57,13 @@ static std::string translate(const std::string &s) PathMatch::PathMatch(const std::vector &paths, const std::string &basepath, Mode mode) { + if (basepath.empty()) + mBasepath = Path::getCurrentPath(); + else if (Path::isAbsolute(basepath)) + mBasepath = basepath; + else + mBasepath = Path::getCurrentPath() + "/" + basepath; + if (mode == Mode::platform) { #ifdef _WIN32 mode = Mode::icase; @@ -74,12 +81,8 @@ PathMatch::PathMatch(const std::vector &paths, const std::string &b if (!regex_string.empty()) regex_string.push_back('|'); - if (p.front() == '.') { - if (Path::isAbsolute(basepath)) - p = basepath + "/" + p; - else - p = Path::getCurrentPath() + "/" + basepath + "/" + p; - } + if (p.front() == '.') + p = mBasepath + "/" + p; p = Path::fromNativeSeparators(p); @@ -107,17 +110,15 @@ PathMatch::PathMatch(const std::vector &paths, const std::string &b mRegex = std::regex(regex_string, std::regex_constants::extended); } -bool PathMatch::match(const std::string &path, const std::string &basepath) const +bool PathMatch::match(const std::string &path) const { std::string p; std::smatch m; if (Path::isAbsolute(path)) p = Path::fromNativeSeparators(Path::simplifyPath(path)); - else if (Path::isAbsolute(basepath)) - p = Path::fromNativeSeparators(Path::simplifyPath(basepath + "/" + path)); else - p = Path::fromNativeSeparators(Path::simplifyPath(Path::getCurrentPath() + "/" + basepath + "/" + path)); + p = Path::fromNativeSeparators(Path::simplifyPath(mBasepath + "/" + path)); return std::regex_search(p, m, mRegex, std::regex_constants::match_any | std::regex_constants::match_not_null); } diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 03925586a8f..3889c382d06 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -40,9 +40,9 @@ * - Otherwise, match all files where the rule matches the file's simplified absolute path. * Globs are allowed in the rule. * - If a rule starts with '.': - * - The rule is interpreted as a path relative to `basepath`, which should be the execution directory for rules - * passed to the CLI, or the directory containing the project file when imported, and then converted to an - * absolute path and treated as such according to the above procedure. + * - The rule is interpreted as a path relative to `basepath` and then converted to an absolute path and + * treated as such according to the above procedure. If the rule is relative to some other directory, it should + * be modified to be relative to `basepath` first (this should be done with rules in project files, for example). * - Otherwise: * - No simplification is done to the rule. * - If the rule ends with a path separator: @@ -83,8 +83,8 @@ class CPPCHECKLIB PathMatch { * If a path is a directory it needs to end with a file separator. * * @param paths List of masks. - * @param basepath Path to which matched paths are relative, when applicable. Can be relative, in which case it is - * appended to Path::getCurrentPath(). + * @param basepath Path to which rules and matched paths are relative, when applicable. Can be relative, in which + * case it is appended to Path::getCurrentPath(). * @param mode Case sensitivity mode. */ explicit PathMatch(const std::vector &paths, const std::string &basepath = std::string(), Mode mode = Mode::platform); @@ -97,9 +97,10 @@ class CPPCHECKLIB PathMatch { * @param path Path to match. * @return true if any of the masks match the path, false otherwise. */ - bool match(const std::string &path, const std::string &basepath = std::string()) const; + bool match(const std::string &path) const; private: + std::string mBasepath; std::regex mRegex; }; diff --git a/test/testpathmatch.cpp b/test/testpathmatch.cpp index 9a8e09b6e65..a6ea8704fbf 100644 --- a/test/testpathmatch.cpp +++ b/test/testpathmatch.cpp @@ -78,176 +78,176 @@ class TestPathMatch : public TestFixture { // Test empty PathMatch void emptymaskemptyfile() const { - ASSERT(!emptyMatcher.match("", basepath)); + ASSERT(!emptyMatcher.match("")); } void emptymaskpath1() const { - ASSERT(!emptyMatcher.match("src/", basepath)); + ASSERT(!emptyMatcher.match("src/")); } void emptymaskpath2() const { - ASSERT(!emptyMatcher.match("../src/", basepath)); + ASSERT(!emptyMatcher.match("../src/")); } void emptymaskpath3() const { - ASSERT(!emptyMatcher.match("/home/user/code/src/", basepath)); - ASSERT(!emptyMatcher.match("d:/home/user/code/src/", basepath)); + ASSERT(!emptyMatcher.match("/home/user/code/src/")); + ASSERT(!emptyMatcher.match("d:/home/user/code/src/")); } // Test PathMatch containing "src/" void onemaskemptypath() const { - ASSERT(!srcMatcher.match("", basepath)); + ASSERT(!srcMatcher.match("")); } void onemasksamepath() const { - ASSERT(srcMatcher.match("src/", basepath)); + ASSERT(srcMatcher.match("src/")); } void onemasksamepathdifferentslash() const { PathMatch srcMatcher2({"src\\"}, basepath); - ASSERT(srcMatcher2.match("src/", basepath)); + ASSERT(srcMatcher2.match("src/")); } void onemasksamepathdifferentcase() const { PathMatch match({"sRc/"}, basepath, PathMatch::Mode::icase); - ASSERT(match.match("srC/", basepath)); + ASSERT(match.match("srC/")); } void onemasksamepathwithfile() const { - ASSERT(srcMatcher.match("src/file.txt", basepath)); + ASSERT(srcMatcher.match("src/file.txt")); } void onemaskshorterpath() const { const std::string longerExclude("longersrc/"); const std::string shorterToMatch("src/"); ASSERT(shorterToMatch.length() < longerExclude.length()); - PathMatch match({longerExclude}, basepath); - ASSERT(match.match(longerExclude, basepath)); - ASSERT(!match.match(shorterToMatch, basepath)); + PathMatch match({longerExclude}); + ASSERT(match.match(longerExclude)); + ASSERT(!match.match(shorterToMatch)); } void onemaskdifferentdir1() const { - ASSERT(!srcMatcher.match("srcfiles/file.txt", basepath)); + ASSERT(!srcMatcher.match("srcfiles/file.txt")); } void onemaskdifferentdir2() const { - ASSERT(!srcMatcher.match("proj/srcfiles/file.txt", basepath)); + ASSERT(!srcMatcher.match("proj/srcfiles/file.txt")); } void onemaskdifferentdir3() const { - ASSERT(!srcMatcher.match("proj/mysrc/file.txt", basepath)); + ASSERT(!srcMatcher.match("proj/mysrc/file.txt")); } void onemaskdifferentdir4() const { - ASSERT(!srcMatcher.match("proj/mysrcfiles/file.txt", basepath)); + ASSERT(!srcMatcher.match("proj/mysrcfiles/file.txt")); } void onemasklongerpath1() const { - ASSERT(srcMatcher.match("/tmp/src/", basepath)); - ASSERT(srcMatcher.match("d:/tmp/src/", basepath)); + ASSERT(srcMatcher.match("/tmp/src/")); + ASSERT(srcMatcher.match("d:/tmp/src/")); } void onemasklongerpath2() const { - ASSERT(srcMatcher.match("src/module/", basepath)); + ASSERT(srcMatcher.match("src/module/")); } void onemasklongerpath3() const { - ASSERT(srcMatcher.match("project/src/module/", basepath)); + ASSERT(srcMatcher.match("project/src/module/")); } void onemaskcwd() const { - ASSERT(!srcMatcher.match("./src", basepath)); + ASSERT(!srcMatcher.match("./src")); } void twomasklongerpath1() const { - PathMatch match({ "src/", "module/" }, basepath); - ASSERT(!match.match("project/", basepath)); + PathMatch match({ "src/", "module/" }); + ASSERT(!match.match("project/")); } void twomasklongerpath2() const { - PathMatch match({ "src/", "module/" }, basepath); - ASSERT(match.match("project/src/", basepath)); + PathMatch match({ "src/", "module/" }); + ASSERT(match.match("project/src/")); } void twomasklongerpath3() const { - PathMatch match({ "src/", "module/" }, basepath); - ASSERT(match.match("project/module/", basepath)); + PathMatch match({ "src/", "module/" }); + ASSERT(match.match("project/module/")); } void twomasklongerpath4() const { - PathMatch match({ "src/", "module/" }, basepath); - ASSERT(match.match("project/src/module/", basepath)); + PathMatch match({ "src/", "module/" }); + ASSERT(match.match("project/src/module/")); } // Test PathMatch containing "foo.cpp" void filemask1() const { - ASSERT(fooCppMatcher.match("foo.cpp", basepath)); + ASSERT(fooCppMatcher.match("foo.cpp")); } void filemaskdifferentcase() const { PathMatch match({"foo.cPp"}, basepath, PathMatch::Mode::icase); - ASSERT(match.match("fOo.cpp", basepath)); + ASSERT(match.match("fOo.cpp")); } void filemask2() const { - ASSERT(fooCppMatcher.match("../foo.cpp", basepath)); + ASSERT(fooCppMatcher.match("../foo.cpp")); } void filemask3() const { - ASSERT(fooCppMatcher.match("src/foo.cpp", basepath)); + ASSERT(fooCppMatcher.match("src/foo.cpp")); } void filemaskcwd() const { - ASSERT(fooCppMatcher.match("./lib/foo.cpp", basepath)); + ASSERT(fooCppMatcher.match("./lib/foo.cpp")); } // Test PathMatch containing "src/foo.cpp" void filemaskpath1() const { - ASSERT(srcFooCppMatcher.match("src/foo.cpp", basepath)); + ASSERT(srcFooCppMatcher.match("src/foo.cpp")); } void filemaskpath2() const { - ASSERT(srcFooCppMatcher.match("proj/src/foo.cpp", basepath)); + ASSERT(srcFooCppMatcher.match("proj/src/foo.cpp")); } void filemaskpath3() const { - ASSERT(!srcFooCppMatcher.match("foo.cpp", basepath)); + ASSERT(!srcFooCppMatcher.match("foo.cpp")); } void filemaskpath4() const { - ASSERT(!srcFooCppMatcher.match("bar/foo.cpp", basepath)); + ASSERT(!srcFooCppMatcher.match("bar/foo.cpp")); } void mixedallmatch() const { // #13570 // when trying to match a directory against a directory entry it erroneously modified a local variable also used for file matching - PathMatch match({ "tests/", "file.c" }, basepath); - ASSERT(match.match("tests/", basepath)); - ASSERT(match.match("lib/file.c", basepath)); + PathMatch match({ "tests/", "file.c" }); + ASSERT(match.match("tests/")); + ASSERT(match.match("lib/file.c")); } void glob() const { - PathMatch match({"test?.cpp"}, basepath); - ASSERT(match.match("test1.cpp", basepath)); - ASSERT(match.match("src/test1.cpp", basepath)); - ASSERT(!match.match("test1.c", basepath)); - ASSERT(!match.match("test.cpp", basepath)); - ASSERT(!match.match("test1.cpp/src", basepath)); + PathMatch match({"test?.cpp"}); + ASSERT(match.match("test1.cpp")); + ASSERT(match.match("src/test1.cpp")); + ASSERT(!match.match("test1.c")); + ASSERT(!match.match("test.cpp")); + ASSERT(!match.match("test1.cpp/src")); } void globstar1() const { - PathMatch match({"src/**/foo.c"}, basepath); - ASSERT(match.match("src/lib/foo/foo.c", basepath)); - ASSERT(match.match("src/lib/foo/bar/foo.c", basepath)); - ASSERT(!match.match("src/lib/foo/foo.cpp", basepath)); - ASSERT(!match.match("src/lib/foo/bar/foo.cpp", basepath)); + PathMatch match({"src/**/foo.c"}); + ASSERT(match.match("src/lib/foo/foo.c")); + ASSERT(match.match("src/lib/foo/bar/foo.c")); + ASSERT(!match.match("src/lib/foo/foo.cpp")); + ASSERT(!match.match("src/lib/foo/bar/foo.cpp")); } void globstar2() const { - PathMatch match({"./src/**/foo.c"}, basepath); - ASSERT(match.match("src/lib/foo/foo.c", basepath)); - ASSERT(match.match("src/lib/foo/bar/foo.c", basepath)); - ASSERT(!match.match("src/lib/foo/foo.cpp", basepath)); - ASSERT(!match.match("src/lib/foo/bar/foo.cpp", basepath)); + PathMatch match({"./src/**/foo.c"}); + ASSERT(match.match("src/lib/foo/foo.c")); + ASSERT(match.match("src/lib/foo/bar/foo.c")); + ASSERT(!match.match("src/lib/foo/foo.cpp")); + ASSERT(!match.match("src/lib/foo/bar/foo.cpp")); } }; From 740b363a0b4c32ade5c2ca4cc92cc802de6c2b8c Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 8 Jul 2025 15:51:30 +0200 Subject: [PATCH 10/35] Remove matchglobs --- lib/utils.cpp | 6 ------ lib/utils.h | 2 -- 2 files changed, 8 deletions(-) diff --git a/lib/utils.cpp b/lib/utils.cpp index 5ed025c8d5c..73e9de58223 100644 --- a/lib/utils.cpp +++ b/lib/utils.cpp @@ -113,12 +113,6 @@ bool matchglob(const std::string& pattern, const std::string& name, bool caseIns } } -bool matchglobs(const std::vector &patterns, const std::string &name, bool caseInsensitive) { - return std::any_of(begin(patterns), end(patterns), [&name, caseInsensitive](const std::string &pattern) { - return matchglob(pattern, name, caseInsensitive); - }); -} - void strTolower(std::string& str) { // This wrapper exists because Sun's CC does not allow a static_cast diff --git a/lib/utils.h b/lib/utils.h index 82edc353160..07350660281 100644 --- a/lib/utils.h +++ b/lib/utils.h @@ -204,8 +204,6 @@ CPPCHECKLIB bool isValidGlobPattern(const std::string& pattern); CPPCHECKLIB bool matchglob(const std::string& pattern, const std::string& name, bool caseInsensitive = false); -CPPCHECKLIB bool matchglobs(const std::vector &patterns, const std::string &name, bool caseInsensitive = false); - CPPCHECKLIB void strTolower(std::string& str); template::value, bool>::type=true> From a9c01efe369ad40556746495ae9a389492338005 Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 8 Jul 2025 16:42:21 +0200 Subject: [PATCH 11/35] Fix clang-tidy issue and windows test --- lib/pathmatch.h | 3 ++- test/cli/more-projects_test.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 3889c382d06..fa3a2d3d1f1 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -21,6 +21,7 @@ #include "config.h" +#include #include #include #include @@ -71,7 +72,7 @@ class CPPCHECKLIB PathMatch { * scase: Case sensitive. * icase: Case insensitive. **/ - enum class Mode { + enum class Mode : std::uint8_t { platform, scase, icase, diff --git a/test/cli/more-projects_test.py b/test/cli/more-projects_test.py index ffa27463b72..8aecebc9c18 100644 --- a/test/cli/more-projects_test.py +++ b/test/cli/more-projects_test.py @@ -492,7 +492,7 @@ def test_project_pathmatch_other_cwd(tmpdir): out_lines = [ 'Checking {} ...'.format(test_file_5), '1/2 files checked 0% done', - 'Checking ../cwd/b/b-rel.c ...', + 'Checking {} ...'.format(os.path.join("..", "cwd", "b", "b-rel.c")), '2/2 files checked 0% done' ] From 6200cb8a87b226e7a0c9f7197afc659a6b988618 Mon Sep 17 00:00:00 2001 From: glank Date: Wed, 9 Jul 2025 10:02:54 +0200 Subject: [PATCH 12/35] Remove obsolete mIgnorePaths check --- cli/cmdlineparser.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index df67c642d37..1ec6f2187f0 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -1613,24 +1613,6 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a return Result::Fail; } - for (auto& path : mIgnoredPaths) - { - path = Path::removeQuotationMarks(std::move(path)); - path = Path::simplifyPath(std::move(path)); - - bool isdir = false; - if (!Path::exists(path, &isdir) && mSettings.debugignore) { - // FIXME: this is misleading because we match from the end of the path so it does not require to exist - //std::cout << "path to ignore does not exist: " << path << std::endl; - } - // TODO: this only works when it exists - if (isdir) { - // If directory name doesn't end with / or \, add it - if (!endsWith(path, '/')) - path += '/'; - } - } - if (!project.guiProject.pathNames.empty()) mPathNames = project.guiProject.pathNames; From 5b21e25344d98aeb94d5e67c8b00fd14ef21f6ed Mon Sep 17 00:00:00 2001 From: glank Date: Wed, 9 Jul 2025 13:13:34 +0200 Subject: [PATCH 13/35] Allow directories to be specified without trailing path separator --- lib/pathmatch.cpp | 24 ++++++++++-------------- lib/pathmatch.h | 33 +++++++++++++-------------------- test/cli/other_test.py | 1 - test/testcmdlineparser.cpp | 4 ++-- test/testpathmatch.cpp | 4 ++-- 5 files changed, 27 insertions(+), 39 deletions(-) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index f44bc748e35..117c4f0d93a 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -84,25 +84,21 @@ PathMatch::PathMatch(const std::vector &paths, const std::string &b if (p.front() == '.') p = mBasepath + "/" + p; - p = Path::fromNativeSeparators(p); + p = Path::fromNativeSeparators(Path::simplifyPath(p)); - if (Path::isAbsolute(p)) { - p = Path::simplifyPath(p); + if (p.back() == '/') + p.pop_back(); - if (p.back() == '/') - regex_string.append("^" + translate(p)); - else - regex_string.append("^" + translate(p) + "$"); - } else { - if (p.back() == '/') - regex_string.append("/" + translate(p)); - else - regex_string.append("/" + translate(p) + "$"); - } + if (Path::isAbsolute(p)) + regex_string.push_back('^'); + else + regex_string.push_back('/'); + + regex_string.append(translate(p) + "(/|$)"); } if (regex_string.empty()) - regex_string = "^$"; + return; if (mode == Mode::icase) mRegex = std::regex(regex_string, std::regex_constants::extended | std::regex_constants::icase); diff --git a/lib/pathmatch.h b/lib/pathmatch.h index fa3a2d3d1f1..b0264df934c 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -31,32 +31,25 @@ /** * Path matching rules: + * - All rules are simplified first (path separators vary by platform): + * - '/./' => '/' + * - '/dir/../' => '/' + * - '//' => '/' + * - Trailing slashes are removed + * - Rules can contain globs: + * - '**' matches any number of characters including path separators. + * - '*' matches any number of characters except path separators. + * - '?' matches any single character except path separators. * - If a rule looks like an absolute path (e.g. starts with '/', but varies by platform): - * - The rule will be simplified (path separators vary by platform): - * - '/./' => '/' - * - '/dir/../' => '/' - * - '//' => '/' - * - If the rule ends with a path separator, match all files where the rule matches the start of the file's - * simplified absolute path. Globs are allowed in the rule. - * - Otherwise, match all files where the rule matches the file's simplified absolute path. - * Globs are allowed in the rule. + * - Match all files where the rule matches the start of the file's simplified absolute path up until a path + * separator or the end of the pathname. * - If a rule starts with '.': * - The rule is interpreted as a path relative to `basepath` and then converted to an absolute path and * treated as such according to the above procedure. If the rule is relative to some other directory, it should * be modified to be relative to `basepath` first (this should be done with rules in project files, for example). * - Otherwise: - * - No simplification is done to the rule. - * - If the rule ends with a path separator: - * - Match all files where the rule matches any part of the file's simplified absolute path, and the matching - * part directly follows a path separator. Globs are allowed in the rule. - * - Otherwise: - * - Match all files where the rule matches the end of the file's simplified absolute path, and the matching - * part directly follows a path separator. Globs are allowed in the rule. - * - * - Glob rules: - * - '**' matches any number of characters including path separators. - * - '*' matches any number of characters except path separators. - * - '?' matches any single character except path separators. + * - Match all files where the rule matches any part of the file's simplified absolute path up until a + * path separator or the end of the pathname, and the matching part directly follows a path separator. **/ /** diff --git a/test/cli/other_test.py b/test/cli/other_test.py index ff35eb0574f..ecfca256ec6 100644 --- a/test/cli/other_test.py +++ b/test/cli/other_test.py @@ -2478,7 +2478,6 @@ def test_addon_suppr_cli_line(tmp_path): __test_addon_suppr(tmp_path, ['--suppress=misra-c2012-2.3:*:3']) -@pytest.mark.xfail(strict=True) # #13437 - TODO: suppression needs to match the whole input path def test_addon_suppr_cli_file_line(tmp_path): __test_addon_suppr(tmp_path, ['--suppress=misra-c2012-2.3:test.c:3']) diff --git a/test/testcmdlineparser.cpp b/test/testcmdlineparser.cpp index c0c0dde9786..71b33b93104 100644 --- a/test/testcmdlineparser.cpp +++ b/test/testcmdlineparser.cpp @@ -3345,7 +3345,7 @@ class TestCmdlineParser : public TestFixture { const char * const argv[] = {"cppcheck", "-isrc\\file.cpp", "src/file.cpp"}; ASSERT(!fillSettingsFromArgs(argv)); ASSERT_EQUALS(1, parser->getIgnoredPaths().size()); - ASSERT_EQUALS("src/file.cpp", parser->getIgnoredPaths()[0]); + ASSERT_EQUALS("src\\file.cpp", parser->getIgnoredPaths()[0]); ASSERT_EQUALS(1, parser->getPathNames().size()); ASSERT_EQUALS("src/file.cpp", parser->getPathNames()[0]); ASSERT_EQUALS("cppcheck: error: could not find or open any of the paths given.\ncppcheck: Maybe all paths were ignored?\n", logger->str()); @@ -3367,7 +3367,7 @@ class TestCmdlineParser : public TestFixture { const char * const argv[] = {"cppcheck", "-isrc\\", "src\\file.cpp"}; ASSERT(!fillSettingsFromArgs(argv)); ASSERT_EQUALS(1, parser->getIgnoredPaths().size()); - ASSERT_EQUALS("src/", parser->getIgnoredPaths()[0]); + ASSERT_EQUALS("src\\", parser->getIgnoredPaths()[0]); ASSERT_EQUALS(1, parser->getPathNames().size()); ASSERT_EQUALS("src/file.cpp", parser->getPathNames()[0]); ASSERT_EQUALS("cppcheck: error: could not find or open any of the paths given.\ncppcheck: Maybe all paths were ignored?\n", logger->str()); diff --git a/test/testpathmatch.cpp b/test/testpathmatch.cpp index a6ea8704fbf..e7f056fdca2 100644 --- a/test/testpathmatch.cpp +++ b/test/testpathmatch.cpp @@ -156,7 +156,7 @@ class TestPathMatch : public TestFixture { } void onemaskcwd() const { - ASSERT(!srcMatcher.match("./src")); + ASSERT(srcMatcher.match("./src")); } void twomasklongerpath1() const { @@ -229,9 +229,9 @@ class TestPathMatch : public TestFixture { PathMatch match({"test?.cpp"}); ASSERT(match.match("test1.cpp")); ASSERT(match.match("src/test1.cpp")); + ASSERT(match.match("test1.cpp/src")); ASSERT(!match.match("test1.c")); ASSERT(!match.match("test.cpp")); - ASSERT(!match.match("test1.cpp/src")); } void globstar1() const { From 6547d33f340f09da1972863c4864ac27f80c1d2f Mon Sep 17 00:00:00 2001 From: glank Date: Wed, 9 Jul 2025 13:55:53 +0200 Subject: [PATCH 14/35] Fix test_project_pathmatch_other_cwd --- test/cli/more-projects_test.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/cli/more-projects_test.py b/test/cli/more-projects_test.py index 8aecebc9c18..c728966ef7b 100644 --- a/test/cli/more-projects_test.py +++ b/test/cli/more-projects_test.py @@ -491,13 +491,16 @@ def test_project_pathmatch_other_cwd(tmpdir): out_lines = [ 'Checking {} ...'.format(test_file_5), - '1/2 files checked 0% done', 'Checking {} ...'.format(os.path.join("..", "cwd", "b", "b-rel.c")), - '2/2 files checked 0% done' ] args = ['--file-filter={}/*/?/**.c*'.format(test_root), '--project=../test.cppcheck'] - assert_cppcheck(args, ec_exp=0, err_exp=[], out_exp=out_lines, cwd=test_cwd) + exitcode, stdout, stderr = cppcheck(args, cwd=test_cwd) + stdout_lines = stdout.splitlines() + assert 0 == exitcode + assert '' == stderr + assert 4 == len(stdout_lines) + assert set(out_lines) <= set(stdout_lines) def test_project_file_filter_no_match(tmpdir): From bf664fd260d25a7bc18d537140ebbeb0aaddf1db Mon Sep 17 00:00:00 2001 From: glank Date: Wed, 9 Jul 2025 19:03:53 +0200 Subject: [PATCH 15/35] Hand-written matching function --- lib/path.cpp | 14 +- lib/pathmatch.cpp | 304 +++++++++++++++++++++++++++++++++---------- lib/pathmatch.h | 52 +++++--- lib/suppressions.cpp | 2 +- 4 files changed, 275 insertions(+), 97 deletions(-) diff --git a/lib/path.cpp b/lib/path.cpp index eba26230022..0fe6d54d5cc 100644 --- a/lib/path.cpp +++ b/lib/path.cpp @@ -173,18 +173,24 @@ std::string Path::getCurrentExecutablePath(const char* fallback) return success ? std::string(buf) : std::string(fallback); } -bool Path::isAbsolute(const std::string& path) +static bool issep(char c) { - const std::string& nativePath = toNativeSeparators(path); + return c == '/' || c == '\\'; +} +bool Path::isAbsolute(const std::string& path) +{ #ifdef _WIN32 if (path.length() < 2) return false; + if (issep(path[0]) && issep(path[1])) + return true; + // On Windows, 'C:\foo\bar' is an absolute path, while 'C:foo\bar' is not - return startsWith(nativePath, "\\\\") || (std::isalpha(nativePath[0]) != 0 && nativePath.compare(1, 2, ":\\") == 0); + return std::isalpha(path[0]) && path[1] == ':' && issep(path[2]); #else - return !nativePath.empty() && nativePath[0] == '/'; + return !path.empty() && issep(path[0]); #endif } diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 117c4f0d93a..c10927fbd5b 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -20,101 +20,263 @@ #include "path.h" -#include -#include +#include +#include +#include +#include #include -#include +#include -/* Escape regex special chars and translate globs to equivalent regex */ -static std::string translate(const std::string &s) -{ - std::string r; - std::size_t i = 0; - - while (i != s.size()) { - int c = s[i++]; - - if (std::strchr("\\[](){}+^$|", c) != nullptr) { - r.push_back('\\'); - r.push_back(c); - } else if (c == '*') { - if (i != s.size() && s[i] == '*') { - r.append(".*"); - i++; +struct Pathstr { + static Pathstr from_pattern(const std::string &pattern, const std::string &basepath, bool icase) + { + if (!pattern.empty() && pattern[0] == '.') + return Pathstr(basepath.c_str(), pattern.c_str(), icase); + return Pathstr(pattern.c_str(), nullptr, icase); + } + + static Pathstr from_path(const std::string &path, const std::string &basepath, bool icase) + { + if (Path::isAbsolute(path)) + return Pathstr(path.c_str(), nullptr, icase); + return Pathstr(basepath.c_str(), path.c_str(), icase); + } + + explicit Pathstr(const char *a = nullptr, const char *b = nullptr, bool lowercase = false) : + s{a, b}, lcase(lowercase) + { + for (int i = 0; i < 2; i++) { + e[i] = s[i]; + + if (s[i] == nullptr || *s[i] == '\0') + continue; + + if (st.l != 0) + st.l++; + + while (*e[i] != '\0') { + e[i]++; + st.l++; + } + + st.p = e[i]; + } + + if (st.l == 0) + st.c = '\0'; + + simplify(false); + } + + std::size_t left() const + { + return st.l; + } + + char current() const + { + if (st.c != EOF) + return st.c; + + char c = st.p[-1]; + + if (c == '\\') + return '/'; + + if (lcase) + return std::tolower(c); + + return c; + } + + void simplify(bool leadsep) { + while (left() != 0) { + State rst = st; + + if (leadsep) { + if (current() != '/') + break; + nextc(); } - else { - r.append("[^/]*"); + + char c = current(); + if (c == '.') { + nextc(); + c = current(); + if (c == '.') { + nextc(); + c = current(); + if (c == '/') { + /* Skip '/../' */ + nextc(); + simplify(false); + while (left() != 0 && current() != '/') + nextc(); + continue; + } + } else if (c == '/') { + /* Skip '/./' */ + continue; + } else if (c == '\0') { + /* Skip leading './' */ + break; + } + } else if (c == '/' && left() != 1) { + /* Skip double separator (keep root) */ + nextc(); + leadsep = false; + continue; } - } else if (c == '?') { - r.append("[^/]"); + + st = rst; + break; + } + } + + void advance() + { + nextc(); + + if (current() == '/') + simplify(true); + } + + void nextc() + { + if (st.l == 0) + return; + + st.l--; + + if (st.l == 0) + st.c = '\0'; + else if (st.c != EOF) { + st.c = EOF; } else { - r.push_back(c); + st.p--; + if (st.p == s[1]) { + st.p = e[0]; + st.c = '/'; + } } } - return r; -} + Pathstr &operator++(int) { + advance(); + return *this; + } -PathMatch::PathMatch(const std::vector &paths, const std::string &basepath, Mode mode) -{ - if (basepath.empty()) - mBasepath = Path::getCurrentPath(); - else if (Path::isAbsolute(basepath)) - mBasepath = basepath; - else - mBasepath = Path::getCurrentPath() + "/" + basepath; - - if (mode == Mode::platform) { -#ifdef _WIN32 - mode = Mode::icase; -#else - mode = Mode::scase; -#endif + char operator*() const { + return current(); } - std::string regex_string; + struct State { + const char *p; + std::size_t l; + int c {EOF}; + }; - for (auto p : paths) { - if (p.empty()) - continue; + const char *s[2] {}; + const char *e[2] {}; + State st {}; + bool lcase; +}; - if (!regex_string.empty()) - regex_string.push_back('|'); - if (p.front() == '.') - p = mBasepath + "/" + p; +static bool match_one(const std::string &pattern, const std::string &path, const std::string &basepath, bool icase) +{ + if (pattern.empty()) + return false; - p = Path::fromNativeSeparators(Path::simplifyPath(p)); + if (pattern == "*" || pattern == "**") + return true; - if (p.back() == '/') - p.pop_back(); + bool real = Path::isAbsolute(pattern) || pattern[0] == '.'; - if (Path::isAbsolute(p)) - regex_string.push_back('^'); - else - regex_string.push_back('/'); + Pathstr s = Pathstr::from_pattern(pattern, basepath, icase); + Pathstr t = Pathstr::from_path(path, basepath, icase); + Pathstr p = s; + Pathstr q = t; - regex_string.append(translate(p) + "(/|$)"); - } + std::stack> b; - if (regex_string.empty()) - return; + for (;;) { + switch (*s) { + case '*': { + bool slash = false; + s++; + if (*s == '*') { + slash = true; + s++; + } + b.emplace(s.st, t.st); + while (*t != '\0' && (slash || *t != '/')) { + if (*s == *t) + b.emplace(s.st, t.st); + t++; + } + continue; + } + case '?': { + if (*t != '\0' && *t != '/') { + s++; + t++; + continue; + } + break; + } + case '\0': { + if (*t == '\0' || (*t == '/' && !real)) + return true; + break; + } + default: { + if (*s == *t) { + s++; + t++; + continue; + } + break; + } + } + + if (b.size() != 0) { + const auto &bp = b.top(); + b.pop(); + s.st = bp.first; + t.st = bp.second; + continue; + } - if (mode == Mode::icase) - mRegex = std::regex(regex_string, std::regex_constants::extended | std::regex_constants::icase); - else - mRegex = std::regex(regex_string, std::regex_constants::extended); + while (*q != '\0' && *q != '/') + q++; + + if (*q == '/') { + q++; + s = p; + t = q; + continue; + } + + return false; + } } + +PathMatch::PathMatch(std::vector patterns, std::string basepath, Mode mode) : + mPatterns(std::move(patterns)), mBasepath(std::move(basepath)), mMode(mode) +{} + bool PathMatch::match(const std::string &path) const { - std::string p; - std::smatch m; + bool icase = (mMode == Mode::icase); - if (Path::isAbsolute(path)) - p = Path::fromNativeSeparators(Path::simplifyPath(path)); - else - p = Path::fromNativeSeparators(Path::simplifyPath(mBasepath + "/" + path)); + return std::any_of(mPatterns.cbegin(), mPatterns.cend(), [=] (const std::string &pattern) { + return match_one(pattern, path, mBasepath, icase); + }); +} - return std::regex_search(p, m, mRegex, std::regex_constants::match_any | std::regex_constants::match_not_null); +bool PathMatch::match(const std::string &pattern, const std::string &path, const std::string &basepath, Mode mode) +{ + return match_one(pattern, path, basepath, mode == Mode::icase); } diff --git a/lib/pathmatch.h b/lib/pathmatch.h index b0264df934c..4d542d099cb 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -22,7 +22,6 @@ #include "config.h" #include -#include #include #include @@ -31,24 +30,24 @@ /** * Path matching rules: - * - All rules are simplified first (path separators vary by platform): + * - All patterns are simplified first (path separators vary by platform): * - '/./' => '/' * - '/dir/../' => '/' * - '//' => '/' * - Trailing slashes are removed - * - Rules can contain globs: + * - Patterns can contain globs: * - '**' matches any number of characters including path separators. * - '*' matches any number of characters except path separators. * - '?' matches any single character except path separators. - * - If a rule looks like an absolute path (e.g. starts with '/', but varies by platform): - * - Match all files where the rule matches the start of the file's simplified absolute path up until a path + * - If a pattern looks like an absolute path (e.g. starts with '/', but varies by platform): + * - Match all files where the pattern matches the start of the file's simplified absolute path up until a path * separator or the end of the pathname. - * - If a rule starts with '.': - * - The rule is interpreted as a path relative to `basepath` and then converted to an absolute path and - * treated as such according to the above procedure. If the rule is relative to some other directory, it should - * be modified to be relative to `basepath` first (this should be done with rules in project files, for example). + * - If a pattern starts with '.': + * - The pattern is interpreted as a path relative to `basepath` and then converted to an absolute path and + * treated as such according to the above procedure. If the pattern is relative to some other directory, it should + * be modified to be relative to `basepath` first (this should be done with patterns in project files, for example). * - Otherwise: - * - Match all files where the rule matches any part of the file's simplified absolute path up until a + * - Match all files where the pattern matches any part of the file's simplified absolute path up until a * path separator or the end of the pathname, and the matching part directly follows a path separator. **/ @@ -66,36 +65,47 @@ class CPPCHECKLIB PathMatch { * icase: Case insensitive. **/ enum class Mode : std::uint8_t { - platform, scase, icase, }; +#ifdef _WIN32 + static constexpr Mode platform_mode = Mode::icase; +#else + static constexpr Mode platform_mode = Mode::scase; +#endif + /** * The constructor. * - * If a path is a directory it needs to end with a file separator. - * - * @param paths List of masks. - * @param basepath Path to which rules and matched paths are relative, when applicable. Can be relative, in which - * case it is appended to Path::getCurrentPath(). + * @param patterns List of patterns. + * @param basepath Path to which patterns and matched paths are relative, when applicable. * @param mode Case sensitivity mode. */ - explicit PathMatch(const std::vector &paths, const std::string &basepath = std::string(), Mode mode = Mode::platform); + explicit PathMatch(std::vector patterns, std::string basepath = std::string(), Mode mode = platform_mode); /** - * @brief Match path against list of masks. - * - * If you want to match a directory the given path needs to end with a path separator. + * @brief Match path against list of patterns. * * @param path Path to match. * @return true if any of the masks match the path, false otherwise. */ bool match(const std::string &path) const; + /** + * @brief Match path against a single pattern. + * + * @param pattern Pattern to use. + * @param path Path to match. + * @param basepath Path to which the pattern and path is relative, when applicable. + * @param mode Case sensitivity mode. + */ + static bool match(const std::string &pattern, const std::string &path, const std::string &basepath = std::string(), Mode mode = platform_mode); + private: + std::vector mPatterns; std::string mBasepath; - std::regex mRegex; + Mode mMode; }; /// @} diff --git a/lib/suppressions.cpp b/lib/suppressions.cpp index 82d80eb2c30..eb21c56a0f1 100644 --- a/lib/suppressions.cpp +++ b/lib/suppressions.cpp @@ -397,7 +397,7 @@ SuppressionList::Suppression::Result SuppressionList::Suppression::isSuppressed( if (!errorId.empty() && !matchglob(errorId, errmsg.errorId)) return Result::Checked; } else { - if (!fileName.empty() && !PathMatch({fileName}).match(errmsg.getFileName())) + if (!fileName.empty() && !PathMatch::match(fileName, errmsg.getFileName())) return Result::None; if ((SuppressionList::Type::unique == type) && (lineNumber != NO_LINE) && (lineNumber != errmsg.lineNumber)) { if (!thisAndNextLine || lineNumber + 1 != errmsg.lineNumber) From c5fb6db7744e3cc5cafb7d95b1e76ca061019320 Mon Sep 17 00:00:00 2001 From: glank Date: Thu, 10 Jul 2025 16:55:29 +0200 Subject: [PATCH 16/35] Use explicit basepath --- cli/cmdlineparser.cpp | 6 +++--- gui/filelist.cpp | 2 +- lib/importproject.cpp | 6 +++--- lib/pathmatch.h | 2 +- lib/suppressions.cpp | 2 +- test/helpers.cpp | 2 +- test/testfilelister.cpp | 20 +++++++++----------- tools/dmake/dmake.cpp | 2 +- 8 files changed, 20 insertions(+), 22 deletions(-) diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 1ec6f2187f0..3abdb683154 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -209,7 +209,7 @@ bool CmdLineParser::fillSettingsFromArgs(int argc, const char* const argv[]) std::list fileSettings; if (!mSettings.fileFilters.empty()) { // filter only for the selected filenames from all project files - PathMatch filtermatcher(mSettings.fileFilters); + PathMatch filtermatcher(mSettings.fileFilters, Path::getCurrentPath()); std::copy_if(fileSettingsRef.cbegin(), fileSettingsRef.cend(), std::back_inserter(fileSettings), [&](const FileSettings &fs) { return filtermatcher.match(fs.filename()); }); @@ -245,7 +245,7 @@ bool CmdLineParser::fillSettingsFromArgs(int argc, const char* const argv[]) std::list filesResolved; // Execute recursiveAddFiles() to each given file parameter // TODO: verbose log which files were ignored? - const PathMatch matcher(ignored); + const PathMatch matcher(ignored, Path::getCurrentPath()); for (const std::string &pathname : pathnamesRef) { const std::string err = FileLister::recursiveAddFiles(filesResolved, Path::toNativeSeparators(pathname), mSettings.library.markupExtensions(), matcher, mSettings.debugignore); if (!err.empty()) { @@ -2136,7 +2136,7 @@ bool CmdLineParser::loadCppcheckCfg() std::list CmdLineParser::filterFiles(const std::vector& fileFilters, const std::list& filesResolved) { std::list files; - PathMatch filtermatcher(fileFilters); + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); std::copy_if(filesResolved.cbegin(), filesResolved.cend(), std::inserter(files, files.end()), [&](const FileWithDetails& entry) { return filtermatcher.match(entry.path()) || filtermatcher.match(entry.spath()); }); diff --git a/gui/filelist.cpp b/gui/filelist.cpp index 43d44550ab0..f470fe56727 100644 --- a/gui/filelist.cpp +++ b/gui/filelist.cpp @@ -119,7 +119,7 @@ static std::vector toStdStringList(const QStringList &stringList) QStringList FileList::applyExcludeList() const { - const PathMatch pathMatch(toStdStringList(mExcludedPaths)); + const PathMatch pathMatch(toStdStringList(mExcludedPaths), QDir::currentPath().toStdString()); QStringList paths; for (const QFileInfo& item : mFileList) { diff --git a/lib/importproject.cpp b/lib/importproject.cpp index ccf9958087e..e1b9576ef18 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -45,7 +45,7 @@ void ImportProject::ignorePaths(const std::vector &ipaths, bool debug) { - PathMatch matcher(ipaths); + PathMatch matcher(ipaths, Path::getCurrentPath()); for (auto it = fileSettings.cbegin(); it != fileSettings.cend();) { if (matcher.match(it->filename())) { if (debug) @@ -840,7 +840,7 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X } // Project files - PathMatch filtermatcher(fileFilters); + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); for (const std::string &cfilename : compileList) { if (!fileFilters.empty() && !filtermatcher.match(cfilename)) continue; @@ -920,7 +920,7 @@ ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::strin SharedItemsProject result; result.pathToProjectFile = filename; - PathMatch filtermatcher(fileFilters); + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 4d542d099cb..9ac63fbdd93 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -82,7 +82,7 @@ class CPPCHECKLIB PathMatch { * @param basepath Path to which patterns and matched paths are relative, when applicable. * @param mode Case sensitivity mode. */ - explicit PathMatch(std::vector patterns, std::string basepath = std::string(), Mode mode = platform_mode); + explicit PathMatch(std::vector patterns = {}, std::string basepath = std::string(), Mode mode = platform_mode); /** * @brief Match path against list of patterns. diff --git a/lib/suppressions.cpp b/lib/suppressions.cpp index eb21c56a0f1..3f694a93ee9 100644 --- a/lib/suppressions.cpp +++ b/lib/suppressions.cpp @@ -397,7 +397,7 @@ SuppressionList::Suppression::Result SuppressionList::Suppression::isSuppressed( if (!errorId.empty() && !matchglob(errorId, errmsg.errorId)) return Result::Checked; } else { - if (!fileName.empty() && !PathMatch::match(fileName, errmsg.getFileName())) + if (!fileName.empty() && !PathMatch::match(fileName, errmsg.getFileName(), std::string())) return Result::None; if ((SuppressionList::Type::unique == type) && (lineNumber != NO_LINE) && (lineNumber != errmsg.lineNumber)) { if (!thisAndNextLine || lineNumber + 1 != errmsg.lineNumber) diff --git a/test/helpers.cpp b/test/helpers.cpp index 8f9d83dd0d1..4e417c6fdce 100644 --- a/test/helpers.cpp +++ b/test/helpers.cpp @@ -87,7 +87,7 @@ ScopedFile::~ScopedFile() { // TODO: simplify the function call // hack to be able to delete *.plist output files std::list files; - const std::string res = FileLister::addFiles(files, mPath, {".plist"}, false, PathMatch({})); + const std::string res = FileLister::addFiles(files, mPath, {".plist"}, false, PathMatch()); if (!res.empty()) { std::cout << "ScopedFile(" << mPath + ") - generating file list failed (" << res << ")" << std::endl; } diff --git a/test/testfilelister.cpp b/test/testfilelister.cpp index 9cae2b29c1f..13057735513 100644 --- a/test/testfilelister.cpp +++ b/test/testfilelister.cpp @@ -60,8 +60,7 @@ class TestFileLister : public TestFixture { // Recursively add add files.. std::list files; - PathMatch matcher({}); - std::string err = FileLister::recursiveAddFiles(files, adddir, {}, matcher); + std::string err = FileLister::recursiveAddFiles(files, adddir, {}, PathMatch()); ASSERT_EQUALS("", err); ASSERT(!files.empty()); @@ -107,7 +106,7 @@ class TestFileLister : public TestFixture { void recursiveAddFilesEmptyPath() const { std::list files; - const std::string err = FileLister::recursiveAddFiles(files, "", {}, PathMatch({})); + const std::string err = FileLister::recursiveAddFiles(files, "", {}, PathMatch()); ASSERT_EQUALS("no path specified", err); } @@ -125,8 +124,7 @@ class TestFileLister : public TestFixture { const std::string basedir = findBaseDir(); std::list files; - PathMatch matcher({}); - std::string err = FileLister::recursiveAddFiles(files, basedir + "lib/token.cpp", {}, matcher); + std::string err = FileLister::recursiveAddFiles(files, basedir + "lib/token.cpp", {}, PathMatch()); ASSERT_EQUALS("", err); ASSERT_EQUALS(1, files.size()); ASSERT_EQUALS(basedir + "lib/token.cpp", files.begin()->path()); @@ -136,7 +134,7 @@ class TestFileLister : public TestFixture { const std::string basedir = findBaseDir() + "."; std::list files; - PathMatch matcher({"lib/"}); // needs to end with slash so it matches directories - added by CmdLineParser + PathMatch matcher({"lib/"}); std::string err = FileLister::recursiveAddFiles(files, basedir, {}, matcher); ASSERT_EQUALS("", err); ASSERT(!files.empty()); @@ -159,27 +157,27 @@ class TestFileLister : public TestFixture { { const std::string addfile = Path::join(Path::join(adddir, "cli"), "main.cpp"); - const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch({})); + const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch()); ASSERT_EQUALS("", err); } { const std::string addfile = Path::join(Path::join(adddir, "lib"), "token.cpp"); - const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch({})); + const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch()); ASSERT_EQUALS("", err); } { const std::string addfile = Path::join(Path::join(adddir, "cli"), "token.cpp"); // does not exist - const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch({})); + const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch()); ASSERT_EQUALS("", err); } { const std::string addfile = Path::join(Path::join(adddir, "lib2"), "token.cpp"); // does not exist - const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch({})); + const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch()); ASSERT_EQUALS("", err); } { const std::string addfile = Path::join(Path::join(adddir, "lib"), "matchcompiler.h"); - const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch({})); + const std::string err = FileLister::addFiles(files, addfile, {}, true,PathMatch()); ASSERT_EQUALS("", err); } diff --git a/tools/dmake/dmake.cpp b/tools/dmake/dmake.cpp index f54907c3759..dd0e5de0f66 100644 --- a/tools/dmake/dmake.cpp +++ b/tools/dmake/dmake.cpp @@ -170,7 +170,7 @@ static std::string getCppFiles(std::vector &files, const std::strin std::list filelist; const std::set extra; const std::vector masks; - const PathMatch matcher(masks); + const PathMatch matcher(masks, Path::getCurrentPath()); std::string err = FileLister::addFiles(filelist, path, extra, recursive, matcher); if (!err.empty()) return err; From 2671b60bc7ac7fc4efd1c5c9179a79680f3df1a1 Mon Sep 17 00:00:00 2001 From: glank Date: Thu, 10 Jul 2025 17:55:01 +0200 Subject: [PATCH 17/35] Fix clang-tidy issue --- lib/pathmatch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index c10927fbd5b..3ee2aa642cb 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -240,7 +240,7 @@ static bool match_one(const std::string &pattern, const std::string &path, const } } - if (b.size() != 0) { + if (!b.empty()) { const auto &bp = b.top(); b.pop(); s.st = bp.first; From 8e50d1ddddefd76034089990e946c2db7d777a41 Mon Sep 17 00:00:00 2001 From: glank Date: Thu, 10 Jul 2025 19:53:10 +0200 Subject: [PATCH 18/35] Suppression match shortcut --- lib/pathmatch.cpp | 39 +++++++++++++++++++++------------------ lib/suppressions.cpp | 2 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 3ee2aa642cb..7849ceca768 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -27,7 +27,8 @@ #include #include -struct Pathstr { +struct Pathstr +{ static Pathstr from_pattern(const std::string &pattern, const std::string &basepath, bool icase) { if (!pattern.empty() && pattern[0] == '.') @@ -89,7 +90,8 @@ struct Pathstr { return c; } - void simplify(bool leadsep) { + void simplify(bool leadsep) + { while (left() != 0) { State rst = st; @@ -161,16 +163,18 @@ struct Pathstr { } } - Pathstr &operator++(int) { + void operator++() + { advance(); - return *this; } - char operator*() const { + char operator*() const + { return current(); } - struct State { + struct State + { const char *p; std::size_t l; int c {EOF}; @@ -195,6 +199,7 @@ static bool match_one(const std::string &pattern, const std::string &path, const Pathstr s = Pathstr::from_pattern(pattern, basepath, icase); Pathstr t = Pathstr::from_path(path, basepath, icase); + Pathstr p = s; Pathstr q = t; @@ -204,23 +209,23 @@ static bool match_one(const std::string &pattern, const std::string &path, const switch (*s) { case '*': { bool slash = false; - s++; + ++s; if (*s == '*') { slash = true; - s++; + ++s; } b.emplace(s.st, t.st); while (*t != '\0' && (slash || *t != '/')) { if (*s == *t) b.emplace(s.st, t.st); - t++; + ++t; } continue; } case '?': { if (*t != '\0' && *t != '/') { - s++; - t++; + ++s; + ++t; continue; } break; @@ -232,8 +237,8 @@ static bool match_one(const std::string &pattern, const std::string &path, const } default: { if (*s == *t) { - s++; - t++; + ++s; + ++t; continue; } break; @@ -249,10 +254,10 @@ static bool match_one(const std::string &pattern, const std::string &path, const } while (*q != '\0' && *q != '/') - q++; + ++q; if (*q == '/') { - q++; + ++q; s = p; t = q; continue; @@ -269,10 +274,8 @@ PathMatch::PathMatch(std::vector patterns, std::string basepath, Mo bool PathMatch::match(const std::string &path) const { - bool icase = (mMode == Mode::icase); - return std::any_of(mPatterns.cbegin(), mPatterns.cend(), [=] (const std::string &pattern) { - return match_one(pattern, path, mBasepath, icase); + return match_one(pattern, path, mBasepath, mMode == Mode::icase); }); } diff --git a/lib/suppressions.cpp b/lib/suppressions.cpp index 3f694a93ee9..7eb9caff60d 100644 --- a/lib/suppressions.cpp +++ b/lib/suppressions.cpp @@ -397,7 +397,7 @@ SuppressionList::Suppression::Result SuppressionList::Suppression::isSuppressed( if (!errorId.empty() && !matchglob(errorId, errmsg.errorId)) return Result::Checked; } else { - if (!fileName.empty() && !PathMatch::match(fileName, errmsg.getFileName(), std::string())) + if (!fileName.empty() && fileName != errmsg.getFileName() && !PathMatch::match(fileName, errmsg.getFileName())) return Result::None; if ((SuppressionList::Type::unique == type) && (lineNumber != NO_LINE) && (lineNumber != errmsg.lineNumber)) { if (!thisAndNextLine || lineNumber + 1 != errmsg.lineNumber) From 89c0e71dea37640643018cd8b47c06fae352ad3c Mon Sep 17 00:00:00 2001 From: glank Date: Thu, 10 Jul 2025 20:54:17 +0200 Subject: [PATCH 19/35] Suppression stuff --- lib/suppressions.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/suppressions.cpp b/lib/suppressions.cpp index 7eb9caff60d..bf97bdb2b81 100644 --- a/lib/suppressions.cpp +++ b/lib/suppressions.cpp @@ -397,12 +397,12 @@ SuppressionList::Suppression::Result SuppressionList::Suppression::isSuppressed( if (!errorId.empty() && !matchglob(errorId, errmsg.errorId)) return Result::Checked; } else { - if (!fileName.empty() && fileName != errmsg.getFileName() && !PathMatch::match(fileName, errmsg.getFileName())) - return Result::None; if ((SuppressionList::Type::unique == type) && (lineNumber != NO_LINE) && (lineNumber != errmsg.lineNumber)) { if (!thisAndNextLine || lineNumber + 1 != errmsg.lineNumber) return Result::None; } + if (!fileName.empty() && fileName != errmsg.getFileName() && !PathMatch::match(fileName, errmsg.getFileName())) + return Result::None; if (hash > 0 && hash != errmsg.hash) return Result::Checked; // the empty check is a hack to allow wildcard suppressions on IDs to be marked as checked From 3327d87ea40d0cdc2195ff3f3bd0619d09201e6c Mon Sep 17 00:00:00 2001 From: glank Date: Fri, 11 Jul 2025 14:37:40 +0200 Subject: [PATCH 20/35] Cleanups, comments, more tests --- lib/pathmatch.cpp | 206 ++++-------------------------------- lib/pathmatch.h | 232 ++++++++++++++++++++++++++++++++++++++++- test/testpathmatch.cpp | 16 +++ 3 files changed, 265 insertions(+), 189 deletions(-) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 7849ceca768..2745556ce21 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -21,173 +21,23 @@ #include "path.h" #include -#include -#include #include #include #include -struct Pathstr -{ - static Pathstr from_pattern(const std::string &pattern, const std::string &basepath, bool icase) - { - if (!pattern.empty() && pattern[0] == '.') - return Pathstr(basepath.c_str(), pattern.c_str(), icase); - return Pathstr(pattern.c_str(), nullptr, icase); - } - - static Pathstr from_path(const std::string &path, const std::string &basepath, bool icase) - { - if (Path::isAbsolute(path)) - return Pathstr(path.c_str(), nullptr, icase); - return Pathstr(basepath.c_str(), path.c_str(), icase); - } - - explicit Pathstr(const char *a = nullptr, const char *b = nullptr, bool lowercase = false) : - s{a, b}, lcase(lowercase) - { - for (int i = 0; i < 2; i++) { - e[i] = s[i]; - - if (s[i] == nullptr || *s[i] == '\0') - continue; - - if (st.l != 0) - st.l++; - - while (*e[i] != '\0') { - e[i]++; - st.l++; - } - - st.p = e[i]; - } - - if (st.l == 0) - st.c = '\0'; - - simplify(false); - } - - std::size_t left() const - { - return st.l; - } - - char current() const - { - if (st.c != EOF) - return st.c; - - char c = st.p[-1]; - - if (c == '\\') - return '/'; - - if (lcase) - return std::tolower(c); - - return c; - } - - void simplify(bool leadsep) - { - while (left() != 0) { - State rst = st; - - if (leadsep) { - if (current() != '/') - break; - nextc(); - } - - char c = current(); - if (c == '.') { - nextc(); - c = current(); - if (c == '.') { - nextc(); - c = current(); - if (c == '/') { - /* Skip '/../' */ - nextc(); - simplify(false); - while (left() != 0 && current() != '/') - nextc(); - continue; - } - } else if (c == '/') { - /* Skip '/./' */ - continue; - } else if (c == '\0') { - /* Skip leading './' */ - break; - } - } else if (c == '/' && left() != 1) { - /* Skip double separator (keep root) */ - nextc(); - leadsep = false; - continue; - } - - st = rst; - break; - } - } - - void advance() - { - nextc(); - - if (current() == '/') - simplify(true); - } - void nextc() - { - if (st.l == 0) - return; - - st.l--; - - if (st.l == 0) - st.c = '\0'; - else if (st.c != EOF) { - st.c = EOF; - } else { - st.p--; - if (st.p == s[1]) { - st.p = e[0]; - st.c = '/'; - } - } - } - - void operator++() - { - advance(); - } - - char operator*() const - { - return current(); - } - - struct State - { - const char *p; - std::size_t l; - int c {EOF}; - }; - - const char *s[2] {}; - const char *e[2] {}; - State st {}; - bool lcase; -}; +PathMatch::PathMatch(std::vector patterns, std::string basepath, Mode mode) : + mPatterns(std::move(patterns)), mBasepath(std::move(basepath)), mMode(mode) +{} +bool PathMatch::match(const std::string &path) const +{ + return std::any_of(mPatterns.cbegin(), mPatterns.cend(), [=] (const std::string &pattern) { + return match(pattern, path, mBasepath, mMode); + }); +} -static bool match_one(const std::string &pattern, const std::string &path, const std::string &basepath, bool icase) +bool PathMatch::match(const std::string &pattern, const std::string &path, const std::string &basepath, Mode mode) { if (pattern.empty()) return false; @@ -197,13 +47,12 @@ static bool match_one(const std::string &pattern, const std::string &path, const bool real = Path::isAbsolute(pattern) || pattern[0] == '.'; - Pathstr s = Pathstr::from_pattern(pattern, basepath, icase); - Pathstr t = Pathstr::from_path(path, basepath, icase); - - Pathstr p = s; - Pathstr q = t; + PathIterator s = PathIterator::from_pattern(pattern, basepath, mode == Mode::icase); + PathIterator t = PathIterator::from_path(path, basepath, mode == Mode::icase); + PathIterator p = s; + PathIterator q = t; - std::stack> b; + std::stack> b; for (;;) { switch (*s) { @@ -214,10 +63,10 @@ static bool match_one(const std::string &pattern, const std::string &path, const slash = true; ++s; } - b.emplace(s.st, t.st); + b.emplace(s.getpos(), t.getpos()); while (*t != '\0' && (slash || *t != '/')) { if (*s == *t) - b.emplace(s.st, t.st); + b.emplace(s.getpos(), t.getpos()); ++t; } continue; @@ -248,8 +97,8 @@ static bool match_one(const std::string &pattern, const std::string &path, const if (!b.empty()) { const auto &bp = b.top(); b.pop(); - s.st = bp.first; - t.st = bp.second; + s.setpos(bp.first); + t.setpos(bp.second); continue; } @@ -266,20 +115,3 @@ static bool match_one(const std::string &pattern, const std::string &path, const return false; } } - - -PathMatch::PathMatch(std::vector patterns, std::string basepath, Mode mode) : - mPatterns(std::move(patterns)), mBasepath(std::move(basepath)), mMode(mode) -{} - -bool PathMatch::match(const std::string &path) const -{ - return std::any_of(mPatterns.cbegin(), mPatterns.cend(), [=] (const std::string &pattern) { - return match_one(pattern, path, mBasepath, mMode == Mode::icase); - }); -} - -bool PathMatch::match(const std::string &pattern, const std::string &path, const std::string &basepath, Mode mode) -{ - return match_one(pattern, path, basepath, mode == Mode::icase); -} diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 9ac63fbdd93..c6c3cebd257 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -21,10 +21,14 @@ #include "config.h" +#include #include +#include #include #include +#include "path.h" + /// @addtogroup CLI /// @{ @@ -58,9 +62,8 @@ class CPPCHECKLIB PathMatch { public: /** - * @brief Case sensitivity mode. + * @brief Match mode. * - * platform: Use the platform default. * scase: Case sensitive. * icase: Case insensitive. **/ @@ -69,6 +72,9 @@ class CPPCHECKLIB PathMatch { icase, }; + /** + * @brief The default mode for the current platform. + **/ #ifdef _WIN32 static constexpr Mode platform_mode = Mode::icase; #else @@ -99,15 +105,237 @@ class CPPCHECKLIB PathMatch { * @param path Path to match. * @param basepath Path to which the pattern and path is relative, when applicable. * @param mode Case sensitivity mode. + * @return true if the pattern matches the path, false otherwise. */ static bool match(const std::string &pattern, const std::string &path, const std::string &basepath = std::string(), Mode mode = platform_mode); private: + friend class TestPathMatch; + class PathIterator; + std::vector mPatterns; std::string mBasepath; Mode mMode; }; +/** + * A more correct and less convenient name for this class would be PathStringsCanonicalReverseIterator. + * + * This class takes two path strings and iterates their concatenation in reverse while doing canonicalization, + * i.e. collapsing double-dots, removing extra slashes, dot-slashes, and trailing slashes, as well as converting + * native slashes to forward slashes and optionally converting characters to lowercase. + * + * Both strings are optional. If both strings are present, then they're concatenated with a slash + * (subject to canonicalization). + * + * Double-dots at the root level are removed. The root slash is preserved, other trailing slashes are removed. + * + * Doing the iteration in reverse allows canonicalization to be performed without lookahead. This is useful + * for comparing path strings, potentially relative to different base paths, without having to do prior string + * processing or extra allocations. + * + * The length of the output is at most strlen(a) + strlen(b) + 1. + * + * Example: + * - input: "/hello/universe/.", "../world//" + * - output: "dlrow/olleh/" + **/ +class PathMatch::PathIterator { +public: + /* Create from a pattern and base path */ + static PathIterator from_pattern(const std::string &pattern, const std::string &basepath, bool icase) + { + if (!pattern.empty() && pattern[0] == '.') + return PathIterator(basepath.c_str(), pattern.c_str(), icase); + return PathIterator(pattern.c_str(), nullptr, icase); + } + + /* Create from path and base path */ + static PathIterator from_path(const std::string &path, const std::string &basepath, bool icase) + { + if (Path::isAbsolute(path)) + return PathIterator(path.c_str(), nullptr, icase); + return PathIterator(basepath.c_str(), path.c_str(), icase); + } + + /* Constructor */ + explicit PathIterator(const char *path_a = nullptr, const char *path_b = nullptr, bool lowercase = false) : + s{path_a, path_b}, lcase(lowercase) + { + for (int i = 0; i < 2; i++) { + e[i] = s[i]; + + if (s[i] == nullptr || *s[i] == '\0') + continue; + + if (st.l != 0) + st.l++; + + while (*e[i] != '\0') { + e[i]++; + st.l++; + } + + st.p = e[i]; + } + + if (st.l == 0) + st.c = '\0'; + + skips(false); + } + + /* Position struct */ + struct Pos { + /* String pointer */ + const char *p; + /* Raw characters left */ + std::size_t l; + /* Buffered character */ + int c {EOF}; + }; + + /* Save the current position */ + const Pos &getpos() const + { + return st; + } + + /* Restore a saved position */ + void setpos(const Pos &pos) + { + st = pos; + } + + /* Read the current character */ + char operator*() const + { + return current(); + } + + /* Go to the next character */ + void operator++() + { + advance(); + } + + /* Consume remaining characters into an std::string and reverse, use for testing */ + std::string read() + { + std::string s; + + while (current() != '\0') { + s.insert(0, 1, current()); + advance(); + } + + return s; + } + +private: + /* Read the current character */ + char current() const + { + if (st.c != EOF) + return st.c; + + char c = st.p[-1]; + + if (c == '\\') + return '/'; + + if (lcase) + return std::tolower(c); + + return c; + } + + /* Do canonicalization on a path component boundary */ + void skips(bool leadsep) + { + while (st.l != 0) { + Pos rst = st; + + if (leadsep) { + if (current() != '/') + break; + nextc(); + } + + char c = current(); + if (c == '.') { + nextc(); + c = current(); + if (c == '.') { + nextc(); + c = current(); + if (c == '/') { + /* Skip '/../' */ + nextc(); + skips(false); + while (st.l != 0 && current() != '/') + nextc(); + continue; + } + } else if (c == '/') { + /* Skip '/./' */ + continue; + } else if (c == '\0') { + /* Skip leading './' */ + break; + } + } else if (c == '/' && st.l != 1) { + /* Skip double separator (keep root) */ + nextc(); + leadsep = false; + continue; + } + + st = rst; + break; + } + } + + /* Go to the next character, doing skips on path separators */ + void advance() + { + nextc(); + + if (current() == '/') + skips(true); + } + + /* Go to the next character */ + void nextc() + { + if (st.l == 0) + return; + + st.l--; + + if (st.l == 0) + st.c = '\0'; + else if (st.c != EOF) { + st.c = EOF; + } else { + st.p--; + if (st.p == s[1]) { + st.p = e[0]; + st.c = '/'; + } + } + } + + /* String start pointers */ + const char *s[2] {}; + /* String end pointers */ + const char *e[2] {}; + /* Current position */ + Pos st {}; + /* Lowercase conversion flag */ + bool lcase; +}; + /// @} #endif // PATHMATCH_H diff --git a/test/testpathmatch.cpp b/test/testpathmatch.cpp index e7f056fdca2..ca35ec6780d 100644 --- a/test/testpathmatch.cpp +++ b/test/testpathmatch.cpp @@ -74,6 +74,7 @@ class TestPathMatch : public TestFixture { TEST_CASE(glob); TEST_CASE(globstar1); TEST_CASE(globstar2); + TEST_CASE(pathiterator); } // Test empty PathMatch @@ -249,6 +250,21 @@ class TestPathMatch : public TestFixture { ASSERT(!match.match("src/lib/foo/foo.cpp")); ASSERT(!match.match("src/lib/foo/bar/foo.cpp")); } + + void pathiterator() const { + using PathIterator = PathMatch::PathIterator; + ASSERT_EQUALS("/hello/world", PathIterator("/hello/universe/.", "../world//").read()); + ASSERT_EQUALS("/", PathIterator("//./..//.///.", "../../..///").read()); + ASSERT_EQUALS("/foo/bar", PathIterator(nullptr, "/foo/bar/.").read()); + ASSERT_EQUALS("/foo/bar", PathIterator("/foo/bar/.", nullptr).read()); + ASSERT_EQUALS("/foo/bar", PathIterator("/foo", "bar").read()); + ASSERT_EQUALS("", PathIterator("", "").read()); + ASSERT_EQUALS("", PathIterator("", nullptr).read()); + ASSERT_EQUALS("", PathIterator(nullptr, "").read()); + ASSERT_EQUALS("", PathIterator(nullptr, nullptr).read()); + ASSERT_EQUALS("c:/windows/system32", PathIterator("C:", "Windows\\System32\\Drivers\\..\\.", true).read()); + ASSERT_EQUALS("C:", PathIterator("C:\\Program Files\\", "..").read()); + } }; REGISTER_TEST(TestPathMatch) From 8d378914fcf180258085665cfab40efb6134b96d Mon Sep 17 00:00:00 2001 From: glank Date: Fri, 11 Jul 2025 14:56:31 +0200 Subject: [PATCH 21/35] More comments --- lib/pathmatch.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 2745556ce21..36cff7ab24d 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -45,32 +45,46 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const if (pattern == "*" || pattern == "**") return true; + /* A "real" path is absolute or relative to the base path. A pattern that isn't "real" can match at any + * path component boundary. */ bool real = Path::isAbsolute(pattern) || pattern[0] == '.'; + /* Pattern iterator */ PathIterator s = PathIterator::from_pattern(pattern, basepath, mode == Mode::icase); + /* Path iterator */ PathIterator t = PathIterator::from_path(path, basepath, mode == Mode::icase); + /* Pattern restart position */ PathIterator p = s; + /* Path restart position */ PathIterator q = t; + /* Backtrack stack */ std::stack> b; for (;;) { switch (*s) { + /* Star or star-star, matches any number of characters */ case '*': { bool slash = false; ++s; if (*s == '*') { + /* Star-star matches slashes as well */ slash = true; ++s; } + /* Add backtrack for matching zero characters */ b.emplace(s.getpos(), t.getpos()); while (*t != '\0' && (slash || *t != '/')) { - if (*s == *t) + if (*s == *t) { + /* Could stop here, but do greedy match and add + * backtrack instead */ b.emplace(s.getpos(), t.getpos()); + } ++t; } continue; } + /* Single character wildcard */ case '?': { if (*t != '\0' && *t != '/') { ++s; @@ -79,11 +93,14 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const } break; } + /* Start of pattern; matches start of path, or a path separator if the + * pattern is not "real" (an absolute or relative path). */ case '\0': { if (*t == '\0' || (*t == '/' && !real)) return true; break; } + /* Literal character */ default: { if (*s == *t) { ++s; @@ -94,6 +111,7 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const } } + /* No match, try to backtrack */ if (!b.empty()) { const auto &bp = b.top(); b.pop(); @@ -102,6 +120,7 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const continue; } + /* Couldn't bactrack, try matching from the next path separator */ while (*q != '\0' && *q != '/') ++q; @@ -112,6 +131,7 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const continue; } + /* No more path seperators to try from */ return false; } } From a3ee6d005c851eb83afc1adb226c827b9decfaf5 Mon Sep 17 00:00:00 2001 From: glank Date: Fri, 11 Jul 2025 15:03:25 +0200 Subject: [PATCH 22/35] Fix shadowing --- lib/pathmatch.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pathmatch.h b/lib/pathmatch.h index c6c3cebd257..6d7ed2048c2 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -222,14 +222,14 @@ class PathMatch::PathIterator { /* Consume remaining characters into an std::string and reverse, use for testing */ std::string read() { - std::string s; + std::string str; while (current() != '\0') { - s.insert(0, 1, current()); + str.insert(0, 1, current()); advance(); } - return s; + return str; } private: From 3f815c85f2de8e056d2b94134cec1fadcb900606 Mon Sep 17 00:00:00 2001 From: glank Date: Fri, 11 Jul 2025 15:07:42 +0200 Subject: [PATCH 23/35] Run dmake --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 54267989b43..d71c614e903 100644 --- a/Makefile +++ b/Makefile @@ -816,7 +816,7 @@ test/testother.o: test/testother.cpp lib/addoninfo.h lib/check.h lib/checkers.h test/testpath.o: test/testpath.cpp lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/tokenize.h lib/tokenlist.h lib/utils.h test/fixture.h test/helpers.h $(CXX) ${INCLUDE_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testpath.cpp -test/testpathmatch.o: test/testpathmatch.cpp lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/pathmatch.h lib/platform.h lib/settings.h lib/standards.h lib/utils.h test/fixture.h +test/testpathmatch.o: test/testpathmatch.cpp lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/settings.h lib/standards.h lib/utils.h test/fixture.h $(CXX) ${INCLUDE_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testpathmatch.cpp test/testplatform.o: test/testplatform.cpp externals/tinyxml2/tinyxml2.h lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/utils.h lib/xml.h test/fixture.h From b383b364f1f35c9eba8c75a003acb2bfb7e443b3 Mon Sep 17 00:00:00 2001 From: glank Date: Fri, 11 Jul 2025 15:12:02 +0200 Subject: [PATCH 24/35] Fix typo --- lib/pathmatch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 36cff7ab24d..0cf145c629e 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -120,7 +120,7 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const continue; } - /* Couldn't bactrack, try matching from the next path separator */ + /* Couldn't backtrack, try matching from the next path separator */ while (*q != '\0' && *q != '/') ++q; From 07135128b4eb39f12aecaed89203ce0384ee9543 Mon Sep 17 00:00:00 2001 From: glank Date: Fri, 11 Jul 2025 16:49:05 +0200 Subject: [PATCH 25/35] Fix naming --- lib/pathmatch.h | 74 ++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 6d7ed2048c2..91d82918b8b 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -159,28 +159,28 @@ class PathMatch::PathIterator { } /* Constructor */ - explicit PathIterator(const char *path_a = nullptr, const char *path_b = nullptr, bool lowercase = false) : - s{path_a, path_b}, lcase(lowercase) + explicit PathIterator(const char *path_a = nullptr, const char *path_b = nullptr, bool lower = false) : + mStart{path_a, path_b}, mLower(lower) { for (int i = 0; i < 2; i++) { - e[i] = s[i]; + mEnd[i] = mStart[i]; - if (s[i] == nullptr || *s[i] == '\0') + if (mStart[i] == nullptr || *mStart[i] == '\0') continue; - if (st.l != 0) - st.l++; + if (mPos.l != 0) + mPos.l++; - while (*e[i] != '\0') { - e[i]++; - st.l++; + while (*mEnd[i] != '\0') { + mEnd[i]++; + mPos.l++; } - st.p = e[i]; + mPos.p = mEnd[i]; } - if (st.l == 0) - st.c = '\0'; + if (mPos.l == 0) + mPos.c = '\0'; skips(false); } @@ -198,13 +198,13 @@ class PathMatch::PathIterator { /* Save the current position */ const Pos &getpos() const { - return st; + return mPos; } /* Restore a saved position */ void setpos(const Pos &pos) { - st = pos; + mPos = pos; } /* Read the current character */ @@ -236,15 +236,15 @@ class PathMatch::PathIterator { /* Read the current character */ char current() const { - if (st.c != EOF) - return st.c; + if (mPos.c != EOF) + return mPos.c; - char c = st.p[-1]; + char c = mPos.p[-1]; if (c == '\\') return '/'; - if (lcase) + if (mLower) return std::tolower(c); return c; @@ -253,8 +253,8 @@ class PathMatch::PathIterator { /* Do canonicalization on a path component boundary */ void skips(bool leadsep) { - while (st.l != 0) { - Pos rst = st; + while (mPos.l != 0) { + Pos pos = mPos; if (leadsep) { if (current() != '/') @@ -273,7 +273,7 @@ class PathMatch::PathIterator { /* Skip '/../' */ nextc(); skips(false); - while (st.l != 0 && current() != '/') + while (mPos.l != 0 && current() != '/') nextc(); continue; } @@ -284,14 +284,14 @@ class PathMatch::PathIterator { /* Skip leading './' */ break; } - } else if (c == '/' && st.l != 1) { + } else if (c == '/' && mPos.l != 1) { /* Skip double separator (keep root) */ nextc(); leadsep = false; continue; } - st = rst; + mPos = pos; break; } } @@ -308,32 +308,32 @@ class PathMatch::PathIterator { /* Go to the next character */ void nextc() { - if (st.l == 0) + if (mPos.l == 0) return; - st.l--; + mPos.l--; - if (st.l == 0) - st.c = '\0'; - else if (st.c != EOF) { - st.c = EOF; + if (mPos.l == 0) + mPos.c = '\0'; + else if (mPos.c != EOF) { + mPos.c = EOF; } else { - st.p--; - if (st.p == s[1]) { - st.p = e[0]; - st.c = '/'; + mPos.p--; + if (mPos.p == mStart[1]) { + mPos.p = mEnd[0]; + mPos.c = '/'; } } } /* String start pointers */ - const char *s[2] {}; + const char *mStart[2] {}; /* String end pointers */ - const char *e[2] {}; + const char *mEnd[2] {}; /* Current position */ - Pos st {}; + Pos mPos {}; /* Lowercase conversion flag */ - bool lcase; + bool mLower; }; /// @} From 625d613f81d786e4e09a1384908b8eb7905a06fe Mon Sep 17 00:00:00 2001 From: glank Date: Fri, 11 Jul 2025 18:43:39 +0200 Subject: [PATCH 26/35] Update comment --- lib/pathmatch.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 91d82918b8b..116f5560cfd 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -142,7 +142,7 @@ class CPPCHECKLIB PathMatch { **/ class PathMatch::PathIterator { public: - /* Create from a pattern and base path */ + /* Create from a pattern and base path, patterns must begin with '.' to be considered relative */ static PathIterator from_pattern(const std::string &pattern, const std::string &basepath, bool icase) { if (!pattern.empty() && pattern[0] == '.') From 0c7d92427fb89bf7cf299767bfc4c87287c8975f Mon Sep 17 00:00:00 2001 From: glank Date: Sat, 12 Jul 2025 16:11:06 +0200 Subject: [PATCH 27/35] Update rules for relative patterns --- lib/importproject.cpp | 10 +++---- lib/pathmatch.cpp | 6 ++--- lib/pathmatch.h | 63 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/lib/importproject.cpp b/lib/importproject.cpp index e1b9576ef18..a7b434a84c6 100644 --- a/lib/importproject.cpp +++ b/lib/importproject.cpp @@ -1254,7 +1254,7 @@ static std::list readXmlStringList(const tinyxml2::XMLElement *node continue; const char *attr = attribute ? child->Attribute(attribute) : child->GetText(); if (attr) - ret.push_back(joinRelativePath(path, attr)); + ret.emplace_back(joinRelativePath(path, attr)); } return ret; } @@ -1266,12 +1266,8 @@ static std::list readXmlPathMatchList(const tinyxml2::XMLElement *n if (strcmp(child->Name(), name) != 0) continue; const char *attr = attribute ? child->Attribute(attribute) : child->GetText(); - if (attr) { - if (attr[0] == '.') - ret.push_back(joinRelativePath(path, attr)); - else - ret.emplace_back(attr); - } + if (attr) + ret.emplace_back(PathMatch::joinRelativePattern(path, attr)); } return ret; } diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 0cf145c629e..092146357c3 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -47,12 +47,12 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const /* A "real" path is absolute or relative to the base path. A pattern that isn't "real" can match at any * path component boundary. */ - bool real = Path::isAbsolute(pattern) || pattern[0] == '.'; + bool real = Path::isAbsolute(pattern) || isRelativePattern(pattern); /* Pattern iterator */ - PathIterator s = PathIterator::from_pattern(pattern, basepath, mode == Mode::icase); + PathIterator s = PathIterator::fromPattern(pattern, basepath, mode == Mode::icase); /* Path iterator */ - PathIterator t = PathIterator::from_path(path, basepath, mode == Mode::icase); + PathIterator t = PathIterator::fromPath(path, basepath, mode == Mode::icase); /* Pattern restart position */ PathIterator p = s; /* Path restart position */ diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 116f5560cfd..7934d7b5ffd 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -34,24 +34,25 @@ /** * Path matching rules: - * - All patterns are simplified first (path separators vary by platform): + * - All patterns are canonicalized (path separators vary by platform): * - '/./' => '/' * - '/dir/../' => '/' * - '//' => '/' - * - Trailing slashes are removed + * - Trailing slashes are removed (root slash is preserved) * - Patterns can contain globs: * - '**' matches any number of characters including path separators. * - '*' matches any number of characters except path separators. * - '?' matches any single character except path separators. * - If a pattern looks like an absolute path (e.g. starts with '/', but varies by platform): - * - Match all files where the pattern matches the start of the file's simplified absolute path up until a path + * - Match all files where the pattern matches the start of the file's canonical absolute path up until a path * separator or the end of the pathname. - * - If a pattern starts with '.': + * - If a pattern looks like a relative path, i.e. is '.' or '..', or + * starts with '.' or '..' followed by a path separator: * - The pattern is interpreted as a path relative to `basepath` and then converted to an absolute path and * treated as such according to the above procedure. If the pattern is relative to some other directory, it should * be modified to be relative to `basepath` first (this should be done with patterns in project files, for example). * - Otherwise: - * - Match all files where the pattern matches any part of the file's simplified absolute path up until a + * - Match all files where the pattern matches any part of the file's canonical absolute path up until a * path separator or the end of the pathname, and the matching part directly follows a path separator. **/ @@ -66,7 +67,7 @@ class CPPCHECKLIB PathMatch { * * scase: Case sensitive. * icase: Case insensitive. - **/ + */ enum class Mode : std::uint8_t { scase, icase, @@ -74,7 +75,7 @@ class CPPCHECKLIB PathMatch { /** * @brief The default mode for the current platform. - **/ + */ #ifdef _WIN32 static constexpr Mode platform_mode = Mode::icase; #else @@ -109,6 +110,44 @@ class CPPCHECKLIB PathMatch { */ static bool match(const std::string &pattern, const std::string &path, const std::string &basepath = std::string(), Mode mode = platform_mode); + /** + * @brief Check if a pattern is a relative path name. + * + * @param pattern Pattern to check. + * @return true if the pattern has the form of a relative path name pattern. + */ + static bool isRelativePattern(const std::string &pattern) + { + if (pattern.empty() || pattern[0] != '.') + return false; + + if (pattern.size() < 2 || pattern[1] == '/' || pattern[1] == '\\') + return true; + + if (pattern[1] != '.') + return false; + + if (pattern.size() < 3 || pattern[2] == '/' || pattern[2] == '\\') + return true; + + return false; + } + + /** + * @brief Join a pattern with a base path. + * + * @param basepath The base path to join the pattern to. + * @param pattern The pattern to join. + * @return The pattern appended to the base path with a separator if the pattern is a relative + * path name, otherwise just returns pattern. + */ + static std::string joinRelativePattern(const std::string &basepath, const std::string &pattern) + { + if (isRelativePattern(pattern)) + return Path::join(basepath, pattern); + return pattern; + } + private: friend class TestPathMatch; class PathIterator; @@ -142,16 +181,16 @@ class CPPCHECKLIB PathMatch { **/ class PathMatch::PathIterator { public: - /* Create from a pattern and base path, patterns must begin with '.' to be considered relative */ - static PathIterator from_pattern(const std::string &pattern, const std::string &basepath, bool icase) + /* Create from a pattern and base path */ + static PathIterator fromPattern(const std::string &pattern, const std::string &basepath, bool icase) { - if (!pattern.empty() && pattern[0] == '.') + if (isRelativePattern(pattern)) return PathIterator(basepath.c_str(), pattern.c_str(), icase); return PathIterator(pattern.c_str(), nullptr, icase); } /* Create from path and base path */ - static PathIterator from_path(const std::string &path, const std::string &basepath, bool icase) + static PathIterator fromPath(const std::string &path, const std::string &basepath, bool icase) { if (Path::isAbsolute(path)) return PathIterator(path.c_str(), nullptr, icase); @@ -270,7 +309,7 @@ class PathMatch::PathIterator { nextc(); c = current(); if (c == '/') { - /* Skip '/../' */ + /* Skip 'dir/../' */ nextc(); skips(false); while (mPos.l != 0 && current() != '/') From 648c23a3c8b6712d5415c83ab38bc64e892e74ae Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 15 Jul 2025 12:38:38 +0200 Subject: [PATCH 28/35] Cleanup, fixes, more tests --- cli/cmdlineparser.cpp | 8 ++- cli/filelister.cpp | 8 +-- lib/path.cpp | 11 +-- lib/pathmatch.cpp | 12 ++-- lib/pathmatch.h | 155 +++++++++++++++++++++++++---------------- test/cli/proj2_test.py | 4 +- test/testpathmatch.cpp | 45 ++++++++---- 7 files changed, 146 insertions(+), 97 deletions(-) diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 3abdb683154..55b91dbd7cc 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -1613,6 +1613,12 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a return Result::Fail; } + for (auto& path : mIgnoredPaths) + { + path = Path::removeQuotationMarks(std::move(path)); + path = Path::fromNativeSeparators(std::move(path)); + } + if (!project.guiProject.pathNames.empty()) mPathNames = project.guiProject.pathNames; @@ -2138,7 +2144,7 @@ std::list CmdLineParser::filterFiles(const std::vector files; PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); std::copy_if(filesResolved.cbegin(), filesResolved.cend(), std::inserter(files, files.end()), [&](const FileWithDetails& entry) { - return filtermatcher.match(entry.path()) || filtermatcher.match(entry.spath()); + return filtermatcher.match(entry.path()); }); return files; } diff --git a/cli/filelister.cpp b/cli/filelister.cpp index f2975d104ad..4749cc80b87 100644 --- a/cli/filelister.cpp +++ b/cli/filelister.cpp @@ -129,9 +129,7 @@ static std::string addFiles2(std::list&files, const std::string } else { // Directory if (recursive) { - // append a slash if it is a directory since that is what we are doing for mIgnoredPaths directory entries. - // otherwise we would ignore all its contents individually instead as a whole. - if (!ignored.match(fname + '/')) { + if (!ignored.match(fname)) { std::list filesSorted; std::string err = addFiles2(filesSorted, fname, extra, recursive, ignored); @@ -243,9 +241,7 @@ static std::string addFiles2(std::list &files, #endif if (path_is_directory) { if (recursive) { - // append a slash if it is a directory since that is what we are doing for mIgnoredPaths directory entries. - // otherwise we would ignore all its contents individually instead as a whole. - if (!ignored.match(new_path + '/')) { + if (!ignored.match(new_path)) { std::string err = addFiles2(files, new_path, extra, recursive, ignored, debug); if (!err.empty()) { return err; diff --git a/lib/path.cpp b/lib/path.cpp index 0fe6d54d5cc..47a1808302c 100644 --- a/lib/path.cpp +++ b/lib/path.cpp @@ -173,24 +173,19 @@ std::string Path::getCurrentExecutablePath(const char* fallback) return success ? std::string(buf) : std::string(fallback); } -static bool issep(char c) -{ - return c == '/' || c == '\\'; -} - bool Path::isAbsolute(const std::string& path) { #ifdef _WIN32 if (path.length() < 2) return false; - if (issep(path[0]) && issep(path[1])) + if ((path[0] == '\\' || path[0] == '/') && (path[1] == '\\' || path[1] == '/')) return true; // On Windows, 'C:\foo\bar' is an absolute path, while 'C:foo\bar' is not - return std::isalpha(path[0]) && path[1] == ':' && issep(path[2]); + return std::isalpha(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/'); #else - return !path.empty() && issep(path[0]); + return !path.empty() && path[0] == '/'; #endif } diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 092146357c3..9dd4ce2b120 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -26,18 +26,18 @@ #include -PathMatch::PathMatch(std::vector patterns, std::string basepath, Mode mode) : - mPatterns(std::move(patterns)), mBasepath(std::move(basepath)), mMode(mode) +PathMatch::PathMatch(std::vector patterns, std::string basepath, Syntax syntax) : + mPatterns(std::move(patterns)), mBasepath(std::move(basepath)), mSyntax(syntax) {} bool PathMatch::match(const std::string &path) const { return std::any_of(mPatterns.cbegin(), mPatterns.cend(), [=] (const std::string &pattern) { - return match(pattern, path, mBasepath, mMode); + return match(pattern, path, mBasepath, mSyntax); }); } -bool PathMatch::match(const std::string &pattern, const std::string &path, const std::string &basepath, Mode mode) +bool PathMatch::match(const std::string &pattern, const std::string &path, const std::string &basepath, Syntax syntax) { if (pattern.empty()) return false; @@ -50,9 +50,9 @@ bool PathMatch::match(const std::string &pattern, const std::string &path, const bool real = Path::isAbsolute(pattern) || isRelativePattern(pattern); /* Pattern iterator */ - PathIterator s = PathIterator::fromPattern(pattern, basepath, mode == Mode::icase); + PathIterator s = PathIterator::fromPattern(pattern, basepath, syntax); /* Path iterator */ - PathIterator t = PathIterator::fromPath(path, basepath, mode == Mode::icase); + PathIterator t = PathIterator::fromPath(path, basepath, syntax); /* Pattern restart position */ PathIterator p = s; /* Path restart position */ diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 7934d7b5ffd..17acc2cd239 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -33,53 +33,58 @@ /// @{ /** - * Path matching rules: - * - All patterns are canonicalized (path separators vary by platform): - * - '/./' => '/' - * - '/dir/../' => '/' - * - '//' => '/' - * - Trailing slashes are removed (root slash is preserved) - * - Patterns can contain globs: - * - '**' matches any number of characters including path separators. - * - '*' matches any number of characters except path separators. - * - '?' matches any single character except path separators. - * - If a pattern looks like an absolute path (e.g. starts with '/', but varies by platform): - * - Match all files where the pattern matches the start of the file's canonical absolute path up until a path - * separator or the end of the pathname. - * - If a pattern looks like a relative path, i.e. is '.' or '..', or - * starts with '.' or '..' followed by a path separator: - * - The pattern is interpreted as a path relative to `basepath` and then converted to an absolute path and - * treated as such according to the above procedure. If the pattern is relative to some other directory, it should - * be modified to be relative to `basepath` first (this should be done with patterns in project files, for example). - * - Otherwise: - * - Match all files where the pattern matches any part of the file's canonical absolute path up until a - * path separator or the end of the pathname, and the matching part directly follows a path separator. + * Path matching rules: + * - All patterns are canonicalized (path separators vary by platform): + * - '/./' => '/' + * - '/dir/../' => '/' + * - '//' => '/' + * - Trailing slashes are removed (root slash is preserved) + * - Patterns can contain globs: + * - '**' matches any number of characters including path separators. + * - '*' matches any number of characters except path separators. + * - '?' matches any single character except path separators. + * - If a pattern looks like an absolute path (e.g. starts with '/', but varies by platform): + * - Match all files where the pattern matches the start of the file's canonical absolute path up until a path + * separator or the end of the pathname. + * - If a pattern looks like a relative path, i.e. is '.' or '..', or + * starts with '.' or '..' followed by a path separator: + * - The pattern is interpreted as a path relative to `basepath` and then converted to an absolute path and + * treated as such according to the above procedure. If the pattern is relative to some other directory, it should + * be modified to be relative to `basepath` first (this should be done with patterns in project files, for example). + * - Otherwise: + * - Match all files where the pattern matches any part of the file's canonical absolute path up until a + * path separator or the end of the pathname, and the matching part directly follows a path separator. + * + * TODO: Handle less common windows windows syntaxes: + * - Drive-specific relative path: C:dir\foo.cpp + * - Root-relative path: \dir\foo.cpp **/ /** - * @brief Simple path matching for ignoring paths in CLI. + * @brief Syntactic path matching for ignoring paths in CLI. */ class CPPCHECKLIB PathMatch { public: /** - * @brief Match mode. + * @brief Path syntax. + * + * windows: Case insensitive, forward and backward slashes, UNC or drive letter root. + * unix: Case sensitive, forward slashes, slash root. * - * scase: Case sensitive. - * icase: Case insensitive. */ - enum class Mode : std::uint8_t { - scase, - icase, + enum class Syntax : std::uint8_t { + windows, + unix, }; /** - * @brief The default mode for the current platform. + * @brief The default syntax for the current platform. */ #ifdef _WIN32 - static constexpr Mode platform_mode = Mode::icase; + static constexpr Syntax platform_syntax = Syntax::windows; #else - static constexpr Mode platform_mode = Mode::scase; + static constexpr Syntax platform_syntax = Syntax::unix; #endif /** @@ -87,9 +92,9 @@ class CPPCHECKLIB PathMatch { * * @param patterns List of patterns. * @param basepath Path to which patterns and matched paths are relative, when applicable. - * @param mode Case sensitivity mode. + * @param syntax Path syntax. */ - explicit PathMatch(std::vector patterns = {}, std::string basepath = std::string(), Mode mode = platform_mode); + explicit PathMatch(std::vector patterns = {}, std::string basepath = std::string(), Syntax syntax = platform_syntax); /** * @brief Match path against list of patterns. @@ -105,10 +110,10 @@ class CPPCHECKLIB PathMatch { * @param pattern Pattern to use. * @param path Path to match. * @param basepath Path to which the pattern and path is relative, when applicable. - * @param mode Case sensitivity mode. + * @param syntax Path syntax. * @return true if the pattern matches the path, false otherwise. */ - static bool match(const std::string &pattern, const std::string &path, const std::string &basepath = std::string(), Mode mode = platform_mode); + static bool match(const std::string &pattern, const std::string &path, const std::string &basepath = std::string(), Syntax syntax = platform_syntax); /** * @brief Check if a pattern is a relative path name. @@ -152,9 +157,12 @@ class CPPCHECKLIB PathMatch { friend class TestPathMatch; class PathIterator; + /* List of patterns */ std::vector mPatterns; + /* Base path to with patterns and paths are relative */ std::string mBasepath; - Mode mMode; + /* The syntax to use */ + Syntax mSyntax; }; /** @@ -167,7 +175,7 @@ class CPPCHECKLIB PathMatch { * Both strings are optional. If both strings are present, then they're concatenated with a slash * (subject to canonicalization). * - * Double-dots at the root level are removed. The root slash is preserved, other trailing slashes are removed. + * Double-dots at the root level are removed. Trailing slashes are removed, the root is preserved. * * Doing the iteration in reverse allows canonicalization to be performed without lookahead. This is useful * for comparing path strings, potentially relative to different base paths, without having to do prior string @@ -182,40 +190,65 @@ class CPPCHECKLIB PathMatch { class PathMatch::PathIterator { public: /* Create from a pattern and base path */ - static PathIterator fromPattern(const std::string &pattern, const std::string &basepath, bool icase) + static PathIterator fromPattern(const std::string &pattern, const std::string &basepath, Syntax syntax) { if (isRelativePattern(pattern)) - return PathIterator(basepath.c_str(), pattern.c_str(), icase); - return PathIterator(pattern.c_str(), nullptr, icase); + return PathIterator(basepath.c_str(), pattern.c_str(), syntax); + return PathIterator(pattern.c_str(), nullptr, syntax); } /* Create from path and base path */ - static PathIterator fromPath(const std::string &path, const std::string &basepath, bool icase) + static PathIterator fromPath(const std::string &path, const std::string &basepath, Syntax syntax) { if (Path::isAbsolute(path)) - return PathIterator(path.c_str(), nullptr, icase); - return PathIterator(basepath.c_str(), path.c_str(), icase); + return PathIterator(path.c_str(), nullptr, syntax); + return PathIterator(basepath.c_str(), path.c_str(), syntax); } /* Constructor */ - explicit PathIterator(const char *path_a = nullptr, const char *path_b = nullptr, bool lower = false) : - mStart{path_a, path_b}, mLower(lower) + explicit PathIterator(const char *path_a = nullptr, const char *path_b = nullptr, Syntax syntax = platform_syntax) : + mStart{path_a, path_b}, mSyntax(syntax) { + const auto issep = [syntax] (char c) { return c == '/' || (syntax == Syntax::windows && c == '\\'); }; + const auto isdrive = [] (char c) { return (c >= 'A' && c <= 'Z' ) || (c >= 'a' && c <= 'z'); }; + for (int i = 0; i < 2; i++) { - mEnd[i] = mStart[i]; + const char *&p = mEnd[i]; + p = mStart[i]; - if (mStart[i] == nullptr || *mStart[i] == '\0') + if (p == nullptr || *p == '\0') continue; - if (mPos.l != 0) + if (mPos.l == 0) { + /* Check length of root component */ + if (issep(p[0])) { + mPos.l++; + if (syntax == Syntax::windows && issep(p[1])) { + mPos.l++; + if (p[2] == '.' || p[2] == '?') { + mPos.l++; + if (issep(p[3])) + mPos.l++; + } + } + } else if (syntax == Syntax::windows && isdrive(p[0]) && p[1] == ':') { + mPos.l += 2; + if (issep(p[2])) + mPos.l++; + } + p += mPos.l; + mRootLength = mPos.l; + } else { + /* Add path separator */ mPos.l++; + } - while (*mEnd[i] != '\0') { - mEnd[i]++; + while (*p != '\0') { + p++; mPos.l++; } - mPos.p = mEnd[i]; + mPos.p = p; } if (mPos.l == 0) @@ -280,11 +313,11 @@ class PathMatch::PathIterator { char c = mPos.p[-1]; - if (c == '\\') - return '/'; - - if (mLower) + if (mSyntax == Syntax::windows) { + if (c == '\\') + return '/'; return std::tolower(c); + } return c; } @@ -292,7 +325,7 @@ class PathMatch::PathIterator { /* Do canonicalization on a path component boundary */ void skips(bool leadsep) { - while (mPos.l != 0) { + while (mPos.l > mRootLength) { Pos pos = mPos; if (leadsep) { @@ -312,7 +345,7 @@ class PathMatch::PathIterator { /* Skip 'dir/../' */ nextc(); skips(false); - while (mPos.l != 0 && current() != '/') + while (mPos.l > mRootLength && current() != '/') nextc(); continue; } @@ -323,7 +356,7 @@ class PathMatch::PathIterator { /* Skip leading './' */ break; } - } else if (c == '/' && mPos.l != 1) { + } else if (c == '/') { /* Skip double separator (keep root) */ nextc(); leadsep = false; @@ -371,8 +404,10 @@ class PathMatch::PathIterator { const char *mEnd[2] {}; /* Current position */ Pos mPos {}; - /* Lowercase conversion flag */ - bool mLower; + /* Length of the root component */ + std::size_t mRootLength {}; + /* Syntax */ + Syntax mSyntax; }; /// @} diff --git a/test/cli/proj2_test.py b/test/cli/proj2_test.py index 1059c475198..c9516d9ddbf 100644 --- a/test/cli/proj2_test.py +++ b/test/cli/proj2_test.py @@ -100,7 +100,7 @@ def test_gui_project_loads_compile_commands_2(tmp_path): proj_dir = tmp_path / 'proj2' shutil.copytree(__proj_dir, proj_dir) __create_compile_commands(proj_dir) - exclude_path_1 = 'proj2/b/' + exclude_path_1 = 'proj2/b' create_gui_project_file(os.path.join(proj_dir, 'test.cppcheck'), import_project='compile_commands.json', exclude_paths=[exclude_path_1]) @@ -157,7 +157,7 @@ def test_gui_project_loads_relative_vs_solution_2(tmp_path): def test_gui_project_loads_relative_vs_solution_with_exclude(tmp_path): proj_dir = tmp_path / 'proj2' shutil.copytree(__proj_dir, proj_dir) - create_gui_project_file(os.path.join(tmp_path, 'test.cppcheck'), root_path='proj2', import_project='proj2/proj2.sln', exclude_paths=['b/']) + create_gui_project_file(os.path.join(tmp_path, 'test.cppcheck'), root_path='proj2', import_project='proj2/proj2.sln', exclude_paths=['b']) ret, stdout, stderr = cppcheck(['--project=test.cppcheck'], cwd=tmp_path) assert ret == 0, stdout assert stderr == __ERR_A diff --git a/test/testpathmatch.cpp b/test/testpathmatch.cpp index ca35ec6780d..407fab95359 100644 --- a/test/testpathmatch.cpp +++ b/test/testpathmatch.cpp @@ -28,6 +28,8 @@ class TestPathMatch : public TestFixture { TestPathMatch() : TestFixture("TestPathMatch") {} private: + static constexpr auto unix = PathMatch::Syntax::unix; + static constexpr auto windows = PathMatch::Syntax::windows; #ifdef _WIN32 const std::string basepath{"C:\\test"}; #else @@ -105,12 +107,12 @@ class TestPathMatch : public TestFixture { } void onemasksamepathdifferentslash() const { - PathMatch srcMatcher2({"src\\"}, basepath); + PathMatch srcMatcher2({"src\\"}, basepath, windows); ASSERT(srcMatcher2.match("src/")); } void onemasksamepathdifferentcase() const { - PathMatch match({"sRc/"}, basepath, PathMatch::Mode::icase); + PathMatch match({"sRc/"}, basepath, windows); ASSERT(match.match("srC/")); } @@ -186,7 +188,7 @@ class TestPathMatch : public TestFixture { } void filemaskdifferentcase() const { - PathMatch match({"foo.cPp"}, basepath, PathMatch::Mode::icase); + PathMatch match({"foo.cPp"}, basepath, windows); ASSERT(match.match("fOo.cpp")); } @@ -252,18 +254,33 @@ class TestPathMatch : public TestFixture { } void pathiterator() const { + /* See https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats + * for information on Windows path syntax. */ using PathIterator = PathMatch::PathIterator; - ASSERT_EQUALS("/hello/world", PathIterator("/hello/universe/.", "../world//").read()); - ASSERT_EQUALS("/", PathIterator("//./..//.///.", "../../..///").read()); - ASSERT_EQUALS("/foo/bar", PathIterator(nullptr, "/foo/bar/.").read()); - ASSERT_EQUALS("/foo/bar", PathIterator("/foo/bar/.", nullptr).read()); - ASSERT_EQUALS("/foo/bar", PathIterator("/foo", "bar").read()); - ASSERT_EQUALS("", PathIterator("", "").read()); - ASSERT_EQUALS("", PathIterator("", nullptr).read()); - ASSERT_EQUALS("", PathIterator(nullptr, "").read()); - ASSERT_EQUALS("", PathIterator(nullptr, nullptr).read()); - ASSERT_EQUALS("c:/windows/system32", PathIterator("C:", "Windows\\System32\\Drivers\\..\\.", true).read()); - ASSERT_EQUALS("C:", PathIterator("C:\\Program Files\\", "..").read()); + ASSERT_EQUALS("/", PathIterator("/", nullptr, unix).read()); + ASSERT_EQUALS("/", PathIterator("//", nullptr, unix).read()); + ASSERT_EQUALS("/", PathIterator("/", "/", unix).read()); + ASSERT_EQUALS("/hello/world", PathIterator("/hello/universe/.", "../world//", unix).read()); + ASSERT_EQUALS("/", PathIterator("//./..//.///.", "../../..///", unix).read()); + ASSERT_EQUALS("/foo/bar", PathIterator(nullptr, "/foo/bar/.", unix).read()); + ASSERT_EQUALS("/foo/bar", PathIterator("/foo/bar/.", nullptr, unix).read()); + ASSERT_EQUALS("/foo/bar", PathIterator("/foo", "bar", unix).read()); + ASSERT_EQUALS("", PathIterator("", "", unix).read()); + ASSERT_EQUALS("", PathIterator("", nullptr, unix).read()); + ASSERT_EQUALS("", PathIterator(nullptr, "", unix).read()); + ASSERT_EQUALS("", PathIterator(nullptr, nullptr, unix).read()); + ASSERT_EQUALS("c:", PathIterator("C:", nullptr, windows).read()); + /* C: without slash is a bit ambigous. It should probably not be considered a root because it's + * not fully qualified (it designates the current directory on the C drive), + * so this test could be considered to be unspecified behavior. */ + ASSERT_EQUALS("c:", PathIterator("C:", "../..", windows).read()); + ASSERT_EQUALS("c:/windows/system32", PathIterator("C:", "Windows\\System32\\Drivers\\..\\.", windows).read()); + ASSERT_EQUALS("c:/", PathIterator("C:\\Program Files\\", "..", windows).read()); + ASSERT_EQUALS("//./", PathIterator("\\\\.\\C:\\", "../..", windows).read()); + ASSERT_EQUALS("//./", PathIterator("\\\\.\\", "..\\..", windows).read()); + ASSERT_EQUALS("//?/", PathIterator("\\\\?\\", "..\\..", windows).read()); + /* The server and share should actually be considered part of the root and not be removed */ + ASSERT_EQUALS("//", PathIterator("\\\\Server\\Share\\Directory", "../..\\../..", windows).read()); } }; From 93479303dc4a822329d462808b847b2d4a73df60 Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 15 Jul 2025 12:42:48 +0200 Subject: [PATCH 29/35] Run format --- lib/pathmatch.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pathmatch.h b/lib/pathmatch.h index 17acc2cd239..fb255a04dd4 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -209,8 +209,12 @@ class PathMatch::PathIterator { explicit PathIterator(const char *path_a = nullptr, const char *path_b = nullptr, Syntax syntax = platform_syntax) : mStart{path_a, path_b}, mSyntax(syntax) { - const auto issep = [syntax] (char c) { return c == '/' || (syntax == Syntax::windows && c == '\\'); }; - const auto isdrive = [] (char c) { return (c >= 'A' && c <= 'Z' ) || (c >= 'a' && c <= 'z'); }; + const auto issep = [syntax] (char c) { + return c == '/' || (syntax == Syntax::windows && c == '\\'); + }; + const auto isdrive = [] (char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + }; for (int i = 0; i < 2; i++) { const char *&p = mEnd[i]; From 2f8791339f9a368adcc23435977865be0d70866b Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 15 Jul 2025 13:02:14 +0200 Subject: [PATCH 30/35] Revert CmdlineParser test cases --- test/testcmdlineparser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/testcmdlineparser.cpp b/test/testcmdlineparser.cpp index 71b33b93104..c0c0dde9786 100644 --- a/test/testcmdlineparser.cpp +++ b/test/testcmdlineparser.cpp @@ -3345,7 +3345,7 @@ class TestCmdlineParser : public TestFixture { const char * const argv[] = {"cppcheck", "-isrc\\file.cpp", "src/file.cpp"}; ASSERT(!fillSettingsFromArgs(argv)); ASSERT_EQUALS(1, parser->getIgnoredPaths().size()); - ASSERT_EQUALS("src\\file.cpp", parser->getIgnoredPaths()[0]); + ASSERT_EQUALS("src/file.cpp", parser->getIgnoredPaths()[0]); ASSERT_EQUALS(1, parser->getPathNames().size()); ASSERT_EQUALS("src/file.cpp", parser->getPathNames()[0]); ASSERT_EQUALS("cppcheck: error: could not find or open any of the paths given.\ncppcheck: Maybe all paths were ignored?\n", logger->str()); @@ -3367,7 +3367,7 @@ class TestCmdlineParser : public TestFixture { const char * const argv[] = {"cppcheck", "-isrc\\", "src\\file.cpp"}; ASSERT(!fillSettingsFromArgs(argv)); ASSERT_EQUALS(1, parser->getIgnoredPaths().size()); - ASSERT_EQUALS("src\\", parser->getIgnoredPaths()[0]); + ASSERT_EQUALS("src/", parser->getIgnoredPaths()[0]); ASSERT_EQUALS(1, parser->getPathNames().size()); ASSERT_EQUALS("src/file.cpp", parser->getPathNames()[0]); ASSERT_EQUALS("cppcheck: error: could not find or open any of the paths given.\ncppcheck: Maybe all paths were ignored?\n", logger->str()); From 2c311229446516f7118dc4d1d7eb0061f54921ab Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 15 Jul 2025 13:04:49 +0200 Subject: [PATCH 31/35] Fix for false positive on index range --- lib/pathmatch.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/pathmatch.h b/lib/pathmatch.h index fb255a04dd4..af96a75fad6 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -226,22 +226,22 @@ class PathMatch::PathIterator { if (mPos.l == 0) { /* Check length of root component */ if (issep(p[0])) { - mPos.l++; + mRootLength++; if (syntax == Syntax::windows && issep(p[1])) { - mPos.l++; + mRootLength++; if (p[2] == '.' || p[2] == '?') { - mPos.l++; + mRootLength++; if (issep(p[3])) - mPos.l++; + mRootLength++; } } } else if (syntax == Syntax::windows && isdrive(p[0]) && p[1] == ':') { - mPos.l += 2; + mRootLength += 2; if (issep(p[2])) - mPos.l++; + mRootLength++; } - p += mPos.l; - mRootLength = mPos.l; + p += mRootLength; + mPos.l = mRootLength; } else { /* Add path separator */ mPos.l++; @@ -252,7 +252,7 @@ class PathMatch::PathIterator { mPos.l++; } - mPos.p = p; + mPos.p = p - 1; } if (mPos.l == 0) @@ -315,7 +315,7 @@ class PathMatch::PathIterator { if (mPos.c != EOF) return mPos.c; - char c = mPos.p[-1]; + char c = *mPos.p; if (mSyntax == Syntax::windows) { if (c == '\\') @@ -394,11 +394,11 @@ class PathMatch::PathIterator { else if (mPos.c != EOF) { mPos.c = EOF; } else { - mPos.p--; if (mPos.p == mStart[1]) { mPos.p = mEnd[0]; mPos.c = '/'; } + mPos.p--; } } From 5426dd21be724611836417a8521d160efb772016 Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 15 Jul 2025 14:52:45 +0200 Subject: [PATCH 32/35] Update docs --- cli/cmdlineparser.cpp | 7 +++---- man/manual-premium.md | 12 ++++++++---- man/manual.md | 12 ++++++++---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 55b91dbd7cc..96eb3c9a08e 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -1774,10 +1774,9 @@ void CmdLineParser::printHelp() const " this is not needed.\n" " --include=\n" " Force inclusion of a file before the checked file.\n" - " -i Give a source file or source file directory to exclude\n" - " from the check. This applies only to source files so\n" - " header files included by source files are not matched.\n" - " Directory name is matched to all parts of the path.\n" + " -i Exclude source files or directories matching str from\n" + " the check. This applies only to source files so header\n" + " files included by source files are not matched.\n" " --inconclusive Allow that Cppcheck reports even though the analysis is\n" " inconclusive.\n" " There are false positives with this option. Each result\n" diff --git a/man/manual-premium.md b/man/manual-premium.md index 848de85f800..e9509fc7b45 100644 --- a/man/manual-premium.md +++ b/man/manual-premium.md @@ -121,13 +121,17 @@ check: All files under src/a and src/b are then checked. -The second option is to use -i, which specifies the files/paths to ignore. With this command no files in src/c are -checked: +The second option is to use -i, which specifies a pattern of path names to ignore. With this command no files in src/c +are checked: cppcheck -isrc/c src -This option is only valid when supplying an input directory. To ignore multiple directories supply the -i flag for each -directory individually. The following command ignores both the src/b and src/c directories: +The above pattern matches any path that has a component named src anywhere, which is directly followed by a component +named c. c can be a file or a directory, in which case all files below c are ignored. Patterns can also be absolute +paths, or relative to the current directory if the first path component is dot or dot-dot. The glob characters ?, \* +and \*\* are allowed. ? matches one character, \* and \*\* match any number of characters. \*\* matches path +separators, where as ? and \* does not. Multiple patterns can be used by supplying the -i flag multiple times. The +following command ignores everything in both the src/b and src/c directories: cppcheck -isrc/b -isrc/c diff --git a/man/manual.md b/man/manual.md index cdc692552d0..fdeafe966a6 100644 --- a/man/manual.md +++ b/man/manual.md @@ -122,13 +122,17 @@ check: All files under src/a and src/b are then checked. -The second option is to use -i, which specifies the files/paths to ignore. With this command no files in src/c are -checked: +The second option is to use -i, which specifies a pattern of path names to ignore. With this command no files in src/c +are checked: cppcheck -isrc/c src -This option is only valid when supplying an input directory. To ignore multiple directories supply the -i flag for each -directory individually. The following command ignores both the src/b and src/c directories: +The above pattern matches any path that has a component named src anywhere, which is directly followed by a component +named c. c can be a file or a directory, in which case all files below c are ignored. Patterns can also be absolute +paths, or relative to the current directory if the first path component is dot or dot-dot. The glob characters ?, \* +and \*\* are allowed. ? matches one character, \* and \*\* match any number of characters. \*\* matches path +separators, where as ? and \* does not. Multiple patterns can be used by supplying the -i flag multiple times. The +following command ignores everything in both the src/b and src/c directories: cppcheck -isrc/b -isrc/c From ae373f558a5a17b5dffb2db042a14b3bf83a2877 Mon Sep 17 00:00:00 2001 From: glank Date: Tue, 15 Jul 2025 15:49:32 +0200 Subject: [PATCH 33/35] Update release notes --- releasenotes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/releasenotes.txt b/releasenotes.txt index 4d3da24ef28..fbcea45df3e 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -29,4 +29,5 @@ Other: - Fixed --file-filter matching of looked up files in provided paths. - Split up cstyleCast checker; dangerous casts produce portability/warning reports, safe casts produce style reports. - Removed deprecated '--showtime=' value 'top5'. Please use 'top5_file' or 'top5_summary' instead. +- Updated path matching syntax for -i, --file-filter, suppressions, GUI excludes, and project file excludes. - From ec23f3041148aefb10cee9079827af46fc08f75e Mon Sep 17 00:00:00 2001 From: glank Date: Wed, 16 Jul 2025 09:52:26 +0200 Subject: [PATCH 34/35] Revert manual changes --- man/manual-premium.md | 12 ++++-------- man/manual.md | 12 ++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/man/manual-premium.md b/man/manual-premium.md index e9509fc7b45..848de85f800 100644 --- a/man/manual-premium.md +++ b/man/manual-premium.md @@ -121,17 +121,13 @@ check: All files under src/a and src/b are then checked. -The second option is to use -i, which specifies a pattern of path names to ignore. With this command no files in src/c -are checked: +The second option is to use -i, which specifies the files/paths to ignore. With this command no files in src/c are +checked: cppcheck -isrc/c src -The above pattern matches any path that has a component named src anywhere, which is directly followed by a component -named c. c can be a file or a directory, in which case all files below c are ignored. Patterns can also be absolute -paths, or relative to the current directory if the first path component is dot or dot-dot. The glob characters ?, \* -and \*\* are allowed. ? matches one character, \* and \*\* match any number of characters. \*\* matches path -separators, where as ? and \* does not. Multiple patterns can be used by supplying the -i flag multiple times. The -following command ignores everything in both the src/b and src/c directories: +This option is only valid when supplying an input directory. To ignore multiple directories supply the -i flag for each +directory individually. The following command ignores both the src/b and src/c directories: cppcheck -isrc/b -isrc/c diff --git a/man/manual.md b/man/manual.md index fdeafe966a6..cdc692552d0 100644 --- a/man/manual.md +++ b/man/manual.md @@ -122,17 +122,13 @@ check: All files under src/a and src/b are then checked. -The second option is to use -i, which specifies a pattern of path names to ignore. With this command no files in src/c -are checked: +The second option is to use -i, which specifies the files/paths to ignore. With this command no files in src/c are +checked: cppcheck -isrc/c src -The above pattern matches any path that has a component named src anywhere, which is directly followed by a component -named c. c can be a file or a directory, in which case all files below c are ignored. Patterns can also be absolute -paths, or relative to the current directory if the first path component is dot or dot-dot. The glob characters ?, \* -and \*\* are allowed. ? matches one character, \* and \*\* match any number of characters. \*\* matches path -separators, where as ? and \* does not. Multiple patterns can be used by supplying the -i flag multiple times. The -following command ignores everything in both the src/b and src/c directories: +This option is only valid when supplying an input directory. To ignore multiple directories supply the -i flag for each +directory individually. The following command ignores both the src/b and src/c directories: cppcheck -isrc/b -isrc/c From 44379ebe6f0a40fa2ac65a8c01fcd82467137337 Mon Sep 17 00:00:00 2001 From: glank Date: Wed, 16 Jul 2025 09:56:44 +0200 Subject: [PATCH 35/35] Update release notes --- releasenotes.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/releasenotes.txt b/releasenotes.txt index fbcea45df3e..5c4700b7b9f 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -10,6 +10,8 @@ GUI: - Changed interface: +- Updated path matching syntax for -i, --file-filter, suppressions, GUI excludes, and project file excludes. +Old patterns that use a `*` may need to use `**` instead if it is intended to match path separators. More details can be seen in the manual. - Deprecations: @@ -29,5 +31,4 @@ Other: - Fixed --file-filter matching of looked up files in provided paths. - Split up cstyleCast checker; dangerous casts produce portability/warning reports, safe casts produce style reports. - Removed deprecated '--showtime=' value 'top5'. Please use 'top5_file' or 'top5_summary' instead. -- Updated path matching syntax for -i, --file-filter, suppressions, GUI excludes, and project file excludes. -