From bc3adb6f008874dedc357ddf90dae865f3454387 Mon Sep 17 00:00:00 2001 From: firewave Date: Thu, 27 Feb 2025 15:13:21 +0100 Subject: [PATCH] fixed #14261 - Regex: added `std::regex` implementation [skip ci] --- Makefile | 17 +++++- cli/cmdlineparser.cpp | 10 ++- cmake/compilerDefinitions.cmake | 6 ++ cmake/options.cmake | 38 +++++++++++- cmake/printInfo.cmake | 7 ++- lib/regex.cpp | 105 ++++++++++++++++++++++++++++++-- lib/regex.h | 10 ++- releasenotes.txt | 2 + test/testcmdlineparser.cpp | 67 ++++++++++++++++++-- test/testregex.cpp | 27 ++++++++ 10 files changed, 268 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 42b754733ec..0c92297c862 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,10 @@ ifndef HAVE_RULES HAVE_RULES= endif +ifndef HAVE_PCRE + HAVE_PCRE=1 +endif + ifndef MATCHCOMPILER MATCHCOMPILER= endif @@ -140,19 +144,26 @@ ifeq (g++, $(findstring g++,$(CXX))) endif override CXXFLAGS += -std=c++11 ifeq ($(HAVE_RULES),yes) + override CPPFLAGS += -DHAVE_RULES + # TODO: handle explicit disabling + HAVE_PCRE=1 + override CPPFLAGS += -DHAVE_STD_REGEX +else ifneq ($(HAVE_RULES),) + $(error invalid HAVE_RULES value '$(HAVE_RULES)') +endif + +ifeq ($(HAVE_PCRE),1) PCRE_CONFIG = $(shell which pcre-config) ifeq ($(PCRE_CONFIG),) $(error Did not find pcre-config) endif override CXXFLAGS += $(shell $(PCRE_CONFIG) --cflags) - override CPPFLAGS += -DHAVE_RULES + override CPPFLAGS += -DHAVE_PCRE ifdef LIBS LIBS += $(shell $(PCRE_CONFIG) --libs) else LIBS=$(shell $(PCRE_CONFIG) --libs) endif -else ifneq ($(HAVE_RULES),) - $(error invalid HAVE_RULES value '$(HAVE_RULES)') endif # older make versions do not support # in $(shell) and newer ones handle the escape sequence literally diff --git a/cli/cmdlineparser.cpp b/cli/cmdlineparser.cpp index f2d2254f833..90a3431b775 100644 --- a/cli/cmdlineparser.cpp +++ b/cli/cmdlineparser.cpp @@ -1317,7 +1317,7 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a } std::string regex_err; - auto regex = Regex::create(rule.pattern, Regex::Engine::Pcre, regex_err); + auto regex = Regex::create(rule.pattern, Regex::defaultEngine(), regex_err); if (!regex) { mLogger.printError("failed to compile rule pattern '" + rule.pattern + "' (" + regex_err + ")."); return Result::Fail; @@ -1345,7 +1345,7 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a node = node->FirstChildElement("rule"); for (; node && strcmp(node->Value(), "rule") == 0; node = node->NextSiblingElement()) { Rule rule; - Regex::Engine regexEngine = Regex::Engine::Pcre; + Regex::Engine regexEngine = Regex::defaultEngine(); for (const tinyxml2::XMLElement *subnode = node->FirstChildElement(); subnode; subnode = subnode->NextSiblingElement()) { const char * const subname = subnode->Name(); @@ -1377,10 +1377,8 @@ CmdLineParser::Result CmdLineParser::parseFromArgs(int argc, const char* const a } else if (std::strcmp(subname, "engine") == 0) { const char * const engine = empty_if_null(subtext); - if (std::strcmp(engine, "pcre") == 0) { - regexEngine = Regex::Engine::Pcre; - } - else { + if (!Regex::stringToEngine(engine, regexEngine)) + { mLogger.printError(std::string("unknown regex engine '") + engine + "'."); return Result::Fail; } diff --git a/cmake/compilerDefinitions.cmake b/cmake/compilerDefinitions.cmake index 5f03b83dcee..523a7e9c989 100644 --- a/cmake/compilerDefinitions.cmake +++ b/cmake/compilerDefinitions.cmake @@ -37,6 +37,12 @@ endif() if(HAVE_RULES) add_definitions(-DHAVE_RULES) + if(HAVE_PCRE) + add_definitions(-DHAVE_PCRE) + endif() + if(HAVE_STD_REGEX) + add_definitions(-DHAVE_STD_REGEX) + endif() endif() if(Boost_FOUND) diff --git a/cmake/options.cmake b/cmake/options.cmake index 0fa27e4734b..e6f0f8e2ed5 100644 --- a/cmake/options.cmake +++ b/cmake/options.cmake @@ -94,7 +94,43 @@ if(NOT BUILD_GUI) endif() endif() -option(HAVE_RULES "Usage of rules (needs PCRE library and headers)" OFF) +option(HAVE_RULES "Usage of rules" OFF) +option(HAVE_PCRE "Allow usage of PCRE in rules" OFF) +option(HAVE_STD_REGEX "Allow usage of std::regex in rules" OFF) +# TODO: add option to specify default regular expression engine +if(NOT HAVE_RULES) + if(HAVE_PCRE) + message(WARNING "HAVE_PCRE has no effect without HAVE_RULES") + endif() + if(HAVE_STD_REGEX) + message(WARNING "HAVE_STD_REGEX has no effect without HAVE_RULES") + endif() +endif() +if(HAVE_RULES) + # TODO: handle explicit disabling + # TODO: enable deprecation warning when PCRE2 support has been added + #message(DEPRECATION "HAVE_RULES will no longer explicitly enable HAVE_PCRE with Cppcheck 2.2x") + #set(HAVE_PCRE ON) + + if(HAVE_PCRE) + # TODO: enable deprecation warning when PCRE2 support has been added + #message(DEPRECATION "Support for PCRE has been deprecated and will be removed in a future Cppcheck version") + endif() + + if(MSVC) + if(HAVE_STD_REGEX) + message(WARNING "Disabling usage of std::regex since its implementation is broken in Visual Studio") + set(HAVE_STD_REGEX OFF) + endif() + else() + # TODO: handle explicit disabling + set(HAVE_STD_REGEX ON) + endif() + + if(NOT HAVE_PCRE AND NOT HAVE_STD_REGEX) + message(FATAL_ERROR "Cannot use rules since no regular expression engine is available") + endif() +endif() option(USE_BUNDLED_TINYXML2 "Usage of bundled TinyXML2 library" ON) if(BUILD_CORE_DLL AND NOT USE_BUNDLED_TINYXML2) message(FATAL_ERROR "Cannot use external TinyXML2 library when building lib as DLL") diff --git a/cmake/printInfo.cmake b/cmake/printInfo.cmake index 87c7e41f284..e44bd94b0b6 100644 --- a/cmake/printInfo.cmake +++ b/cmake/printInfo.cmake @@ -79,7 +79,12 @@ endif() message(STATUS) message(STATUS "HAVE_RULES = ${HAVE_RULES}") if(HAVE_RULES) - message(STATUS "PCRE_LIBRARY = ${PCRE_LIBRARY}") + message(STATUS "HAVE_PCRE = ${HAVE_PCRE}") + if(HAVE_PCRE) + message(STATUS "PCRE_INCLUDE = ${PCRE_INCLUDE}") + message(STATUS "PCRE_LIBRARY = ${PCRE_LIBRARY}") + endif() + message(STATUS "HAVE_STD_REGEX = ${HAVE_STD_REGEX}") endif() message(STATUS) message(STATUS "DISALLOW_THREAD_EXECUTOR = ${DISALLOW_THREAD_EXECUTOR}") diff --git a/lib/regex.cpp b/lib/regex.cpp index 3a94b293ed5..16cc5b0958a 100644 --- a/lib/regex.cpp +++ b/lib/regex.cpp @@ -22,11 +22,18 @@ #include +#if defined(HAVE_STD_REGEX) +#include +#endif + +#if defined(HAVE_PCRE) #ifdef _WIN32 #define PCRE_STATIC #endif #include +#endif +#if defined(HAVE_PCRE) namespace { std::string pcreErrorCodeToString(const int pcreExecRet) { @@ -249,6 +256,58 @@ namespace { return ""; } } +#endif // HAVE_PCRE + +#if defined(HAVE_STD_REGEX) +namespace { + class StdRegex : public Regex + { + public: + explicit StdRegex(std::string pattern) + : mPattern(std::move(pattern)) + {} + + std::string compile() + { + if (mCompiled) + return "regular expression has already been compiled"; + + try { + mRegex = std::regex(mPattern); + } catch (const std::exception& e) { + return e.what(); + } + mCompiled = true; + return ""; + } + + std::string match(const std::string& str, const MatchFn& matchFn) const override + { + if (!mCompiled) + return "regular expression has not been compiled yet"; + + auto I = std::sregex_iterator(str.cbegin(), str.cend(), mRegex); + const auto E = std::sregex_iterator(); + while (I != E) + { + const std::smatch& match = *I; + matchFn(match.position(), match.position() + match.length()); + ++I; + } + return ""; + } + + Engine engine() const override { + return Engine::Std; + } + + private: + std::string mPattern; + std::regex mRegex; + bool mCompiled{}; + }; +} +#endif // HAVE_STD_REGEX template static T* createAndCompileRegex(std::string pattern, std::string& err) @@ -261,10 +320,21 @@ static T* createAndCompileRegex(std::string pattern, std::string& err) std::shared_ptr Regex::create(std::string pattern, Engine engine, std::string& err) { Regex* regex = nullptr; - if (engine == Engine::Pcre) - regex = createAndCompileRegex(std::move(pattern), err); - else { - err = "unknown regular expression engine"; + switch(engine) + { +#if defined(HAVE_PCRE) + case Engine::Pcre: + regex = createAndCompileRegex(std::move(pattern), err); + break; +#endif +#if defined(HAVE_STD_REGEX) + case Engine::Std: + regex = createAndCompileRegex(std::move(pattern), err); + break; +#endif + default: + err = "unknown regular expression engine"; + break; } if (!err.empty()) { delete regex; @@ -273,4 +343,31 @@ std::shared_ptr Regex::create(std::string pattern, Engine engine, std::st return std::shared_ptr(regex); } +bool Regex::stringToEngine(const char* engine, Regex::Engine& regexEngine) +{ +#if defined(HAVE_PCRE) + if (std::strcmp(engine, "pcre") == 0) { + regexEngine = Regex::Engine::Pcre; + return true; + } +#endif +#if defined(HAVE_STD_REGEX) + if (std::strcmp(engine, "std") == 0) { + regexEngine = Regex::Engine::Std; + return true; + } +#endif + regexEngine = Regex::Engine::Unknown; + return false; +} + +Regex::Engine Regex::defaultEngine() +{ +#if defined(HAVE_PCRE) + return Regex::Engine::Pcre; +#else + return Regex::Engine::Std; +#endif +} + #endif // HAVE_RULES diff --git a/lib/regex.h b/lib/regex.h index c17fba41d4e..07cad924f5d 100644 --- a/lib/regex.h +++ b/lib/regex.h @@ -41,12 +41,20 @@ class CPPCHECKLIB Regex enum class Engine : std::uint8_t { Unknown = 0, - Pcre = 1 +#if defined(HAVE_PCRE) + Pcre = 1, +#endif +#if defined(HAVE_STD_REGEX) + Std = 2 +#endif }; virtual Engine engine() const = 0; static std::shared_ptr create(std::string pattern, Engine engine, std::string& err); + + static bool stringToEngine(const char* engine, Regex::Engine& regexEngine); + static Engine defaultEngine(); }; #endif // HAVE_RULES diff --git a/releasenotes.txt b/releasenotes.txt index 0ca26f2374a..7ec42af2dda 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -21,4 +21,6 @@ Infrastructure & dependencies: - Other: +- Added support for `std::regex` as the regular engine for rules. It can be specified using `std` in the engine` element in a rule XML. +- - diff --git a/test/testcmdlineparser.cpp b/test/testcmdlineparser.cpp index 326b7ad613d..d68372ed77d 100644 --- a/test/testcmdlineparser.cpp +++ b/test/testcmdlineparser.cpp @@ -409,6 +409,8 @@ class TestCmdlineParser : public TestFixture { #ifdef HAVE_RULES TEST_CASE(ruleFileMulti); TEST_CASE(ruleFileSingle); + TEST_CASE(ruleFileSinglePcre); + TEST_CASE(ruleFileSingleStd); TEST_CASE(ruleFileEmpty); TEST_CASE(ruleFileMissing); TEST_CASE(ruleFileInvalid); @@ -2702,7 +2704,15 @@ class TestCmdlineParser : public TestFixture { REDIRECT; const char * const argv[] = {"cppcheck", "--rule=.*\\", "file.cpp"}; ASSERT_EQUALS_ENUM(CmdLineParser::Result::Fail, parser->parseFromArgs(3, argv)); +#if defined(HAVE_PCRE) ASSERT_EQUALS("cppcheck: error: failed to compile rule pattern '.*\\' (\\ at end of pattern).\n", logger->str()); +#elif defined(HAVE_STD_REGEX) + #if defined(_LIBCPP_VERSION) + ASSERT_EQUALS("cppcheck: error: failed to compile rule pattern '.*\\' (The expression contained an invalid escaped character, or a trailing escape.).\n", logger->str()); + #else + ASSERT_EQUALS("cppcheck: error: failed to compile rule pattern '.*\\' (Invalid escape at end of regular expression).\n", logger->str()); + #endif +#endif } #else void ruleNotSupported() { @@ -2719,7 +2729,6 @@ class TestCmdlineParser : public TestFixture { ScopedFile file("rule.xml", "\n" "\n" - "pcre\n" "raw\n" ".+\n" "\n" @@ -2742,14 +2751,14 @@ class TestCmdlineParser : public TestFixture { ASSERT_EQUALS_ENUM(CmdLineParser::Result::Success, parseFromArgs(argv)); ASSERT_EQUALS(2, settings->rules.size()); auto it = settings->rules.cbegin(); - ASSERT_EQUALS_ENUM(Regex::Engine::Pcre, it->regex->engine()); + ASSERT_EQUALS_ENUM(Regex::defaultEngine(), it->regex->engine()); ASSERT_EQUALS("raw", it->tokenlist); ASSERT_EQUALS(".+", it->pattern); ASSERT_EQUALS_ENUM(Severity::error, it->severity); ASSERT_EQUALS("ruleId1", it->id); ASSERT_EQUALS("ruleSummary1", it->summary); ++it; - ASSERT_EQUALS_ENUM(Regex::Engine::Pcre, it->regex->engine()); + ASSERT_EQUALS_ENUM(Regex::defaultEngine(), it->regex->engine()); ASSERT_EQUALS("define", it->tokenlist); ASSERT_EQUALS(".*", it->pattern); ASSERT_EQUALS_ENUM(Severity::warning, it->severity); @@ -2773,7 +2782,7 @@ class TestCmdlineParser : public TestFixture { ASSERT_EQUALS_ENUM(CmdLineParser::Result::Success, parseFromArgs(argv)); ASSERT_EQUALS(1, settings->rules.size()); auto it = settings->rules.cbegin(); - ASSERT_EQUALS_ENUM(Regex::Engine::Pcre, it->regex->engine()); + ASSERT_EQUALS_ENUM(Regex::defaultEngine(), it->regex->engine()); ASSERT_EQUALS("define", it->tokenlist); ASSERT_EQUALS(".+", it->pattern); ASSERT_EQUALS_ENUM(Severity::error, it->severity); @@ -2781,6 +2790,46 @@ class TestCmdlineParser : public TestFixture { ASSERT_EQUALS("ruleSummary", it->summary); } + void ruleFileSinglePcre() { + REDIRECT; + ScopedFile file("rule_single_pcre.xml", + "\n" + ".+\n" + "pcre\n" + "\n"); + const char * const argv[] = {"cppcheck", "--rule-file=rule_single_pcre.xml", "file.cpp"}; +#if defined(HAVE_PCRE) + ASSERT_EQUALS_ENUM(CmdLineParser::Result::Success, parseFromArgs(argv)); + ASSERT_EQUALS(1, settings->rules.size()); + auto it = settings->rules.cbegin(); + ASSERT_EQUALS_ENUM(Regex::Engine::Pcre, it->regex->engine()); + ASSERT_EQUALS(".+", it->pattern); +#else + ASSERT_EQUALS_ENUM(CmdLineParser::Result::Fail, parseFromArgs(argv)); + ASSERT_EQUALS("cppcheck: error: unknown regex engine 'pcre'.\n", logger->str()); // TODO: lacks file context +#endif + } + + void ruleFileSingleStd() { + REDIRECT; + ScopedFile file("rule_single_std.xml", + "\n" + ".+\n" + "std\n" + "\n"); + const char * const argv[] = {"cppcheck", "--rule-file=rule_single_std.xml", "file.cpp"}; +#if defined(HAVE_STD_REGEX) + ASSERT_EQUALS_ENUM(CmdLineParser::Result::Success, parseFromArgs(argv)); + ASSERT_EQUALS(1, settings->rules.size()); + auto it = settings->rules.cbegin(); + ASSERT_EQUALS_ENUM(Regex::Engine::Std, it->regex->engine()); + ASSERT_EQUALS(".+", it->pattern); +#else + ASSERT_EQUALS_ENUM(CmdLineParser::Result::Fail, parseFromArgs(argv)); + ASSERT_EQUALS("cppcheck: error: unknown regex engine 'std'..\n", logger->str()); // TODO: lacks file context +#endif + } + void ruleFileEmpty() { REDIRECT; const char * const argv[] = {"cppcheck", "--rule-file=", "file.cpp"}; @@ -2941,7 +2990,15 @@ class TestCmdlineParser : public TestFixture { "\n"); const char * const argv[] = {"cppcheck", "--rule-file=rule.xml", "file.cpp"}; ASSERT_EQUALS_ENUM(CmdLineParser::Result::Fail, parser->parseFromArgs(3, argv)); +#if defined(HAVE_PCRE) ASSERT_EQUALS("cppcheck: error: unable to load rule-file 'rule.xml' - pattern '.+\\' failed to compile (\\ at end of pattern).\n", logger->str()); +#elif defined(HAVE_STD_REGEX) + #if defined(_LIBCPP_VERSION) + ASSERT_EQUALS("cppcheck: error: unable to load rule-file 'rule.xml' - pattern '.+\\' failed to compile (The expression contained an invalid escaped character, or a trailing escape.).\n", logger->str()); + #else + ASSERT_EQUALS("cppcheck: error: unable to load rule-file 'rule.xml' - pattern '.+\\' failed to compile (Invalid escape at end of regular expression).\n", logger->str()); + #endif +#endif } void ruleFileInvalidEngine() { @@ -2953,7 +3010,7 @@ class TestCmdlineParser : public TestFixture { "\n"); const char * const argv[] = {"cppcheck", "--rule-file=rule.xml", "file.cpp"}; ASSERT_EQUALS_ENUM(CmdLineParser::Result::Fail, parseFromArgs(argv)); - ASSERT_EQUALS("cppcheck: error: unknown regex engine 'llvm'.\n", logger->str()); + ASSERT_EQUALS("cppcheck: error: unknown regex engine 'llvm'.\n", logger->str()); // TODO: lacks file context } #else void ruleFileNotSupported() { diff --git a/test/testregex.cpp b/test/testregex.cpp index f26a9fee4fa..efdb8d972e0 100644 --- a/test/testregex.cpp +++ b/test/testregex.cpp @@ -85,8 +85,24 @@ class TestRegExBase : public TestFixture { void compileError() const { std::string exp; +#if defined(HAVE_PCRE) if (mEngine == Regex::Engine::Pcre) exp = "missing terminating ] for character class"; +#endif +#if defined(HAVE_STD_REGEX) + if (mEngine == Regex::Engine::Std) + { +#if defined(_MSC_VER) + exp = "regex_error(error_brack): The expression contained mismatched [ and ]."; +#elif defined(_LIBCPP_VERSION) + exp = "The expression contained mismatched [ and ]."; +#elif defined(__clang__) + exp = "Unexpected character within '[...]' in regular expression"; +#else + exp = "Unexpected character in bracket expression."; +#endif + } +#endif (void)assertRegex("[", exp); } @@ -198,11 +214,22 @@ class TestRegExBase : public TestFixture { #undef assertRegex }; +#if defined (HAVE_PCRE) class TestRegExPcre : public TestRegExBase { public: TestRegExPcre() : TestRegExBase("TestRegExPcre", Regex::Engine::Pcre) {} }; REGISTER_TEST(TestRegExPcre) +#endif + +#if defined (HAVE_STD_REGEX) +class TestRegExStd : public TestRegExBase { +public: + TestRegExStd() : TestRegExBase("TestRegExStd", Regex::Engine::Std) {} +}; + +REGISTER_TEST(TestRegExStd) +#endif #endif // HAVE_RULES