From 42bb4794a18b7a4a5d31662b5cef411eb9bfc644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marjam=C3=A4ki?= Date: Fri, 6 Nov 2026 17:59:48 +0100 Subject: [PATCH 1/2] Fix #724 (##: valid token pastes are rejected) When the left operand of ## is a punctuator, check the pasted result after the right operand has been expanded, instead of checking the unexpanded parameter name. --- simplecpp.cpp | 44 +++++++++++++++++++++++++++++++------------- test.cpp | 28 ++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/simplecpp.cpp b/simplecpp.cpp index bdec34a6..ec573d74 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -160,6 +160,25 @@ static bool sameline(const simplecpp::Token *tok1, const simplecpp::Token *tok2) return tok1 && tok2 && tok1->location.sameline(tok2->location); } +/** is s a punctuator (C11 6.4.6, C++ [lex.operators]) */ +static bool isPunctuator(const std::string &s) +{ + static const std::set punctuators{ + "[", "]", "(", ")", "{", "}", ".", "->", "++", "--", "&", "*", "+", "-", "~", "!", + "/", "%", "<<", ">>", "<", ">", "<=", ">=", "==", "!=", "^", "|", "&&", "||", + "?", ":", ";", "...", "=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|=", + ",", "#", "##", "<:", ":>", "<%", "%>", "%:", "%:%:", "::", ".*", "->*", "<=>" + }; + return punctuators.find(s) != punctuators.end(); +} + +/** is s a valid result when a punctuator is pasted with ## */ +static bool isValidPunctuatorPaste(const std::string &s) +{ + // a pp-number can start with '.' + return isPunctuator(s) || (s.size() >= 2 && s[0] == '.' && std::isdigit(static_cast(s[1]))); +} + static bool isAlternativeBinaryOp(const simplecpp::Token *tok, const std::string &alt) { return (tok->name && @@ -2458,17 +2477,17 @@ namespace simplecpp { if (!sameline(tok, tok->next) || !sameline(tok, tok->next->next)) throw invalidHashHash::unexpectedNewline(tok->location, name()); - const bool canBeConcatenatedWithEqual = A->isOneOf("+-*/%&|^") || A->str() == "<<" || A->str() == ">>"; const bool canBeConcatenatedStringOrChar = isStringLiteral(A->str()) || isCharLiteral(A->str()); - const bool unexpectedA = (!A->name && !A->number && !A->str().empty() && !canBeConcatenatedWithEqual && !canBeConcatenatedStringOrChar); + // A is a punctuator; the result is checked when B has been expanded + const bool punctuatorA = (!A->name && !A->number && !A->str().empty() && !canBeConcatenatedStringOrChar); const Token * const B = tok->next->next; - if (!B->name && !B->number && B->op && !B->isOneOf("#=")) - throw invalidHashHash::unexpectedToken(tok->location, name(), B); - - if ((canBeConcatenatedWithEqual && B->op != '=') || - (!canBeConcatenatedWithEqual && B->op == '=')) - throw invalidHashHash::cannotCombine(tok->location, name(), A, B); + if (!punctuatorA) { + if (!B->name && !B->number && B->op && !B->isOneOf("#=")) + throw invalidHashHash::unexpectedToken(tok->location, name(), B); + if (B->op == '=') + throw invalidHashHash::cannotCombine(tok->location, name(), A, B); + } // Superficial check; more in-depth would in theory be possible _after_ expandArg if (canBeConcatenatedStringOrChar && (B->number || !B->name)) @@ -2500,16 +2519,15 @@ namespace simplecpp { else if (varargs && A->op == ',') { strAB = ","; } - else if (varargs && unexpectedA) { - throw invalidHashHash::unexpectedToken(tok->location, name(), A); - } else { + if (punctuatorA && !isValidPunctuatorPaste(A->str() + tokensB.cfront()->str())) + throw invalidHashHash::cannotCombine(tok->location, name(), A, tokensB.cfront()); strAB = A->str() + tokensB.cfront()->str(); tokensB.deleteToken(tokensB.front()); } } else { - if (unexpectedA) - throw invalidHashHash::unexpectedToken(tok->location, name(), A); + if (punctuatorA && !isValidPunctuatorPaste(A->str() + B->str())) + throw invalidHashHash::cannotCombine(tok->location, name(), A, B); strAB = A->str() + B->str(); } diff --git a/test.cpp b/test.cpp index 2561e3bf..a066d5da 100644 --- a/test.cpp +++ b/test.cpp @@ -2134,7 +2134,7 @@ static void hashhash_invalid_1() const char code[] = "#define f(a) (##x)\nf(1)\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); - ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'f', Invalid ## usage when expanding 'f': Unexpected token '('\n", toString(outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'f', Invalid ## usage when expanding 'f': Combining '(' and 'x' yields an invalid token.\n", toString(outputList)); } static void hashhash_invalid_2() @@ -2188,6 +2188,28 @@ static void hashhash_empty_va_args() ASSERT_EQUALS("\n\n\n( 2 )", preprocess(code)); } +static void hashhash_punctuator() // #724 +{ + const char code[] = + "#define cat(a,b) a##b\n" + "#define MINUS_GT -##>\n" + "#define VA(...) <##__VA_ARGS__\n" + "cat(+,=) cat(<<,=) cat(-,>) cat(&,&) cat(|,|) cat(+,+) cat(<,=) cat(.,5) MINUS_GT VA(<) VA()\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n\n\n+= <<= -> && || ++ <= .5 -> << <", preprocess(code, &outputList)); + ASSERT_EQUALS("", toString(outputList)); +} + +static void hashhash_punctuator_invalid() +{ + const char code[] = + "#define cat(a,b) a##b\n" + "cat(+,x)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'cat', Invalid ## usage when expanding 'cat': Combining '+' and 'x' yields an invalid token.\n", toString(outputList)); +} + static void hashhash_va_args_unexpected() { const char code[] = @@ -2195,7 +2217,7 @@ static void hashhash_va_args_unexpected() "C(1)\n"; 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)); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'C', Invalid ## usage when expanding 'C': Combining '!' and '1' yields an invalid token.\n", toString(outputList)); } static void hashhash_universal_character() @@ -4942,6 +4964,8 @@ 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_punctuator); + TEST_CASE(hashhash_punctuator_invalid); TEST_CASE(hashhash_va_args_unexpected); // C standard, 5.1.1.2, paragraph 4: From f2d2413f56206ee3c8fe8b695fd4cf909bc5e9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Marjam=C3=A4ki?= Date: Fri, 6 Nov 2026 18:50:04 +0100 Subject: [PATCH 2/2] Update integration test for new ## error message --- integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.py b/integration_test.py index 42dda9b2..b24f6352 100644 --- a/integration_test.py +++ b/integration_test.py @@ -501,7 +501,7 @@ def test_define(record_property, tmpdir): # #589 record_property("stderr", stderr) assert exitcode == 0 - assert stderr == "test.cpp:1: syntax error: failed to expand 'TEST_P', Invalid ## usage when expanding 'TEST_P': Unexpected token ')'\n" + assert stderr == "test.cpp:1: syntax error: failed to expand 'TEST_P', Invalid ## usage when expanding 'TEST_P': Combining ')' and '_' yields an invalid token.\n" assert stdout == '\n' def test_utf16_bom(tmpdir):