From 49a6543cef36ed87ea58e396e2bc7be727eca041 Mon Sep 17 00:00:00 2001 From: haelius Date: Wed, 17 Jun 2026 12:30:56 +0100 Subject: [PATCH] fix(header-parser): join multi-line method declarations before parsing --- .../experimental/impl_header_parser.cpp | 51 ++++++++++++++++++- .../experimental/test_impl_header_parser.cpp | 31 +++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index d6e2474..58b0e79 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.cpp @@ -257,7 +257,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/test_impl_header_parser.cpp b/tests/experimental/test_impl_header_parser.cpp index e0833db..70bcc44 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 @@ -420,3 +421,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(); +}