diff --git a/Makefile b/Makefile index 188529d82a9..d71c614e903 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 @@ -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 @@ -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 diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index 2c879daa904..96eb3c9a08e 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, Path::getCurrentPath()); 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, 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()) { @@ -1622,19 +1616,7 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a 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 += '/'; - } + path = Path::fromNativeSeparators(std::move(path)); } if (!project.guiProject.pathNames.empty()) @@ -1792,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" @@ -2160,13 +2141,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, Path::getCurrentPath()); 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()); }); 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/gui/filelist.cpp b/gui/filelist.cpp index 3115967c451..f470fe56727 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), QDir::currentPath().toStdString()); QStringList paths; for (const QFileInfo& item : mFileList) { diff --git a/lib/importproject.cpp b/lib/importproject.cpp index d37ad71c2c9..a7b434a84c6 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, Path::getCurrentPath()); 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); @@ -858,8 +840,9 @@ bool ImportProject::importVcxproj(const std::string &filename, const tinyxml2::X } // Project files + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); for (const std::string &cfilename : compileList) { - if (!fileFilters.empty() && !matchglobs(fileFilters, cfilename)) + if (!fileFilters.empty() && !filtermatcher.match(cfilename)) continue; for (const ProjectConfiguration &p : projectConfigurationList) { @@ -937,6 +920,8 @@ ImportProject::SharedItemsProject ImportProject::importVcxitems(const std::strin SharedItemsProject result; result.pathToProjectFile = filename; + PathMatch filtermatcher(fileFilters, Path::getCurrentPath()); + tinyxml2::XMLDocument doc; const tinyxml2::XMLError error = doc.LoadFile(filename.c_str()); if (error != tinyxml2::XML_SUCCESS) { @@ -957,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); @@ -1269,7 +1254,20 @@ 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; +} + +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) + ret.emplace_back(PathMatch::joinRelativePattern(path, attr)); } return ret; } @@ -1339,13 +1337,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/path.cpp b/lib/path.cpp index eba26230022..47a1808302c 100644 --- a/lib/path.cpp +++ b/lib/path.cpp @@ -175,16 +175,17 @@ std::string Path::getCurrentExecutablePath(const char* fallback) bool Path::isAbsolute(const std::string& path) { - const std::string& nativePath = toNativeSeparators(path); - #ifdef _WIN32 if (path.length() < 2) return false; + 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 startsWith(nativePath, "\\\\") || (std::isalpha(nativePath[0]) != 0 && nativePath.compare(1, 2, ":\\") == 0); + return std::isalpha(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/'); #else - return !nativePath.empty() && nativePath[0] == '/'; + return !path.empty() && path[0] == '/'; #endif } diff --git a/lib/pathmatch.cpp b/lib/pathmatch.cpp index 638c2bce005..9dd4ce2b120 100644 --- a/lib/pathmatch.cpp +++ b/lib/pathmatch.cpp @@ -19,74 +19,119 @@ #include "pathmatch.h" #include "path.h" -#include "utils.h" -#include -#include +#include +#include +#include +#include -PathMatch::PathMatch(std::vector paths, bool caseSensitive) - : mPaths(std::move(paths)), mCaseSensitive(caseSensitive) + +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 { - for (std::string& p : mPaths) - { - p = Path::fromNativeSeparators(p); - if (!mCaseSensitive) - strTolower(p); - } - // TODO: also make lowercase? - mWorkingDirectory.push_back(Path::fromNativeSeparators(Path::getCurrentPath())); + return std::any_of(mPatterns.cbegin(), mPatterns.cend(), [=] (const std::string &pattern) { + return match(pattern, path, mBasepath, mSyntax); + }); } -bool PathMatch::match(const std::string &path) const +bool PathMatch::match(const std::string &pattern, const std::string &path, const std::string &basepath, Syntax syntax) { - if (path.empty()) + if (pattern.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; + 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) || isRelativePattern(pattern); - const bool is_absolute = Path::isAbsolute(path); + /* Pattern iterator */ + PathIterator s = PathIterator::fromPattern(pattern, basepath, syntax); + /* Path iterator */ + PathIterator t = PathIterator::fromPath(path, basepath, syntax); + /* Pattern restart position */ + PathIterator p = s; + /* Path restart position */ + PathIterator q = t; - // 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); + /* Backtrack stack */ + std::stack> b; - // Filtering directory name - if (endsWith(pathToMatch,'/')) { - if (pathToMatch.length() > finddir.length()) + 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) { + /* 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; + ++t; 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) + } + 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; } - // Filtering filename - else { - if (pathToMatch.length() > findpath.length()) + /* Literal character */ + default: { + if (*s == *t) { + ++s; + ++t; 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; + } + break; + } } - } - return false; -} -std::string PathMatch::removeFilename(const std::string &path) -{ - const std::size_t ind = path.find_last_of('/'); - return path.substr(0, ind + 1); + /* No match, try to backtrack */ + if (!b.empty()) { + const auto &bp = b.top(); + b.pop(); + s.setpos(bp.first); + t.setpos(bp.second); + continue; + } + + /* Couldn't backtrack, try matching from the next path separator */ + while (*q != '\0' && *q != '/') + ++q; + + if (*q == '/') { + ++q; + s = p; + t = q; + continue; + } + + /* No more path seperators to try from */ + return false; + } } diff --git a/lib/pathmatch.h b/lib/pathmatch.h index f0ace4fbc94..af96a75fad6 100644 --- a/lib/pathmatch.h +++ b/lib/pathmatch.h @@ -21,52 +21,397 @@ #include "config.h" +#include +#include +#include #include #include +#include "path.h" + /// @addtogroup CLI /// @{ /** - * @brief Simple path matching for ignoring paths in CLI. + * 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 Syntactic path matching for ignoring paths in CLI. */ class CPPCHECKLIB PathMatch { public: /** - * The constructor. + * @brief Path syntax. * - * If a path is a directory it needs to end with a file separator. + * windows: Case insensitive, forward and backward slashes, UNC or drive letter root. + * unix: Case sensitive, forward slashes, slash root. * - * @param paths List of masks. - * @param caseSensitive Match the case of the characters when - * matching paths? */ - explicit PathMatch(std::vector paths, bool caseSensitive = true); + enum class Syntax : std::uint8_t { + windows, + unix, + }; /** - * @brief Match path against list of masks. + * @brief The default syntax for the current platform. + */ +#ifdef _WIN32 + static constexpr Syntax platform_syntax = Syntax::windows; +#else + static constexpr Syntax platform_syntax = Syntax::unix; +#endif + + /** + * The constructor. * - * If you want to match a directory the given path needs to end with a path separator. + * @param patterns List of patterns. + * @param basepath Path to which patterns and matched paths are relative, when applicable. + * @param syntax Path syntax. + */ + explicit PathMatch(std::vector patterns = {}, std::string basepath = std::string(), Syntax syntax = platform_syntax); + + /** + * @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; -protected: + /** + * @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 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(), Syntax syntax = platform_syntax); + + /** + * @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 Remove filename part from the path. - * @param path Path to edit. - * @return path without filename part. + * @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 removeFilename(const std::string &path); + 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; + + /* List of patterns */ + std::vector mPatterns; + /* Base path to with patterns and paths are relative */ + std::string mBasepath; + /* The syntax to use */ + Syntax mSyntax; +}; + +/** + * 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. 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 + * 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 fromPattern(const std::string &pattern, const std::string &basepath, Syntax syntax) + { + if (isRelativePattern(pattern)) + 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, Syntax syntax) + { + if (Path::isAbsolute(path)) + 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, 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++) { + const char *&p = mEnd[i]; + p = mStart[i]; + + if (p == nullptr || *p == '\0') + continue; + + if (mPos.l == 0) { + /* Check length of root component */ + if (issep(p[0])) { + mRootLength++; + if (syntax == Syntax::windows && issep(p[1])) { + mRootLength++; + if (p[2] == '.' || p[2] == '?') { + mRootLength++; + if (issep(p[3])) + mRootLength++; + } + } + } else if (syntax == Syntax::windows && isdrive(p[0]) && p[1] == ':') { + mRootLength += 2; + if (issep(p[2])) + mRootLength++; + } + p += mRootLength; + mPos.l = mRootLength; + } else { + /* Add path separator */ + mPos.l++; + } + + while (*p != '\0') { + p++; + mPos.l++; + } + + mPos.p = p - 1; + } + + if (mPos.l == 0) + mPos.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 mPos; + } + + /* Restore a saved position */ + void setpos(const Pos &pos) + { + mPos = 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 str; + + while (current() != '\0') { + str.insert(0, 1, current()); + advance(); + } + + return str; + } private: - std::vector mPaths; - bool mCaseSensitive; - std::vector mWorkingDirectory; + /* Read the current character */ + char current() const + { + if (mPos.c != EOF) + return mPos.c; + + char c = *mPos.p; + + if (mSyntax == Syntax::windows) { + if (c == '\\') + return '/'; + return std::tolower(c); + } + + return c; + } + + /* Do canonicalization on a path component boundary */ + void skips(bool leadsep) + { + while (mPos.l > mRootLength) { + Pos pos = mPos; + + if (leadsep) { + if (current() != '/') + break; + nextc(); + } + + char c = current(); + if (c == '.') { + nextc(); + c = current(); + if (c == '.') { + nextc(); + c = current(); + if (c == '/') { + /* Skip 'dir/../' */ + nextc(); + skips(false); + while (mPos.l > mRootLength && current() != '/') + nextc(); + continue; + } + } else if (c == '/') { + /* Skip '/./' */ + continue; + } else if (c == '\0') { + /* Skip leading './' */ + break; + } + } else if (c == '/') { + /* Skip double separator (keep root) */ + nextc(); + leadsep = false; + continue; + } + + mPos = pos; + 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 (mPos.l == 0) + return; + + mPos.l--; + + if (mPos.l == 0) + mPos.c = '\0'; + else if (mPos.c != EOF) { + mPos.c = EOF; + } else { + if (mPos.p == mStart[1]) { + mPos.p = mEnd[0]; + mPos.c = '/'; + } + mPos.p--; + } + } + + /* String start pointers */ + const char *mStart[2] {}; + /* String end pointers */ + const char *mEnd[2] {}; + /* Current position */ + Pos mPos {}; + /* Length of the root component */ + std::size_t mRootLength {}; + /* Syntax */ + Syntax mSyntax; }; /// @} diff --git a/lib/suppressions.cpp b/lib/suppressions.cpp index 6255927cf75..bf97bdb2b81 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,12 +397,12 @@ SuppressionList::Suppression::Result SuppressionList::Suppression::isSuppressed( if (!errorId.empty() && !matchglob(errorId, errmsg.errorId)) return Result::Checked; } else { - if (!fileName.empty() && !matchglob(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 diff --git a/lib/utils.cpp b/lib/utils.cpp index 11661556968..73e9de58223 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; } @@ -117,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> diff --git a/oss-fuzz/Makefile b/oss-fuzz/Makefile index 88449d09051..4d218a111a7 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 @@ -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 diff --git a/releasenotes.txt b/releasenotes.txt index 4d3da24ef28..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: diff --git a/test/cli/more-projects_test.py b/test/cli/more-projects_test.py index 57f400fa197..c728966ef7b 100644 --- a/test/cli/more-projects_test.py +++ b/test/cli/more-projects_test.py @@ -372,6 +372,137 @@ 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), + 'Checking {} ...'.format(os.path.join("..", "cwd", "b", "b-rel.c")), + ] + + args = ['--file-filter={}/*/?/**.c*'.format(test_root), '--project=../test.cppcheck'] + 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): test_file = os.path.join(tmpdir, 'test.cpp') with open(test_file, 'wt') as f: @@ -705,7 +836,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..ecfca256ec6 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*\\']) @@ -2498,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/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 048c2e4c17a..13057735513 100644 --- a/test/testfilelister.cpp +++ b/test/testfilelister.cpp @@ -27,8 +27,6 @@ #include #include #include -#include -#include class TestFileLister : public TestFixture { public: @@ -62,9 +60,7 @@ class TestFileLister : public TestFixture { // Recursively add add files.. std::list files; - std::vector masks; - PathMatch matcher(std::move(masks)); - std::string err = FileLister::recursiveAddFiles(files, adddir, {}, matcher); + std::string err = FileLister::recursiveAddFiles(files, adddir, {}, PathMatch()); ASSERT_EQUALS("", err); ASSERT(!files.empty()); @@ -110,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); } @@ -118,8 +114,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,9 +124,7 @@ class TestFileLister : public TestFixture { const std::string basedir = findBaseDir(); std::list files; - std::vector ignored; - PathMatch matcher(ignored); - 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()); @@ -141,8 +134,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/"}); std::string err = FileLister::recursiveAddFiles(files, basedir, {}, matcher); ASSERT_EQUALS("", err); ASSERT(!files.empty()); @@ -165,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/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()); diff --git a/test/testpathmatch.cpp b/test/testpathmatch.cpp index 45fbde54d58..407fab95359 100644 --- a/test/testpathmatch.cpp +++ b/test/testpathmatch.cpp @@ -20,7 +20,6 @@ #include "fixture.h" #include -#include #include @@ -29,10 +28,17 @@ 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")}; + static constexpr auto unix = PathMatch::Syntax::unix; + static constexpr auto windows = PathMatch::Syntax::windows; +#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); @@ -67,6 +73,10 @@ class TestPathMatch : public TestFixture { TEST_CASE(filemaskpath3); TEST_CASE(filemaskpath4); TEST_CASE(mixedallmatch); + TEST_CASE(glob); + TEST_CASE(globstar1); + TEST_CASE(globstar2); + TEST_CASE(pathiterator); } // Test empty PathMatch @@ -97,13 +107,12 @@ class TestPathMatch : public TestFixture { } void onemasksamepathdifferentslash() const { - const PathMatch srcMatcher2{std::vector(1, "src\\")}; + PathMatch srcMatcher2({"src\\"}, basepath, windows); ASSERT(srcMatcher2.match("src/")); } void onemasksamepathdifferentcase() const { - std::vector masks(1, "sRc/"); - PathMatch match(std::move(masks), false); + PathMatch match({"sRc/"}, basepath, windows); ASSERT(match.match("srC/")); } @@ -115,7 +124,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)); } @@ -150,30 +159,26 @@ class TestPathMatch : public TestFixture { } void onemaskcwd() const { - ASSERT(!srcMatcher.match("./src")); + ASSERT(srcMatcher.match("./src")); } 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 +188,7 @@ class TestPathMatch : public TestFixture { } void filemaskdifferentcase() const { - std::vector masks(1, "foo.cPp"); - PathMatch match(std::move(masks), false); + PathMatch match({"foo.cPp"}, basepath, windows); ASSERT(match.match("fOo.cpp")); } @@ -219,11 +223,65 @@ 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 glob() const { + 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")); + } + + 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")); + } + + 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("/", 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()); + } }; REGISTER_TEST(TestPathMatch) 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;