From f032cf291e7adc70b13e6a0f7be4d80ca2248f56 Mon Sep 17 00:00:00 2001 From: haelius <68783915+jm-clius@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:30:32 +0100 Subject: [PATCH] fix(header-parser): join multi-line method declarations before parsing (#91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(header-parser): join multi-line method declarations before parsing * strip qualifiers/attributes before return-type matching (#92) --------- Co-authored-by: Álex --- .../experimental/impl_header_parser.cpp | 75 ++++++++++++++++++- tests/experimental/fixtures/universal_impl.h | 7 ++ .../experimental/test_impl_header_parser.cpp | 66 ++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index d6e2474..7599f87 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.cpp @@ -8,6 +8,28 @@ #include #include +// --------------------------------------------------------------------------- +// Strip leading declaration specifiers / attributes from a return-type string. +// --------------------------------------------------------------------------- + +static QString stripDeclarationSpecifiers(QString string) +{ + static const QRegularExpression attributeRe("\\[\\[[^\\]]*\\]\\]"); + static const QRegularExpression specifierRe( + "^(static|virtual|inline|explicit|constexpr|consteval|friend)\\s+"); + + string.remove(attributeRe); + string = string.trimmed(); + + QRegularExpressionMatch specifierMatch = specifierRe.match(string); + while (specifierMatch.hasMatch()) { + string = string.mid(specifierMatch.capturedLength()).trimmed(); + specifierMatch = specifierRe.match(string); + } + + return string; +} + // --------------------------------------------------------------------------- // C++ type string → LIDL TypeExpr // --------------------------------------------------------------------------- @@ -139,7 +161,7 @@ static bool parseMethodLine(const QString& line, MethodDecl& out) if (cppKeywords.contains(methodName)) return false; out.name = methodName.toStdString(); - QString retTypeStr = prefix.left(nameStart).trimmed(); + QString retTypeStr = stripDeclarationSpecifiers(prefix.left(nameStart).trimmed()); out.returnType = cppTypeToLidl(retTypeStr); // Flag methods whose impl returns LogosMap / LogosList so the generator // can emit nlohmann→Qt conversion code in the glue layer. @@ -257,7 +279,56 @@ ImplParseResult parseImplHeader(const QString& headerPath, QString source = QString::fromUtf8(hf.readAll()); hf.close(); - QStringList lines = source.split('\n'); + // Split into physical lines, then merge any whose parentheses are still + // open into one logical line. The scanner below is line-based — it only + // accepts a method when a single trimmed line ends in ';' and + // parseMethodLine finds a balanced '(...)' on it — so without this a method + // signature wrapped across several physical lines is silently dropped. + // Parens inside comments / string / char literals are ignored. + QStringList lines; + { + const QStringList physical = source.split('\n'); + QString acc; + int parenDepth = 0; + bool inBlockComment = false; + for (const QString& phys : physical) { + bool inStr = false; + bool inChr = false; + for (int i = 0; i < phys.size(); ++i) { + const QChar c = phys[i]; + const QChar n = (i + 1 < phys.size()) ? phys[i + 1] : QChar(); + if (inBlockComment) { + if (c == '*' && n == '/') { inBlockComment = false; ++i; } + } else if (inStr) { + if (c == '\\') ++i; else if (c == '"') inStr = false; + } else if (inChr) { + if (c == '\\') ++i; else if (c == '\'') inChr = false; + } else if (c == '/' && n == '*') { + inBlockComment = true; ++i; + } else if (c == '/' && n == '/') { + break; + } else if (c == '"') { + inStr = true; + } else if (c == '\'') { + inChr = true; + } else if (c == '(') { + ++parenDepth; + } else if (c == ')') { + if (parenDepth > 0) --parenDepth; + } + } + if (acc.isEmpty()) + acc = phys; + else + acc += ' ' + phys.trimmed(); + if (parenDepth <= 0) { + lines.append(acc); + acc.clear(); + } + } + if (!acc.isEmpty()) + lines.append(acc); + } // State machine: find "class ", then collect declarations. // `InLogosEvents` is entered by the literal `logos_events:` token diff --git a/tests/experimental/fixtures/universal_impl.h b/tests/experimental/fixtures/universal_impl.h index ab96d6a..8453198 100644 --- a/tests/experimental/fixtures/universal_impl.h +++ b/tests/experimental/fixtures/universal_impl.h @@ -14,6 +14,13 @@ public: QStringList listNames(); QVariantList anyList(); StdLogosResult fetchResult(); + [[nodiscard]] StdLogosResult fetchResultNodiscard(); + static StdLogosResult fetchResultStatic(); + [[nodiscard]] static StdLogosResult fetchResultNodiscardStatic(); + static [[nodiscard]] StdLogosResult fetchResultStaticNodiscard(); + [[nodiscard]] [[deprecated]] StdLogosResult fetchResultMultiAttr(); + inline static StdLogosResult fetchResultInlineStatic(); + [[nodiscard]] consteval StdLogosResult fetchResultConsteval(); // A std::function member: the parser must skip it (not treat it as a // callable method). Used to predate the typed `logos_events:` mechanism diff --git a/tests/experimental/test_impl_header_parser.cpp b/tests/experimental/test_impl_header_parser.cpp index e0833db..88ee404 100644 --- a/tests/experimental/test_impl_header_parser.cpp +++ b/tests/experimental/test_impl_header_parser.cpp @@ -1,4 +1,5 @@ #include +#include #include "impl_header_parser.h" #include #include @@ -310,6 +311,41 @@ TEST_F(ImplHeaderParserTest, UniversalTypesAndMetadataEvents) EXPECT_FALSE(fetchResult->jsonReturn); EXPECT_TRUE(fetchResult->resultReturn); + auto fetchResultNodiscard = findMethod("fetchResultNodiscard"); + ASSERT_NE(fetchResultNodiscard, nullptr); + EXPECT_EQ(fetchResultNodiscard->returnType.name, "result"); + EXPECT_TRUE(fetchResultNodiscard->resultReturn); + + auto fetchResultStatic = findMethod("fetchResultStatic"); + ASSERT_NE(fetchResultStatic, nullptr); + EXPECT_EQ(fetchResultStatic->returnType.name, "result"); + EXPECT_TRUE(fetchResultStatic->resultReturn); + + auto fetchResultNodiscardStatic = findMethod("fetchResultNodiscardStatic"); + ASSERT_NE(fetchResultNodiscardStatic, nullptr); + EXPECT_EQ(fetchResultNodiscardStatic->returnType.name, "result"); + EXPECT_TRUE(fetchResultNodiscardStatic->resultReturn); + + auto fetchResultStaticNodiscard = findMethod("fetchResultStaticNodiscard"); + ASSERT_NE(fetchResultStaticNodiscard, nullptr); + EXPECT_EQ(fetchResultStaticNodiscard->returnType.name, "result"); + EXPECT_TRUE(fetchResultStaticNodiscard->resultReturn); + + auto fetchResultMultiAttr = findMethod("fetchResultMultiAttr"); + ASSERT_NE(fetchResultMultiAttr, nullptr); + EXPECT_EQ(fetchResultMultiAttr->returnType.name, "result"); + EXPECT_TRUE(fetchResultMultiAttr->resultReturn); + + auto fetchResultInlineStatic = findMethod("fetchResultInlineStatic"); + ASSERT_NE(fetchResultInlineStatic, nullptr); + EXPECT_EQ(fetchResultInlineStatic->returnType.name, "result"); + EXPECT_TRUE(fetchResultInlineStatic->resultReturn); + + auto fetchResultConsteval = findMethod("fetchResultConsteval"); + ASSERT_NE(fetchResultConsteval, nullptr); + EXPECT_EQ(fetchResultConsteval->returnType.name, "result"); + EXPECT_TRUE(fetchResultConsteval->resultReturn); + for (const auto& m : r.module.methods) { EXPECT_NE(m.name, "void") << "Keyword should not appear as method name"; } @@ -420,3 +456,33 @@ TEST_F(ImplHeaderParserTest, SameLineSectionSpecifiers) EXPECT_EQ(findMethod("versionReady"), nullptr); EXPECT_EQ(findMethod("downloadProgress"), nullptr); } + + +TEST_F(ImplHeaderParserTest, ParsesMultiLineSignature) +{ + QTemporaryDir dir; + ASSERT_TRUE(dir.isValid()); + const QString hp = dir.filePath("ml_impl.h"); + { + QFile f(hp); + ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text)); + f.write( + "#pragma once\n" + "#include \n" + "class MlImpl {\n" + "public:\n" + " std::string single(const std::string& a);\n" + " std::string wrapped(const std::string& first,\n" + " const std::string& second);\n" + "};\n"); + } + auto r = parseImplHeader(hp, "MlImpl", + fixturesDir() + "/sample_metadata.json", err); + ASSERT_FALSE(r.hasError()) << r.error.toStdString(); + + QStringList names; + for (const auto& m : r.module.methods) names << QString::fromStdString(m.name); + EXPECT_TRUE(names.contains("single")); + EXPECT_TRUE(names.contains("wrapped")) + << "got: " << names.join(",").toStdString(); +}