From 3b3b30e579388f80c245940b2d2b562860dbfafa Mon Sep 17 00:00:00 2001 From: Paul Fultz II Date: Tue, 23 Jun 2026 11:31:12 -0500 Subject: [PATCH 01/10] Remove dead branch in `simplecpp::Macro::expandHashHash()` (#664) --- simplecpp.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/simplecpp.cpp b/simplecpp.cpp index a3ca6916..4898800f 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -2318,9 +2318,6 @@ namespace simplecpp { const Token *nextTok = B->next; if (canBeConcatenatedStringOrChar) { - if (unexpectedA) - throw invalidHashHash::unexpectedToken(tok->location, name(), A); - // It seems clearer to handle this case separately even though the code is similar-ish, but we don't want to merge here. // TODO The question is whether the ## or varargs may still apply, and how to provoke? if (expandArg(tokensB, B, parametertokens)) { From 1b43796072fddf5ead6aea5c72d266d678f3cb45 Mon Sep 17 00:00:00 2001 From: glankk Date: Wed, 1 Jul 2026 20:29:47 +0200 Subject: [PATCH 02/10] Add file load callback (fixes #667) (#668) --- simplecpp.cpp | 3 +++ simplecpp.h | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/simplecpp.cpp b/simplecpp.cpp index 4898800f..3e572fc0 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -3155,6 +3155,9 @@ std::pair simplecpp::FileDataCache::tryload(FileDat mImpl->mIdMap.emplace(fileId, data); mData.emplace_back(data); + if (mLoadCallback) + mLoadCallback(*data); + return {data, true}; } diff --git a/simplecpp.h b/simplecpp.h index de20ae2e..3d6fc526 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -498,6 +499,12 @@ namespace simplecpp { return mData.cend(); } + using load_callback_type = std::function; + + void set_load_callback(load_callback_type cb) { + mLoadCallback = std::move(cb); + } + private: struct Impl; std::unique_ptr mImpl; @@ -508,6 +515,7 @@ namespace simplecpp { container_type mData; name_map_type mNameMap; + load_callback_type mLoadCallback; }; /** Converts character literal (including prefix, but not ud-suffix) to long long value. From 163fc92dd930b902c4c49e6673d88fadc5713106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20St=C3=B6neberg?= Date: Thu, 2 Jul 2026 08:41:36 +0200 Subject: [PATCH 03/10] improved test coverage of `simplecpp::Macro::expandHashHash()` (#665) --- test.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test.cpp b/test.cpp index 603d596d..2250ecf1 100644 --- a/test.cpp +++ b/test.cpp @@ -1875,6 +1875,16 @@ static void hashhash_empty_va_args() ASSERT_EQUALS("\n\n\n( 2 )", preprocess(code)); } +static void hashhash_va_args_unexpected() +{ + const char code[] = + "#define C(...)!##__VA_ARGS__\n" + "C(1)"; + simplecpp::OutputList outputList; + preprocess(code, simplecpp::DUI(), &outputList); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'C', Invalid ## usage when expanding 'C': Unexpected token '!'\n", toString(outputList)); +} + static void hashhash_universal_character() { const char code[] = @@ -1884,6 +1894,16 @@ static void hashhash_universal_character() ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '\\u01' and '04' yields universal character '\\u0104'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4.\n", toString(outputList)); } +static void hashhash_universal_character_2() +{ + const char code[] = + "#define A(x,y) x##y\nint A(\\U0104, 0104);"; + simplecpp::OutputList outputList; + preprocess(code, simplecpp::DUI(), &outputList); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '\\U0104' and '0104' yields universal character '\\U01040104'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4.\n", toString(outputList)); +} + + static void has_include_1() { const char code[] = "#ifdef __has_include\n" @@ -4019,11 +4039,14 @@ static void runTests(int argc, char **argv, Input input) TEST_CASE(hashhash_invalid_missing_args); TEST_CASE(hashhash_null_stmt); TEST_CASE(hashhash_empty_va_args); + TEST_CASE(hashhash_va_args_unexpected); + // C standard, 5.1.1.2, paragraph 4: // If a character sequence that matches the syntax of a universal // character name is produced by token concatenation (6.10.3.3), // the behavior is undefined." TEST_CASE(hashhash_universal_character); + TEST_CASE(hashhash_universal_character_2); // c++17 __has_include TEST_CASE(has_include_1); From fc7dca0bea10e1f37a9ff15ec234941122a3685a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20St=C3=B6neberg?= Date: Fri, 3 Jul 2026 13:48:46 +0200 Subject: [PATCH 04/10] optimized `std::stack` usage in `simplecpp::TokenList::combineOperators()` (#659) --- simplecpp.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/simplecpp.cpp b/simplecpp.cpp index 3e572fc0..df8ad5b8 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -1017,8 +1017,7 @@ static bool isAlternativeAndBitandBitor(const simplecpp::Token* tok) void simplecpp::TokenList::combineOperators() { - std::stack executableScope; - executableScope.push(false); + std::stack> executableScope{{false}}; for (Token *tok = front(); tok; tok = tok->next) { if (tok->op == '{') { if (executableScope.top()) { From 162eb964e8728990c442eece2cf26b4871a443ba Mon Sep 17 00:00:00 2001 From: glankk Date: Tue, 7 Jul 2026 10:38:36 +0200 Subject: [PATCH 05/10] Fix #669: Manually included files are not reported missing by preprocess (#670) --- simplecpp.cpp | 13 ++++++++++- test.cpp | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/simplecpp.cpp b/simplecpp.cpp index df8ad5b8..d2c398c4 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -3517,8 +3517,19 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL includetokenstack.push(rawtokens.cfront()); for (auto it = dui.includes.cbegin(); it != dui.includes.cend(); ++it) { const FileData *const filedata = cache.get("", *it, dui, false, files, outputList).first; - if (filedata != nullptr && filedata->tokens.cfront() != nullptr) + if (filedata == nullptr) { + if (outputList) { + simplecpp::Output err{ + simplecpp::Output::EXPLICIT_INCLUDE_NOT_FOUND, + {}, + "Can not open include file '" + *it + "' that is explicitly included." + }; + outputList->emplace_back(std::move(err)); + } + } + else if (filedata->tokens.cfront() != nullptr) { includetokenstack.push(filedata->tokens.cfront()); + } } std::map> maybeUsedMacros; diff --git a/test.cpp b/test.cpp index 2250ecf1..b13140aa 100644 --- a/test.cpp +++ b/test.cpp @@ -2926,6 +2926,64 @@ static void include9() ASSERT_EQUALS("\n#line 2 \"1.h\"\nx = 1 ;", out.stringify()); } +static void include10() // #669 - -include with load() +{ + const char code_c[] = "X\n"; + const char code_h[] = "#define X 123\n"; + + std::vector files; + + const simplecpp::TokenList rawtokens_c = makeTokenList(code_c, files, "src.c"); + const simplecpp::TokenList rawtokens_h = makeTokenList(code_h, files, "inc.h"); + + ASSERT_EQUALS(2U, files.size()); + ASSERT_EQUALS("src.c", files[0]); + ASSERT_EQUALS("inc.h", files[1]); + + simplecpp::FileDataCache cache; + cache.insert({"src.c", rawtokens_c}); + cache.insert({"inc.h", rawtokens_h}); + + simplecpp::OutputList outputList; + simplecpp::DUI dui; + dui.includes.emplace_back("inc.h"); + dui.includes.emplace_back("missing.h"); + cache = simplecpp::load(rawtokens_c, files, dui, &outputList, std::move(cache)); + + ASSERT_EQUALS(1, outputList.size()); + ASSERT_EQUALS("Can not open include file 'missing.h' that is explicitly included.", outputList.begin()->msg); +} + +static void include11() // #669 - -include with preprocess() +{ + const char code_c[] = "X\n"; + const char code_h[] = "#define X 123\n"; + + std::vector files; + + const simplecpp::TokenList rawtokens_c = makeTokenList(code_c, files, "src.c"); + const simplecpp::TokenList rawtokens_h = makeTokenList(code_h, files, "inc.h"); + + ASSERT_EQUALS(2U, files.size()); + ASSERT_EQUALS("src.c", files[0]); + ASSERT_EQUALS("inc.h", files[1]); + + simplecpp::FileDataCache cache; + cache.insert({"src.c", rawtokens_c}); + cache.insert({"inc.h", rawtokens_h}); + + simplecpp::OutputList outputList; + simplecpp::TokenList out(files); + simplecpp::DUI dui; + dui.includes.emplace_back("inc.h"); + dui.includes.emplace_back("missing.h"); + simplecpp::preprocess(out, rawtokens_c, files, cache, dui, &outputList); + + ASSERT_EQUALS("123", out.stringify()); + ASSERT_EQUALS(1, outputList.size()); + ASSERT_EQUALS("Can not open include file 'missing.h' that is explicitly included.", outputList.begin()->msg); +} + static void readfile_nullbyte() { const char code[] = "ab\0cd"; @@ -4118,6 +4176,8 @@ static void runTests(int argc, char **argv, Input input) TEST_CASE(include7); // #include MACRO TEST_CASE(include8); // #include MACRO(X) TEST_CASE(include9); // #include MACRO + TEST_CASE(include10); // -include with load() + TEST_CASE(include11); // -include with preprocess() TEST_CASE(multiline1); TEST_CASE(multiline2); From c44d686e1c4d25a72fb285d4c1d381c0d3cfb2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marjam=C3=A4ki?= Date: Fri, 10 Jul 2026 23:23:08 +0200 Subject: [PATCH 06/10] Fix #673 (unique Output::Type when directive is used in macro parameter) (#674) --- main.cpp | 3 +++ simplecpp.cpp | 17 ++++++++++++++++- simplecpp.h | 1 + test.cpp | 5 ++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index 685ada12..06afcfbb 100644 --- a/main.cpp +++ b/main.cpp @@ -272,6 +272,9 @@ int main(int argc, char **argv) case simplecpp::Output::SYNTAX_ERROR: std::cerr << "syntax error: "; break; + case simplecpp::Output::DIRECTIVE_AS_MACRO_PARAMETER: + std::cerr << "directive as macro parameter: "; + break; case simplecpp::Output::PORTABILITY_BACKSLASH: std::cerr << "portability: "; break; diff --git a/simplecpp.cpp b/simplecpp.cpp index d2c398c4..748ea6a9 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -1602,7 +1602,7 @@ namespace simplecpp { else if (rawtok->op == ')') --par; else if (rawtok->op == '#' && !sameline(rawtok->previous, rawtok)) - throw Error(rawtok->location, "it is invalid to use a preprocessor directive as macro parameter"); + throw invalidDirectiveAsMacroParameter(rawtok->location); rawtokens2.push_back(new Token(rawtok->str(), rawtok1->location, rawtok->whitespaceahead)); rawtok = rawtok->next; } @@ -1698,6 +1698,11 @@ namespace simplecpp { const std::string what; }; + struct invalidDirectiveAsMacroParameter : public Error { + invalidDirectiveAsMacroParameter(const Location &loc) + : Error(loc, "it is invalid to use a preprocessor directive as macro parameter") {} + }; + /** Struct that is thrown when macro is expanded with wrong number of parameters */ struct wrongNumberOfParameters : public Error { wrongNumberOfParameters(const Location &loc, const std::string ¯oName) : Error(loc, "Wrong number of parameters for macro \'" + macroName + "\'.") {} @@ -3344,6 +3349,16 @@ static bool preprocessToken(simplecpp::TokenList &output, const simplecpp::Token simplecpp::TokenList value(files); try { tok1 = it->second.expand(value, tok, macros, files); + } catch (const simplecpp::Macro::invalidDirectiveAsMacroParameter &err) { + if (outputList) { + simplecpp::Output out{ + simplecpp::Output::DIRECTIVE_AS_MACRO_PARAMETER, + err.location, + "failed to expand \'" + tok->str() + "\', " + err.what + }; + outputList->emplace_back(std::move(out)); + } + return false; } catch (const simplecpp::Macro::Error &err) { if (outputList) { simplecpp::Output out{ diff --git a/simplecpp.h b/simplecpp.h index 3d6fc526..ee2eb100 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -238,6 +238,7 @@ namespace simplecpp { MISSING_HEADER, INCLUDE_NESTED_TOO_DEEPLY, SYNTAX_ERROR, + DIRECTIVE_AS_MACRO_PARAMETER, PORTABILITY_BACKSLASH, UNHANDLED_CHAR_ERROR, EXPLICIT_INCLUDE_NOT_FOUND, diff --git a/test.cpp b/test.cpp index b13140aa..3a858929 100644 --- a/test.cpp +++ b/test.cpp @@ -197,6 +197,9 @@ static std::string toString(const simplecpp::OutputList &outputList) case simplecpp::Output::Type::SYNTAX_ERROR: ostr << "syntax_error,"; break; + case simplecpp::Output::DIRECTIVE_AS_MACRO_PARAMETER: + ostr << "directive_as_macro_parameter,"; + break; case simplecpp::Output::Type::PORTABILITY_BACKSLASH: ostr << "portability_backslash,"; break; @@ -1338,7 +1341,7 @@ static void define_ifdef() simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); - ASSERT_EQUALS("file0,3,syntax_error,failed to expand 'A', it is invalid to use a preprocessor directive as macro parameter\n", toString(outputList)); + ASSERT_EQUALS("file0,3,directive_as_macro_parameter,failed to expand 'A', it is invalid to use a preprocessor directive as macro parameter\n", toString(outputList)); } From 36e6e1211b76393c6d543578f33f4ab1ea7441b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20St=C3=B6neberg?= Date: Sun, 12 Jul 2026 15:21:36 +0200 Subject: [PATCH 07/10] improved test coverage of throwing code (#666) --- test.cpp | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/test.cpp b/test.cpp index 3a858929..990a6daf 100644 --- a/test.cpp +++ b/test.cpp @@ -132,11 +132,11 @@ static std::string readfile(const char code[], std::size_t size, simplecpp::Outp return makeTokenList(code,size,files,std::string(),outputList).stringify(); } -static std::string preprocess(const char code[], const simplecpp::DUI &dui, simplecpp::OutputList *outputList, std::list *macroUsage = nullptr, std::list *ifCond = nullptr, const std::string &file = std::string()) +static std::string preprocess(const char code[], std::size_t size, const simplecpp::DUI &dui, simplecpp::OutputList *outputList, std::list *macroUsage = nullptr, std::list *ifCond = nullptr, const std::string &file = std::string()) { std::vector files; simplecpp::FileDataCache cache; - simplecpp::TokenList tokens = makeTokenList(code,files, file); + simplecpp::TokenList tokens = makeTokenList(code, size, files, file); if (dui.removeComments) tokens.removeComments(); simplecpp::TokenList tokens2(files); @@ -145,6 +145,11 @@ static std::string preprocess(const char code[], const simplecpp::DUI &dui, simp return tokens2.stringify(); } +static std::string preprocess(const char code[], const simplecpp::DUI &dui, simplecpp::OutputList *outputList, std::list *macroUsage = nullptr, std::list *ifCond = nullptr, const std::string &file = std::string()) +{ + return preprocess(code, strlen(code), dui, outputList, macroUsage, ifCond, file); +} + static std::string preprocess(const char code[]) { return preprocess(code, simplecpp::DUI(), nullptr); @@ -165,6 +170,11 @@ static std::string preprocess(const char code[], simplecpp::OutputList *outputLi return preprocess(code, simplecpp::DUI(), outputList); } +static std::string preprocess(const char code[], std::size_t size, simplecpp::OutputList *outputList) +{ + return preprocess(code, size, simplecpp::DUI(), outputList); +} + static std::string preprocess(const char code[], std::list *ifCond) { return preprocess(code, simplecpp::DUI(), nullptr, nullptr, ifCond); @@ -916,6 +926,24 @@ static void define_invalid_2() ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define, bad macro syntax\n", toString(outputList)); } +static void define_invalid_3() +{ + const char code[] = "#define R()\n" + "R"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to expand 'R', Wrong number of parameters for macro 'R'.\n", toString(outputList)); +} + +static void define_invalid_4() +{ + const char code[] = "#define X(...)\n" + "X"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to expand 'X', Wrong number of parameters for macro 'X'.\n", toString(outputList)); +} + static void define_define_1() { const char code[] = "#define A(x) (x+1)\n" @@ -1345,6 +1373,30 @@ static void define_ifdef() } +static void if_invalid_1() +{ + const char code[] = "#if'\\u'"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, expected digit\n", toString(outputList)); +} + +static void if_invalid_2() +{ + const char code[] = "#if-0xBBB4444444444444%~B"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, division overflow\n", toString(outputList)); +} + +static void if_invalid_3() +{ + const char code[] = "#if@u'\\udefa'"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, surrogate code points not allowed in universal character names\n", toString(outputList)); +} + static void pragma_backslash() { const char code[] = "#pragma comment (longstring, \\\n" @@ -2036,6 +2088,42 @@ static void has_include_6() ASSERT_EQUALS("\n\nA", preprocess(code, dui)); } +static void define_has_include_invalid_1() +{ + const char code[] = "#define A)__has_include\n" + "#if\u000BA"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, missing __has_include argument\n", toString(outputList)); +} + +static void define_has_include_invalid_2() +{ + const char code[] = "#define f __has_include\n" + "#if#f<"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, missing __has_include argument\n", toString(outputList)); +} + +static void define_has_include_invalid_3() +{ + const char code[] = "#define\u0000X\u0007__has_include(\n" + "#if%X&"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, sizeof(code), &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, invalid __has_include expression\n", toString(outputList)); +} + +static void define_has_include_invalid_4() +{ + const char code[] = "#define\u0000X\u0000__has_include<2\n" + "#if*X"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, sizeof(code), &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, invalid __has_include expression\n", toString(outputList)); +} + static void strict_ansi_1() { const char code[] = "#if __STRICT_ANSI__\n" @@ -4010,6 +4098,8 @@ static void runTests(int argc, char **argv, Input input) TEST_CASE(define23); // #40 TEST_CASE(define_invalid_1); TEST_CASE(define_invalid_2); + TEST_CASE(define_invalid_3); + TEST_CASE(define_invalid_4); TEST_CASE(define_define_1); TEST_CASE(define_define_2); TEST_CASE(define_define_3); @@ -4055,6 +4145,10 @@ static void runTests(int argc, char **argv, Input input) // UB: #ifdef as macro parameter TEST_CASE(define_ifdef); + TEST_CASE(if_invalid_1); + TEST_CASE(if_invalid_2); + TEST_CASE(if_invalid_3); + TEST_CASE(dollar); TEST_CASE(error1); @@ -4117,6 +4211,11 @@ static void runTests(int argc, char **argv, Input input) TEST_CASE(has_include_5); TEST_CASE(has_include_6); + TEST_CASE(define_has_include_invalid_1); + TEST_CASE(define_has_include_invalid_2); + TEST_CASE(define_has_include_invalid_3); + TEST_CASE(define_has_include_invalid_4); + TEST_CASE(strict_ansi_1); TEST_CASE(strict_ansi_2); TEST_CASE(strict_ansi_3); From c968a8ec5a64f93a50f0b5bb79ab9d87edf34352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marjam=C3=A4ki?= Date: Mon, 13 Jul 2026 17:08:20 +0200 Subject: [PATCH 08/10] Fix #678 workaround needed for gcc bug (old gcc), defaulted noexcept constructors (#679) --- .github/workflows/gcc-versions.yml | 30 ++++++++++++++++++++++++++++++ simplecpp.cpp | 4 ++-- simplecpp.h | 13 +++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/gcc-versions.yml diff --git a/.github/workflows/gcc-versions.yml b/.github/workflows/gcc-versions.yml new file mode 100644 index 00000000..9c365a24 --- /dev/null +++ b/.github/workflows/gcc-versions.yml @@ -0,0 +1,30 @@ +# the purpose of this github action is to test that simplecpp source code can be compiled using old gcc versions. +# the gcc images we use here are copied from the official gcc images on docker hub + +name: gcc-versions + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + + strategy: + matrix: + #image: ["ghcr.io/cppcheck-opensource/gcc:5.4", "ghcr.io/cppcheck-opensource/gcc:6.5", "ghcr.io/cppcheck-opensource/gcc:7.5", "ghcr.io/cppcheck-opensource/gcc:8.5", "ghcr.io/cppcheck-opensource/gcc:9.5"] + image: ["ghcr.io/cppcheck-opensource/gcc:5.4"] + fail-fast: false + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Build and run tests + run: | + docker run --rm -v ${{ github.workspace }}:/simplecpp -w /simplecpp ${{ matrix.image }} make -j$(nproc) simplecpp testrunner + docker run --rm -v ${{ github.workspace }}:/simplecpp -w /simplecpp ${{ matrix.image }} ./testrunner diff --git a/simplecpp.cpp b/simplecpp.cpp index 748ea6a9..35980cbe 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -3131,8 +3131,8 @@ simplecpp::FileDataCache::FileDataCache() {} simplecpp::FileDataCache::~FileDataCache() = default; -simplecpp::FileDataCache::FileDataCache(FileDataCache &&) noexcept = default; -simplecpp::FileDataCache &simplecpp::FileDataCache::operator=(simplecpp::FileDataCache &&) noexcept = default; +simplecpp::FileDataCache::FileDataCache(FileDataCache &&) SIMPLECPP_NOEXCEPT = default; +simplecpp::FileDataCache &simplecpp::FileDataCache::operator=(simplecpp::FileDataCache &&) SIMPLECPP_NOEXCEPT = default; static bool getFileId(const std::string &path, FileID &id); diff --git a/simplecpp.h b/simplecpp.h index ee2eb100..27c538ea 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -60,6 +60,15 @@ # endif #endif +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 9 +// Hack to workaround GCC bug. +// Details: https://trac.cppcheck.net/ticket/14850 +// seen on g++ before 10.x +#define SIMPLECPP_NOEXCEPT +#else +#define SIMPLECPP_NOEXCEPT noexcept +#endif + namespace simplecpp { /** C code standard */ enum cstd_t : std::int8_t { CUnknown=-1, C89, C99, C11, C17, C23, C2Y }; @@ -454,10 +463,10 @@ namespace simplecpp { ~FileDataCache(); FileDataCache(const FileDataCache &) = delete; - FileDataCache(FileDataCache &&) noexcept; + FileDataCache(FileDataCache &&) SIMPLECPP_NOEXCEPT; FileDataCache &operator=(const FileDataCache &) = delete; - FileDataCache &operator=(FileDataCache &&) noexcept; + FileDataCache &operator=(FileDataCache &&) SIMPLECPP_NOEXCEPT; /** Get the cached data for a file, or load and then return it if it isn't cached. * returns the file data and true if the file was loaded, false if it was cached. */ From 178f363c7e9158a95f064a00871b81cec373dd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20St=C3=B6neberg?= Date: Tue, 14 Jul 2026 15:52:34 +0200 Subject: [PATCH 09/10] made it possibly to actually override the C++ standard in CMake (#677) --- .clang-tidy | 5 +++++ CMakeLists.txt | 6 +++++- test.cpp | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index e0b384bf..184cac3d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -31,10 +31,15 @@ Checks: > -modernize-avoid-c-arrays, -modernize-loop-convert, -modernize-pass-by-value, + -modernize-use-auto, + -modernize-use-designated-initializers, -modernize-use-nodiscard, + -modernize-use-ranges, + -modernize-use-starts-ends-with, -modernize-use-trailing-return-type, -readability-avoid-nested-conditional-operator, -readability-braces-around-statements, + -readability-container-contains, -readability-function-cognitive-complexity, -readability-function-size, -readability-implicit-bool-conversion, diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a90efae..f2c99e89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,13 @@ cmake_minimum_required (VERSION 3.10) project (simplecpp LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 11 CACHE STRING "C++ standard to use") set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(CMAKE_CXX_STANDARD LESS 11) + message(FATAL_ERROR "C++ standard was set to ${CMAKE_CXX_STANDARD} but 11 is required as a minimum") +endif() + include(CheckCXXCompilerFlag) if (WIN32) diff --git a/test.cpp b/test.cpp index 990a6daf..37b7da8d 100644 --- a/test.cpp +++ b/test.cpp @@ -18,6 +18,10 @@ #include #include +#ifdef __cpp_lib_string_view +#include +#endif + #ifndef SIMPLECPP_TEST_SOURCE_DIR #error "SIMPLECPP_TEST_SOURCE_DIR is not defined." #endif From f6947eb7cfda1d240aafbb960cc13678a3ed7d8d Mon Sep 17 00:00:00 2001 From: glankk Date: Mon, 20 Jul 2026 13:42:51 +0200 Subject: [PATCH 10/10] Fix #671: Missing newline at end of file should produce a warning (#672) --- integration_test.py | 24 +++---- main.cpp | 8 +++ simplecpp.cpp | 13 ++++ simplecpp.h | 1 + test.cpp | 148 ++++++++++++++++++++++++++++++++------------ 5 files changed, 142 insertions(+), 52 deletions(-) diff --git a/integration_test.py b/integration_test.py index c000b27b..42dda9b2 100644 --- a/integration_test.py +++ b/integration_test.py @@ -17,7 +17,7 @@ def __test_relative_header_create_header(dir, with_pragma_once=True): #error header_was_already_included #endif const int dummy = 1; - """) + """'\n') return header_file, "error: #error header_was_already_included" def __test_relative_header_create_source(dir, include1, include2, is_include1_sys=False, is_include2_sys=False, inv=False): @@ -31,7 +31,7 @@ def __test_relative_header_create_source(dir, include1, include2, is_include1_sy #undef TEST_H_INCLUDED #include {format_include(include1, is_include1_sys)} #include {format_include(include2, is_include2_sys)} - """) + """'\n') return src_file @pytest.mark.parametrize("with_pragma_once", (False, True)) @@ -201,27 +201,27 @@ def test_same_name_header(record_property, tmpdir): #include #include TEST - """) + """'\n') with open(header_a, "wt") as f: f.write(""" #include "same_name.h" - """) + """'\n') with open(header_b, "wt") as f: f.write(""" #include "same_name.h" - """) + """'\n') with open(same_name_a, "wt") as f: f.write(""" #define TEST E - """) + """'\n') with open(same_name_b, "wt") as f: f.write(""" #define TEST OK - """) + """'\n') args = [ format_include_path_arg(include_a), @@ -279,13 +279,13 @@ def test_pragma_once_matching(record_property, tmpdir): for n in names_to_test: f.write(f""" #include {n} - """); + """'\n'); with open(once_header, "wt") as f: f.write(f""" #pragma once ONCE - """); + """'\n'); args = [ format_include_path_arg(test_dir), @@ -463,7 +463,7 @@ def test_include_header_twice(tmpdir): #ifdef BBB # error BBB is defined #endif - """) + """'\n') test_file = os.path.join(tmpdir, 'test.c') with open(test_file, 'wt') as f: @@ -473,7 +473,7 @@ def test_include_header_twice(tmpdir): # define BBB # include "test.h" - """) + """'\n') args = [test_file] @@ -507,7 +507,7 @@ def test_define(record_property, tmpdir): # #589 def test_utf16_bom(tmpdir): test_file = os.path.join(tmpdir, "test.cpp") with open(test_file, 'wb') as f: - f.write(b'\xFF\xFE\x3B\x00') + f.write(b'\xFF\xFE\x3B\x00\x0A\x00') args = [test_file] diff --git a/main.cpp b/main.cpp index 06afcfbb..abb07228 100644 --- a/main.cpp +++ b/main.cpp @@ -278,6 +278,14 @@ int main(int argc, char **argv) case simplecpp::Output::PORTABILITY_BACKSLASH: std::cerr << "portability: "; break; + case simplecpp::Output::PORTABILITY_NO_EOF_NEWLINE: + if (simplecpp::getCStd(dui.std) == simplecpp::CUnknown) { + // Only UB for c code, suppress for c++ code + // If no standard is specified then prefer to have a false negative + continue; + } + std::cerr << "portability: "; + break; case simplecpp::Output::UNHANDLED_CHAR_ERROR: std::cerr << "unhandled char error: "; break; diff --git a/simplecpp.cpp b/simplecpp.cpp index 35980cbe..5ab6aa30 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -662,6 +662,7 @@ static const std::string COMMENT_END("*/"); void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, OutputList *outputList) { unsigned int multiline = 0U; + bool trailing_nl = true; const Token *oldLastToken = nullptr; @@ -671,6 +672,8 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, if (!stream.good()) break; + trailing_nl = false; + if (ch >= 0x80) { if (outputList) { simplecpp::Output err{ @@ -693,6 +696,7 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, } else { location.line += multiline + 1; multiline = 0U; + trailing_nl = true; } if (!multiline) location.col = 1; @@ -961,6 +965,15 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, location.adjust(currentToken); } + if (!trailing_nl && outputList) { + Output err{ + Output::PORTABILITY_NO_EOF_NEWLINE, + location, + "No newline at end of file." + }; + outputList->emplace_back(std::move(err)); + } + combineOperators(); } diff --git a/simplecpp.h b/simplecpp.h index 27c538ea..10f7d2a8 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -249,6 +249,7 @@ namespace simplecpp { SYNTAX_ERROR, DIRECTIVE_AS_MACRO_PARAMETER, PORTABILITY_BACKSLASH, + PORTABILITY_NO_EOF_NEWLINE, UNHANDLED_CHAR_ERROR, EXPLICIT_INCLUDE_NOT_FOUND, FILE_NOT_FOUND, diff --git a/test.cpp b/test.cpp index 37b7da8d..1771526c 100644 --- a/test.cpp +++ b/test.cpp @@ -217,6 +217,9 @@ static std::string toString(const simplecpp::OutputList &outputList) case simplecpp::Output::Type::PORTABILITY_BACKSLASH: ostr << "portability_backslash,"; break; + case simplecpp::Output::Type::PORTABILITY_NO_EOF_NEWLINE: + ostr << "portability_no_eof_newline,"; + break; case simplecpp::Output::Type::UNHANDLED_CHAR_ERROR: ostr << "unhandled_char_error,"; break; @@ -241,15 +244,15 @@ static void backslash() // preprocessed differently simplecpp::OutputList outputList; - readfile("//123 \\\n456", &outputList); + readfile("//123 \\\n456\n", &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("//123 \\ \n456", &outputList); + readfile("//123 \\ \n456\n", &outputList); ASSERT_EQUALS("file0,1,portability_backslash,Combination 'backslash space newline' is not portable.\n", toString(outputList)); outputList.clear(); - readfile("#define A \\\n123", &outputList); + readfile("#define A \\\n123\n", &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("#define A \\ \n123", &outputList); + readfile("#define A \\ \n123\n", &outputList); ASSERT_EQUALS("file0,1,portability_backslash,Combination 'backslash space newline' is not portable.\n", toString(outputList)); } @@ -1465,7 +1468,7 @@ static void error4() simplecpp::FileDataCache cache; simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); - const simplecpp::TokenList rawtoken = makeTokenList(code, sizeof(code),files,"test.c"); + const simplecpp::TokenList rawtoken = makeTokenList(code, sizeof(code)-1,files,"test.c"); simplecpp::preprocess(tokens2, rawtoken, files, cache, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,#error,#error x\n", toString(outputList)); } @@ -1478,7 +1481,7 @@ static void error5() simplecpp::FileDataCache cache; simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); - const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code),files,"test.c"); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,#error,#error x\n", toString(outputList)); } @@ -1491,7 +1494,7 @@ static void error6() simplecpp::FileDataCache cache; simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); - const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code),files,"test.c"); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,#error,#error \n", toString(outputList)); } @@ -1503,7 +1506,7 @@ static void error7() simplecpp::FileDataCache cache; simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); - const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code),files,"test.c"); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,#error,#error blabla\n", toString(outputList)); } @@ -1515,7 +1518,7 @@ static void error8() simplecpp::FileDataCache cache; simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); - const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code),files,"test.c"); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,#error,#error blabla\n", toString(outputList)); } @@ -3081,9 +3084,9 @@ static void include11() // #669 - -include with preprocess() static void readfile_nullbyte() { - const char code[] = "ab\0cd"; + const char code[] = "ab\0cd\n"; simplecpp::OutputList outputList; - ASSERT_EQUALS("ab cd", readfile(code,sizeof(code), &outputList)); + ASSERT_EQUALS("ab cd", readfile(code,sizeof(code)-1, &outputList)); ASSERT_EQUALS(true, outputList.empty()); // should warning be written? } @@ -3198,7 +3201,7 @@ static void readfile_string_error() outputList.clear(); // Don't warn for a multiline define - readfile("#define A \"abs\\\n\"", &outputList); + readfile("#define A \"abs\\\n\"\n", &outputList); ASSERT_EQUALS("", toString(outputList)); } @@ -3210,11 +3213,11 @@ static void readfile_cpp14_number() static void readfile_unhandled_chars() { simplecpp::OutputList outputList; - readfile("// 你好世界", &outputList); + readfile("// 你好世界\n", &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("s=\"你好世界\"", &outputList); + readfile("s=\"你好世界\"\n", &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("int 你好世界=0;", &outputList); + readfile("int 你好世界=0;\n", &outputList); ASSERT_EQUALS("file0,1,unhandled_char_error,The code contains unhandled character(s) (character code=228). Neither unicode nor extended ascii is supported.\n", toString(outputList)); } @@ -3234,6 +3237,70 @@ static void readfile_file_not_found() ASSERT_EQUALS("file0,0,file_not_found,File is missing: NotAFile\n", toString(outputList)); } +static void readfile_no_eof_newline() +{ + { + const char code[] = ""; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "\\\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file.\n", toString(outputList)); + } + { + const char code[] = "#define A"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file.\n", toString(outputList)); + } + { + const char code[] = "#define A\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "#define A\\"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file.\n", toString(outputList)); + } + { + const char code[] = "// comment"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file.\n", toString(outputList)); + } + { + const char code[] = "// comment\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "/* comment \n comment */"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("file0,2,portability_no_eof_newline,No newline at end of file.\n", toString(outputList)); + } + { + const char code[] = "/* comment \n comment */\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } +} + static void stringify1() { const char code_c[] = "#include \"A.h\"\n" @@ -3377,31 +3444,31 @@ static void unicode() { { const char code[] = "\xFE\xFF\x00\x31\x00\x32"; - ASSERT_EQUALS("12", readfile(code, sizeof(code))); + ASSERT_EQUALS("12", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x31\x00\x32\x00"; - ASSERT_EQUALS("12", readfile(code, sizeof(code))); + ASSERT_EQUALS("12", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE\xFF\x00\x2f\x00\x2f\x00\x0a\x00\x31"; - ASSERT_EQUALS("//\n1", readfile(code, sizeof(code))); + ASSERT_EQUALS("//\n1", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x2f\x00\x2f\x00\x0a\x00\x31\x00"; - ASSERT_EQUALS("//\n1", readfile(code, sizeof(code))); + ASSERT_EQUALS("//\n1", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE\xFF\x00\x22\x00\x61\x00\x22"; - ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code))); + ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x22\x00\x61\x00\x22\x00"; - ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code))); + ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code)-1)); } { const char code[] = "\xff\xfe\x0d\x00\x0a\x00\x2f\x00\x2f\x00\x31\x00\x0d\x00\x0a\x00"; - ASSERT_EQUALS("\n//1", readfile(code, sizeof(code))); + ASSERT_EQUALS("\n//1", readfile(code, sizeof(code)-1)); } } @@ -3409,35 +3476,35 @@ static void unicode_invalid() { { const char code[] = "\xFF"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFE\xFF\x31"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + const char code[] = "\xFE\xFF\x31\x00"; + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFF\xFE\x31"; - ASSERT_EQUALS("1", readfile(code, sizeof(code))); + const char code[] = "\xFF\xFE\x31\x00"; + ASSERT_EQUALS("1", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE\xFF\x31\x32"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x31\x32"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFE\xFF\x00\x31\x00\x32\x33"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + const char code[] = "\xFE\xFF\x00\x31\x00\x32\x33\x00"; + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFF\xFE\x31\x00\x32\x00\x33"; - ASSERT_EQUALS("123", readfile(code, sizeof(code))); + const char code[] = "\xFF\xFE\x31\x00\x32\x00\x33\x00"; + ASSERT_EQUALS("123", readfile(code, sizeof(code)-1)); } } @@ -3805,19 +3872,19 @@ static void tokenlist_api() // sized array + size { char input[] = "code"; // NOLINT(misc-const-correctness) - simplecpp::TokenList(input,sizeof(input),filenames,""); + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); } { const char input[] = "code"; - simplecpp::TokenList(input,sizeof(input),filenames,""); + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); } { unsigned char input[] = "code"; // NOLINT(misc-const-correctness) - simplecpp::TokenList(input,sizeof(input),filenames,""); + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); } { const unsigned char input[] = "code"; - simplecpp::TokenList(input,sizeof(input),filenames,""); + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); } #endif // !defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) // pointer via View @@ -3837,11 +3904,11 @@ static void tokenlist_api() // sized array + size via View/std::span { char input[] = "code"; // NOLINT(misc-const-correctness) - simplecpp::TokenList({input,sizeof(input)},filenames,""); + simplecpp::TokenList({input,sizeof(input)-1},filenames,""); } { const char input[] = "code"; - simplecpp::TokenList({input,sizeof(input)},filenames,""); + simplecpp::TokenList({input,sizeof(input)-1},filenames,""); } // sized array { @@ -4305,6 +4372,7 @@ static void runTests(int argc, char **argv, Input input) TEST_CASE(readfile_unhandled_chars); TEST_CASE(readfile_error); TEST_CASE(readfile_file_not_found); + TEST_CASE(readfile_no_eof_newline); TEST_CASE(stringify1);