Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
//
// Scans qtbase/src headers for Qt classes suitable for fuzz testing.
//
// Strategy:
// For each .h file under qtbase/src/<module>/...
// 1. Read the file.
// 2. Strip block comments so that class declarations inside /* ... */
// are never matched.
// 3. Search for all Q-prefixed class declarations (with or without
// inheritance). No QObject ancestry is required.
// 4. For each candidate class, scan its own body for:
// a. the Q_OBJECT macro — enables introspection-based fuzzing.
// b. pure-virtual method names — for abstract detection.
// c. constructors — for default-constructibility detection.
// d. public non-signal methods with their parameter signatures —
// for direct-call fuzzing.
// 5. Walk backwards from the class declaration for platform guards.
// 6. Record the canonical class name, module, and all flags.
//
// Fuzzing strategy per class:
// - hasQObject == true: introspection-based (QMetaObject slots/invokables)
// AND direct-call (all public methods).
// - hasQObject == false: direct-call only (all public methods).
//
// Abstract detection uses pure-virtual name sets:
// - A class is directly abstract if its own body declares any "= 0".
// - A class inherits abstract status from a base only if the base has
// unresolved pure virtuals that the subclass doesn't override.
//
// Universe construction (all Q-prefixed classes):
// Phase 1 – collect all Q-prefixed class declarations from all headers.
// Phase 2 – propagate unresolved pure virtuals through the chain.
// Phase 3 – collect results for requested modules.
#include "scanner.h"
#include "modulemap.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <fstream>
#include <iostream>
#include <regex>
#include <set>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
namespace QtFuzz {
namespace {
// Returns true if src contains any QML exposure macro.
static bool hasQmlMarker(const std::string &src)
{
return src.find("QML_ELEMENT") != std::string::npos
|| src.find("QML_NAMED_ELEMENT") != std::string::npos
|| src.find("QML_UNCREATABLE") != std::string::npos
|| src.find("QML_INTERFACE") != std::string::npos
|| src.find("QML_ANONYMOUS") != std::string::npos;
}
std::string readFile(const fs::path &p)
{
std::ifstream f(p);
if (!f)
return {};
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
std::string stripBlockComments(const std::string &source)
{
std::string result;
result.reserve(source.size());
size_t i = 0;
while (i < source.size()) {
if (i + 1 < source.size() && source[i] == '/' && source[i + 1] == '*') {
i += 2;
while (i < source.size()) {
if (i + 1 < source.size() && source[i] == '*' && source[i + 1] == '/') {
i += 2;
break;
}
result += (source[i] == '\n') ? '\n' : ' ';
++i;
}
} else {
result += source[i++];
}
}
return result;
}
// Returns the preprocessor keyword of a source line, or "" if the line is not
// a preprocessor directive. E.g. "# if defined(X)" → "if", "#endif" → "endif".
static std::string ppKeyword(const std::string &line)
{
size_t p = 0;
while (p < line.size() && std::isspace(static_cast<unsigned char>(line[p])))
++p;
if (p >= line.size() || line[p] != '#')
return { };
++p;
while (p < line.size() && std::isspace(static_cast<unsigned char>(line[p])))
++p;
size_t kwStart = p;
while (p < line.size() && std::isalpha(static_cast<unsigned char>(line[p])))
++p;
return line.substr(kwStart, p - kwStart);
}
// Elides the entire content of any #if QT_*_REMOVED_SINCE(...) block by
// replacing all lines in the block — including the #if and #endif lines —
// with blank lines (newlines are preserved so offsets are unaffected).
//
// Rationale: QT_*_REMOVED_SINCE guards wrap API that has been removed from
// the compiled library. A text-based scanner sees those declarations even
// though they will never be compiled, which causes the generator to emit
// fuzz calls for non-existent symbols. Eliding the blocks before any
// further parsing prevents those false matches.
//
// Nested #if/#endif blocks are handled correctly via a depth counter.
std::string elideRemovedSinceBlocks(const std::string &source)
{
std::string result;
result.reserve(source.size());
size_t i = 0;
while (i < source.size()) {
// Grab one line (not including the trailing '\n').
size_t lineStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t lineEnd = i;
bool hasNewline = (i < source.size());
if (hasNewline)
++i; // consume '\n'
std::string line = source.substr(lineStart, lineEnd - lineStart);
std::string kw = ppKeyword(line);
// Detect #if / #ifdef / #ifndef lines that mention _REMOVED_SINCE.
bool isRemovedIf = (kw == "if" || kw == "ifdef" || kw == "ifndef") &&
line.find("_REMOVED_SINCE") != std::string::npos;
if (isRemovedIf) {
// Blank out the opening #if line.
result.append(lineEnd - lineStart, ' ');
if (hasNewline)
result += '\n';
// Consume lines until the matching #endif, tracking nesting.
int depth = 1;
while (i < source.size() && depth > 0) {
size_t bStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t bEnd = i;
bool bHasNewline = (i < source.size());
if (bHasNewline)
++i;
std::string bline = source.substr(bStart, bEnd - bStart);
std::string bkw = ppKeyword(bline);
if (bkw == "if" || bkw == "ifdef" || bkw == "ifndef")
++depth;
else if (bkw == "endif")
--depth;
// Blank out every line inside the removed block.
result.append(bEnd - bStart, ' ');
if (bHasNewline)
result += '\n';
}
} else {
result += line;
if (hasNewline)
result += '\n';
}
}
return result;
}
// Elide method declarations inside blocks guarded exclusively by non-Linux
// platform macros (Q_OS_WIN*, Q_OS_MAC*, Q_OS_IOS, Q_OS_ANDROID, …).
//
// The heuristic: if the opening #if/#ifdef/#ifndef line mentions one of the
// non-Linux OS macros AND does NOT also mention Q_OS_LINUX or Q_OS_UNIX,
// blank the entire block (including the guard lines). This prevents methods
// like QProcess::nativeArguments() (Windows-only) from being emitted.
std::string elideNonLinuxPlatformBlocks(const std::string &source)
{
static const std::regex kNonLinuxOs(
R"(\bQ_OS_(WIN\w*|WINDOWS|MAC\w*|DARWIN|IOS|TVOS|WATCHOS|VISIONOS|ANDROID\w*|WASM|VXWORKS|INTEGRITY|QNX)\b)",
std::regex::optimize);
static const std::regex kLinuxOs(
R"(\bQ_OS_(LINUX|UNIX)\b)",
std::regex::optimize);
std::string result;
result.reserve(source.size());
size_t i = 0;
while (i < source.size()) {
size_t lineStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t lineEnd = i;
bool hasNewline = (i < source.size());
if (hasNewline)
++i;
std::string line = source.substr(lineStart, lineEnd - lineStart);
std::string kw = ppKeyword(line);
bool isIfLike = (kw == "if" || kw == "ifdef" || kw == "ifndef");
bool mentionsNonLinux = isIfLike && std::regex_search(line, kNonLinuxOs);
bool mentionsLinux = isIfLike && std::regex_search(line, kLinuxOs);
if (mentionsNonLinux && !mentionsLinux) {
// Blank the opening guard line.
result.append(lineEnd - lineStart, ' ');
if (hasNewline)
result += '\n';
int depth = 1;
while (i < source.size() && depth > 0) {
size_t bStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t bEnd = i;
bool bHasNewline = (i < source.size());
if (bHasNewline)
++i;
std::string bline = source.substr(bStart, bEnd - bStart);
std::string bkw = ppKeyword(bline);
if (bkw == "if" || bkw == "ifdef" || bkw == "ifndef")
++depth;
else if (bkw == "endif")
--depth;
result.append(bEnd - bStart, ' ');
if (bHasNewline)
result += '\n';
}
} else {
result += line;
if (hasNewline)
result += '\n';
}
}
return result;
}
// Elide blocks guarded exclusively by Q_OS_UNIX or Q_OS_LINUX so that
// Unix/Linux-only methods (e.g. QProcess::childProcessModifier()) are not
// emitted in generated fuzz tests that must compile on all platforms.
// A block is considered Unix-only when the guard line mentions Q_OS_UNIX or
// Q_OS_LINUX but does NOT also mention any other OS macro (Q_OS_WIN*, etc.).
std::string elideUnixOnlyBlocks(const std::string &source)
{
static const std::regex kUnixOs(
R"(\bQ_OS_(UNIX|LINUX)\b)",
std::regex::optimize);
static const std::regex kNonUnixOs(
R"(\bQ_OS_(WIN\w*|WINDOWS|MAC\w*|DARWIN|IOS|TVOS|WATCHOS|VISIONOS|ANDROID\w*|WASM|VXWORKS|INTEGRITY|QNX)\b)",
std::regex::optimize);
std::string result;
result.reserve(source.size());
size_t i = 0;
while (i < source.size()) {
size_t lineStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t lineEnd = i;
bool hasNewline = (i < source.size());
if (hasNewline)
++i;
std::string line = source.substr(lineStart, lineEnd - lineStart);
std::string kw = ppKeyword(line);
bool isIfLike = (kw == "if" || kw == "ifdef" || kw == "ifndef");
bool mentionsUnix = isIfLike && std::regex_search(line, kUnixOs);
bool mentionsOther = isIfLike && std::regex_search(line, kNonUnixOs);
if (mentionsUnix && !mentionsOther) {
result.append(lineEnd - lineStart, ' ');
if (hasNewline)
result += '\n';
int depth = 1;
while (i < source.size() && depth > 0) {
size_t bStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t bEnd = i;
bool bHasNewline = (i < source.size());
if (bHasNewline)
++i;
std::string bline = source.substr(bStart, bEnd - bStart);
std::string bkw = ppKeyword(bline);
if (bkw == "if" || bkw == "ifdef" || bkw == "ifndef")
++depth;
else if (bkw == "endif")
--depth;
result.append(bEnd - bStart, ' ');
if (bHasNewline)
result += '\n';
}
} else {
result += line;
if (hasNewline)
result += '\n';
}
}
return result;
}
// Elide blocks guarded by a simple '#ifdef X' where X is an optional
// compile-time feature macro that may not be defined in a standard Linux
// Qt desktop build (e.g. QT_KEYPAD_NAVIGATION, Q_QDOC, QT_NO_WHEELEVENT).
//
// In a standard desktop build, '#ifdef QT_KEYPAD_NAVIGATION' is inactive,
// so methods like setNavigationMode() inside that block do not exist and
// must not appear in generated fuzz tests.
//
// Safe macros (NOT elided):
// Q_OS_LINUX, Q_OS_UNIX — Linux-platform code; already handled upstream.
// QT_DEPRECATED_SINCE, QT_REMOVED_SINCE — handled by elideRemovedSinceBlocks.
//
// Only positive '#ifdef' guards are elided. '#ifndef QT_NO_FEATURE' blocks
// are kept because in a standard Qt build those features ARE present and
// the enclosed methods DO exist.
std::string elideOptionalIfdefBlocks(const std::string &source)
{
static const std::regex kSafeIfdef(
R"(\b(Q_OS_LINUX|Q_OS_UNIX|QT_DEPRECATED_SINCE|QT_REMOVED_SINCE)\b)",
std::regex::optimize);
std::string result;
result.reserve(source.size());
size_t i = 0;
while (i < source.size()) {
size_t lineStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t lineEnd = i;
bool hasNewline = (i < source.size());
if (hasNewline)
++i;
std::string line = source.substr(lineStart, lineEnd - lineStart);
std::string kw = ppKeyword(line);
// Only elide simple '#ifdef X' blocks (not '#ifndef', '#if expr').
bool isIfdef = (kw == "ifdef");
bool isSafe = isIfdef && std::regex_search(line, kSafeIfdef);
if (isIfdef && !isSafe) {
// Blank the opening #ifdef line and everything through #endif.
result.append(lineEnd - lineStart, ' ');
if (hasNewline)
result += '\n';
int depth = 1;
while (i < source.size() && depth > 0) {
size_t bStart = i;
while (i < source.size() && source[i] != '\n')
++i;
size_t bEnd = i;
bool bHasNewline = (i < source.size());
if (bHasNewline)
++i;
std::string bline = source.substr(bStart, bEnd - bStart);
std::string bkw = ppKeyword(bline);
if (bkw == "if" || bkw == "ifdef" || bkw == "ifndef")
++depth;
else if (bkw == "endif")
--depth;
result.append(bEnd - bStart, ' ');
if (bHasNewline)
result += '\n';
}
} else {
result += line;
if (hasNewline)
result += '\n';
}
}
return result;
}
// Strip preprocessor directives (#if, #ifdef, #ifndef, #else, #elif,
// #endif, #define, #undef) from a string, replacing the directive line
// with spaces (preserving newlines so line counts are unaffected).
// This prevents macro names like QT_CONFIG, QT_CORE_REMOVED_SINCE, and
// `defined` from appearing as false method-name matches.
std::string stripPreprocessorLines(const std::string &source)
{
std::string result;
result.reserve(source.size());
size_t i = 0;
while (i < source.size()) {
// Check for '#' at start of line (possibly preceded by whitespace).
while (i < source.size() && source[i] != '\n') {
if (source[i] == '#') {
// Replace the rest of this line with spaces.
while (i < source.size() && source[i] != '\n') {
result += ' ';
++i;
}
goto next_line;
} else if (!std::isspace(static_cast<unsigned char>(source[i]))) {
// Non-whitespace before '#' — not a preprocessor line.
break;
}
result += source[i++];
}
// Copy rest of line normally.
while (i < source.size() && source[i] != '\n')
result += source[i++];
next_line:
if (i < source.size()) result += source[i++]; // '\n'
}
return result;
}
// ---------------------------------------------------------------------------
// Q_OBJECT macro detection
// ---------------------------------------------------------------------------
// Returns true if the class body at classPos (opening '{') contains the
// Q_OBJECT macro at depth 1 (directly inside the class, not in a nested
// class or method body).
bool bodyHasQObjectMacro(const std::string &source, size_t classPos)
{
size_t pos = source.find('{', classPos);
if (pos == std::string::npos)
return false;
int depth = 0;
size_t i = pos;
while (i < source.size()) {
const char c = source[i];
if (c == '{') {
++depth;
} else if (c == '}') {
--depth;
if (depth == 0)
break;
}
if (depth == 1 && source.compare(i, 8, "Q_OBJECT") == 0) {
size_t after = i + 8;
if (after >= source.size()
|| (!std::isalnum(static_cast<unsigned char>(source[after]))
&& source[after] != '_')) {
return true;
}
}
++i;
}
return false;
}
// ---------------------------------------------------------------------------
// Pure-virtual detection and name extraction
// ---------------------------------------------------------------------------
std::string extractMethodName(const std::string &ctx)
{
if (ctx.empty())
return { };
int depth = 1;
size_t parenPos = std::string::npos;
for (size_t i = ctx.size() - 1; i > 0; --i) {
const char c = ctx[i - 1];
if (c == ')') {
++depth;
} else if (c == '(') {
--depth;
if (depth == 0) {
parenPos = i - 1;
break;
}
}
}
if (parenPos == std::string::npos || parenPos == 0)
return {};
size_t nameEnd = parenPos;
while (nameEnd > 0 && std::isspace(static_cast<unsigned char>(ctx[nameEnd - 1])))
--nameEnd;
if (nameEnd == 0)
return {};
size_t nameStart = nameEnd;
while (nameStart > 0 &&
(std::isalnum(static_cast<unsigned char>(ctx[nameStart - 1]))
|| ctx[nameStart - 1] == '_'))
--nameStart;
if (nameStart == nameEnd)
return {};
return ctx.substr(nameStart, nameEnd - nameStart);
}
std::unordered_set<std::string>
collectPureVirtualNames(const std::string &source, size_t classPos)
{
std::unordered_set<std::string> names;
size_t pos = source.find('{', classPos);
if (pos == std::string::npos)
return names;
static const std::regex kPureVirtualRe(
R"(\)\s*(?:(?:const|volatile|noexcept|override|final|Q_DECL_\w+)\s*)*=\s*0\s*[;)])",
std::regex::optimize);
int depth = 0;
bool inString = false;
bool inChar = false;
size_t i = pos;
size_t bodyStart = pos + 1;
while (i < source.size()) {
const char c = source[i];
if (!inChar && c == '"' && (i == 0 || source[i - 1] != '\\'))
inString = !inString;
else if (!inString && c == '\'' && (i == 0 || source[i - 1] != '\\'))
inChar = !inChar;
if (inString || inChar) {
++i;
continue;
}
if (c == '/' && i + 1 < source.size() && source[i + 1] == '/') {
i = source.find('\n', i);
if (i == std::string::npos)
break;
continue;
}
if (c == '/' && i + 1 < source.size() && source[i + 1] == '*') {
i = source.find("*/", i + 2);
if (i == std::string::npos)
break;
i += 2;
continue;
}
if (c == '{') {
++depth;
} else if (c == '}') {
--depth;
if (depth == 0)
break;
}
if (depth == 1 && c == ')') {
auto sub = source.substr(i, std::min<size_t>(64, source.size() - i));
if (std::regex_search(sub, kPureVirtualRe)) {
size_t lookbackStart = (i > bodyStart + 512) ? i - 512 : bodyStart;
std::string ctx = source.substr(lookbackStart, i - lookbackStart + 1);
std::string name = extractMethodName(ctx);
if (!name.empty())
names.insert(name);
}
}
++i;
}
return names;
}
bool bodyHasPureVirtual(const std::string &source, size_t classPos)
{
return !collectPureVirtualNames(source, classPos).empty();
}
std::unordered_set<std::string>
collectDeclaredMethodNames(const std::string &source, size_t classPos)
{
std::unordered_set<std::string> names;
size_t pos = source.find('{', classPos);
if (pos == std::string::npos)
return names;
std::string body;
int depth = 0;
size_t i = pos;
while (i < source.size()) {
const char c = source[i];
if (c == '{') {
++depth;
body += (depth > 1) ? ' ' : '{';
} else if (c == '}') {
--depth;
if (depth == 0)
break;
body += ' ';
} else if (depth >= 1) {
body += c;
}
++i;
}
static const std::regex kDeclRe(R"(\b([A-Za-z_]\w*)\s*\()", std::regex::optimize);
static const std::unordered_set<std::string> kSkip = {
"if", "for", "while", "switch", "return", "sizeof", "alignof",
"decltype", "static_assert", "Q_OBJECT", "Q_GADGET", "Q_PROPERTY",
"Q_DECLARE_FLAGS", "Q_DECLARE_PRIVATE", "Q_DISABLE_COPY",
"Q_INVOKABLE", "Q_REVISION", "Q_SLOTS", "Q_SIGNALS", "signals",
"slots", "emit", "connect", "disconnect", "override", "final",
"explicit", "virtual", "inline", "constexpr", "static", "operator",
};
auto it = std::sregex_iterator(body.begin(), body.end(), kDeclRe);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
std::string name = (*it)[1].str();
if (!kSkip.count(name))
names.insert(name);
}
return names;
}
// ---------------------------------------------------------------------------
// Public method signature extraction
// ---------------------------------------------------------------------------
std::string trim(const std::string &s)
{
size_t a = s.find_first_not_of(" \t\n\r");
if (a == std::string::npos)
return { };
size_t b = s.find_last_not_of(" \t\n\r");
return s.substr(a, b - a + 1);
}
std::vector<std::string> splitParams(const std::string ¶mList)
{
std::vector<std::string> result;
int anglDepth = 0;
int parenDepth = 0;
size_t start = 0;
for (size_t i = 0; i < paramList.size(); ++i) {
const char c = paramList[i];
if (c == '<') {
++anglDepth;
} else if (c == '>' && anglDepth > 0) {
--anglDepth;
} else if (c == '(') {
++parenDepth;
} else if (c == ')' && parenDepth > 0) {
--parenDepth;
} else if (c == ',' && anglDepth == 0 && parenDepth == 0) {
result.push_back(trim(paramList.substr(start, i - start)));
start = i + 1;
}
}
std::string last = trim(paramList.substr(start));
if (!last.empty())
result.push_back(last);
return result;
}
MethodParam parseParam(const std::string ¶m)
{
MethodParam mp;
size_t eqPos = param.find('=');
std::string decl = (eqPos != std::string::npos)
? trim(param.substr(0, eqPos))
: trim(param);
if (decl.empty())
return mp;
size_t nameEnd = decl.size();
while (nameEnd > 0 && std::isspace(static_cast<unsigned char>(decl[nameEnd - 1])))
--nameEnd;
if (nameEnd > 0 && decl[nameEnd - 1] == '>') {
mp.type = trim(decl);
return mp;
}
size_t nameStart = nameEnd;
while (nameStart > 0 &&
(std::isalnum(static_cast<unsigned char>(decl[nameStart - 1]))
|| decl[nameStart - 1] == '_'))
--nameStart;
if (nameStart == nameEnd) {
mp.type = trim(decl);
return mp;
}
std::string lastName = decl.substr(nameStart, nameEnd - nameStart);
// Qt parameter names never contain underscores — if the last token does,
// it is a trailing macro (e.g. QT6_DECL_NEW_OVERLOAD_TAIL). Strip it and
// retry so the real param name (e.g. "thread") is found instead.
while (lastName.find('_') != std::string::npos && nameStart > 0) {
nameEnd = nameStart;
while (nameEnd > 0 && std::isspace(static_cast<unsigned char>(decl[nameEnd - 1])))
--nameEnd;
nameStart = nameEnd;
while (nameStart > 0 &&
(std::isalnum(static_cast<unsigned char>(decl[nameStart - 1]))
|| decl[nameStart - 1] == '_'))
--nameStart;
if (nameStart == nameEnd)
break;
lastName = decl.substr(nameStart, nameEnd - nameStart);
}
static const std::unordered_set<std::string> kTypeKeywords = {
"const", "volatile", "unsigned", "signed", "long", "short",
"int", "char", "float", "double", "bool", "void", "auto",
"struct", "class", "enum", "typename", "inline", "explicit",
};
if (kTypeKeywords.count(lastName) || lastName.empty()) {
mp.type = trim(decl.substr(0, nameEnd));
return mp;
}
if (nameStart > 0) {
char prevChar = decl[nameStart - 1];
if (prevChar == ':') {
mp.type = trim(decl.substr(0, nameEnd));
return mp;
}
}
mp.name = lastName;
mp.type = trim(decl.substr(0, nameStart));
if (mp.type.empty() && nameStart > 0)
mp.type = "auto";
return mp;
}
// Returns true if the type is a Q* type that is likely forward-declared
// (and therefore cannot be default-constructed or referenced).
// We only allow Q* types that we know are complete value types.
static bool isLikelyForwardDeclared(const std::string &type)
{
// Strip const, ref, ptr, whitespace to get the base type name.
std::string t = type;
while (!t.empty() && (t.back() == '&' || t.back() == '*' || t.back() == ' '))
t.pop_back();
while (t.size() >= 6 && t.substr(0, 6) == "const ")
t = trim(t.substr(6));
// Qualified types and templates are not simple forward decls.
if (t.find("::") != std::string::npos)
return false;
if (t.find('<') != std::string::npos)
return false;
// Non-Q types are usually primitives or std types — not forward decls.
if (t.empty() || t[0] != 'Q')
return false;
if (t.size() < 2 || !std::isupper(static_cast<unsigned char>(t[1])))
return false;
// These Q* types are always complete (defined in standard Qt headers
// included transitively via qobject.h / qstring.h etc.)
static const std::unordered_set<std::string> kAlwaysComplete = {
"QObject", "QWidget",
"QString", "QByteArray", "QVariant",
"QUrl", "QChar", "QStringList",
"QPoint", "QPointF", "QSize", "QSizeF",
"QRect", "QRectF", "QLine", "QLineF",
"QDate", "QTime", "QDateTime",
"QModelIndex", "QPersistentModelIndex",
"QLocale", "QMargins", "QMarginsF",
"QVector2D", "QVector3D", "QVector4D",
"QTimeZone",
};
return !kAlwaysComplete.count(t);
}
static const std::unordered_set<std::string> kDeclKeywords = {
"virtual", "inline", "static", "explicit", "constexpr", "consteval",
"constinit", "override", "final", "noexcept", "const", "volatile",
"Q_INVOKABLE", "Q_REVISION",
"QT_DEPRECATED_VERSION_X_6_0", "QT_DEPRECATED_VERSION_X_6_1",
"QT_DEPRECATED_VERSION_X_6_2", "QT_DEPRECATED_VERSION_X_6_3",
"QT_DEPRECATED_VERSION_X_6_4", "QT_DEPRECATED_VERSION_X_6_5",
"QT_DEPRECATED_VERSION_X_6_6", "QT_DEPRECATED_VERSION_X_6_7",
"QT_DEPRECATED_VERSION_X_6_8", "QT_DEPRECATED_VERSION_X_6_9",
"Q_REQUIRED_RESULT", "Q_NODISCARD", "Q_DECL_COLD_FUNCTION",
"Q_WEAK_OVERLOAD", "Q_ALWAYS_INLINE",
};
// C++ keywords that must never be treated as method names.
static const std::unordered_set<std::string> kCppKeywords = {
// Common Qt/STL function/method names that appear in default argument
// expressions and can be mis-parsed as param types or method names.
"count", "size", "length", "indexOf", "data", "begin", "end",
"value", "key", "at", "get", "first", "last", "front", "back",
"top", "bottom", "left", "right", "width", "height", "x", "y", "z",
"defaultTypeFor", "interval", "remainingTime",
"bool", "int", "char", "float", "double", "void", "auto",
"long", "short", "unsigned", "signed",
"if", "else", "for", "while", "do", "switch", "case", "break",
"continue", "return", "goto", "default",
"class", "struct", "union", "enum", "namespace", "template",
"typename", "typedef", "using", "static", "extern", "register",
"const", "volatile", "mutable", "constexpr", "consteval",
"inline", "virtual", "explicit", "friend", "operator",
"new", "delete", "throw", "try", "catch", "noexcept",
"public", "protected", "private",
"true", "false", "nullptr", "this",
"sizeof", "alignof", "decltype", "typeof",
"override", "final",
// Preprocessor-related identifiers that can appear after macro expansion
// or inside #if blocks that weren't fully stripped.
"defined",
};
// Method names that are known to be unsafe or unproductive for fuzzing.
static const std::unordered_set<std::string> kSkippedMethodNames = {
"metaObject", "qt_metacall", "qt_metacast",
"qt_check_for_QGADGET_macro", "qt_static_metacall",
"deleteLater", "destroy",
"winId", "effectiveWinId", "internalWinId",
"nativeInterface", "handle", "platformInterface",
"windowHandle", "platformWindow",
"paintEngine", "sharedPainter", "redirected", "initPainter",
"legacyDefaultTypeForOs",
// Specific method names known to cause issues.
"panel", // lowercase enum value leaking as method name in QFrame
"pointer", // lowercase enum value / protected member leaking in QMenu
// macOS-only methods that are exposed in headers but not available on Linux.
"toNSMenu", "setAsDockMenu", "toNSToolBar",
// setTimeZone: zero-param false match handled by post-processing below.
// Platform-specific methods (mobile/embedded) exposed by preprocessor stripping.
"hasEditFocus", "setEditFocus",
// Methods taking std::initializer_list — skip by name as a safety net.
"setTabOrder",
// Internal/private methods exposed by macro expansion.
"menuObject",
// Methods that take/return QTextDocument internals not available in the widget.
"setMetaInformation", "metaInformation",
// Methods that would require complex setup not worth fuzzing directly.
"addButton", "button", "rootIndex", "setRootIndex", "setCurrentModelIndex",
// These return types that are forward-declared and not fully defined
// in the class header — calling them triggers incomplete-type errors.
"headerTextFormat", "weekdayTextFormat", "dateTextFormat",
"dateEditFormat",
// QPainterPath is forward-declared in qtransform.h
"selectionArea", "clipPath", "shape", "opaqueArea",
// QPalette is forward-declared in qmetatype.h
"palette",
// QRegion is forward-declared in various headers
"visibleRegion", "childrenRegion",
// QFont is forward-declared in various headers
"font", "defaultFont",
// QCursor is forward-declared in various headers
"cursor",
};
std::vector<MethodSignature>
collectPublicMethods(const std::string &stripped,
size_t classPos,
const std::string &className)
{
std::vector<MethodSignature> result;
size_t pos = stripped.find('{', classPos);
if (pos == std::string::npos)
return result;
// Collect depth-1 body text.
// Rules:
// depth == 1 (class body): characters are copied verbatim.
// Opening '{' at depth 1 → 2 (method/nested-class body begins):
// replace with ' '.
// Characters at depth > 1 (inside method bodies, nested classes, etc.):
// replace with ' '. This prevents names used in inline implementations
// (e.g. d.constData() inside size()) from being matched as method names.
// Closing '}' at depth 2 → 1 (method body ends):
// replace with ';' so the backward return-type scan has a clear
// statement boundary even for inline methods that have no explicit ';'.
// Closing '}' at depth > 2 → still inside a body: replace with ' '.
//
// Then:
// 1. Elide QT_*_REMOVED_SINCE blocks so removed API is never parsed.
// 2. Strip remaining preprocessor directive lines.
std::string body;
{
int depth = 0;
size_t i = pos;
while (i < stripped.size()) {
const char c = stripped[i];
if (c == '{') {
++depth;
body += (depth > 1) ? ' ' : '{';
} else if (c == '}') {
--depth;
if (depth == 0)
break;
// depth==1: closed a method body → synthetic statement boundary
// depth >1: closed a brace nested inside a method body → space
body += (depth == 1) ? ';' : ' ';
} else if (depth == 1) {
// Strip // line comments — their text (e.g. "min()" in
// "suppress bogus reading of min() as a macro", or
// "Feature (\w+)calendar…") must not be parsed as method calls.
if (c == '/' && i + 1 < stripped.size() && stripped[i + 1] == '/') {
// Replace from here to end-of-line with spaces.
while (i < stripped.size() && stripped[i] != '\n') {
body += ' ';
++i;
}
continue; // the \n (if any) will be handled on the next iteration
}
body += c; // class-level declaration text
} else {
body += ' '; // method-body content → suppress
}
++i;
}
}
body = elideRemovedSinceBlocks(body);
body = elideNonLinuxPlatformBlocks(body);
body = elideUnixOnlyBlocks(body);
body = elideOptionalIfdefBlocks(body);
body = stripPreprocessorLines(body);
// Collect nested type names declared directly inside the class body.
//
// nestedStructs: simple value types (struct X) — qualify as ClassName::X
// and default-construct with {} in generated code.
// nestedClasses: complex types (class X — iterators, handles, etc.) —
// methods taking these as params are SKIPPED because we
// cannot trivially construct them from raw bytes.
// Enums (enum X, enum class X) get their :: added by the codegen's
// existing "upper-case nested type" path; no separate qualification needed.
std::unordered_set<std::string> nestedStructs;
std::unordered_set<std::string> nestedClasses;
{
static const std::regex kNestedStructRe(
R"(\bstruct\s+([A-Za-z_]\w*))",
std::regex::optimize);
static const std::regex kNestedClassRe(
R"(\bclass\s+([A-Za-z_]\w*))",
std::regex::optimize);
for (auto it = std::sregex_iterator(body.begin(), body.end(), kNestedStructRe);
it != std::sregex_iterator(); ++it)
nestedStructs.insert((*it)[1].str());
for (auto it = std::sregex_iterator(body.begin(), body.end(), kNestedClassRe);
it != std::sregex_iterator(); ++it)
nestedClasses.insert((*it)[1].str());
// typedef declarations: add the alias name to nestedClasses so that
// methods using private or pointer-aliased typedefs as params are
// skipped (e.g. QWinEventNotifier::HANDLE is typedef Qt::HANDLE HANDLE
// in the private section — inaccessible and non-castable from uint8_t).
// Exclude function-pointer typedefs (contain '(' before ';') to avoid
// catastrophic backtracking on headers like qopenglextrafunctions.h
// that have thousands of PFNGL* typedefs.
static const std::regex kTypedefRe(R"(\btypedef\b[^;(]*\s+(\w+)\s*;)",
std::regex::optimize);
for (auto it = std::sregex_iterator(body.begin(), body.end(), kTypedefRe);
it != std::sregex_iterator(); ++it)
nestedClasses.insert((*it)[1].str());
}
static const std::regex kAccessRe(
R"(\b(public|protected|private)\s*(?:Q_SLOTS|Q_SIGNALS|signals|slots)?\s*:)",
std::regex::optimize);
enum class Section { Private, Protected, Public, Signals, Slots };
struct SectionLabel {
size_t pos;
Section section;
};
std::vector<SectionLabel> sections;
{
auto it = std::sregex_iterator(body.begin(), body.end(), kAccessRe);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
std::string acc = (*it)[1].str();
std::string full = (*it)[0].str();
Section sec = Section::Private;
if (acc == "public") {
if (full.find("Q_SLOTS") != std::string::npos ||
full.find("slots") != std::string::npos)
sec = Section::Slots;
else if (full.find("Q_SIGNALS") != std::string::npos ||
full.find("signals") != std::string::npos)
sec = Section::Signals;
else
sec = Section::Public;
} else if (acc == "protected") {
sec = Section::Protected;
} else {
sec = Section::Private;
}
sections.push_back({ static_cast<size_t>((*it).position()), sec });
}
}
std::sort(sections.begin(), sections.end(),
[](const SectionLabel &a, const SectionLabel &b) {
return a.pos < b.pos;
});
auto sectionAt = [&](size_t p) -> Section {
Section cur = Section::Private;
for (const auto &lbl : sections) {
if (lbl.pos >= p)
break;
cur = lbl.section;
}
return cur;
};
static const std::regex kMethodCallRe(
R"(\b([A-Za-z_]\w*)\s*\()",
std::regex::optimize);
std::set<std::string> seen;
auto it = std::sregex_iterator(body.begin(), body.end(), kMethodCallRe);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
const std::smatch &m = *it;
std::string name = m[1].str();
size_t matchPos = static_cast<size_t>(m.position());
// Skip declaration keywords.
if (kDeclKeywords.count(name))
continue;
// Skip C++ keywords.
if (kCppKeywords.count(name))
continue;
// Must be in public or public Q_SLOTS section.
Section sec = sectionAt(matchPos);
if (sec != Section::Public && sec != Section::Slots)
continue;
// Skip constructor and destructor.
if (name == className)
continue;
// Skip Q_* macros.
if (name.size() >= 2 && name.substr(0, 2) == "Q_")
continue;
// Skip QT_* macros (QT_DEPRECATED_VERSION_X_*, QT7_ONLY, QT6_ONLY, …).
// The QT_ prefix check handles the common case; the underscore check
// catches variants like QT7_ONLY that start with QT<digit>_.
if (name.size() >= 3 && name.substr(0, 3) == "QT_")
continue;
// Skip any name containing '_': Qt public API method names never
// contain underscores — underscored identifiers are always macros.
if (name.find('_') != std::string::npos)
continue;
// Skip known problematic method names.
if (kSkippedMethodNames.count(name))
continue;
if (name == "emit" || name == "connect" || name == "disconnect")
continue;
// Skip Qt property binding accessors.
if (name.size() >= 8 && name.substr(0, 8) == "bindable")
continue;
// Determine context by examining what immediately precedes this word.
{
size_t i = matchPos;
while (i > 0 && std::isspace(static_cast<unsigned char>(body[i-1])))
--i;
if (i > 0) {
char prev = body[i-1];
if (prev == '(' || prev == '=' || prev == ','
|| prev == '!' || prev == '+' || prev == '-'
|| prev == '|' || prev == '<' || prev == '~'
|| prev == '^' || prev == '%' || prev == ':')
continue;
if (std::isalnum(static_cast<unsigned char>(prev)) || prev == '_') {
size_t wordEnd = i;
while (i > 0 && (std::isalnum(static_cast<unsigned char>(body[i-1]))
|| body[i-1] == '_'))
--i;
std::string preceding = body.substr(i, wordEnd - i);
static const std::unordered_set<std::string> kExprKeywords = {
"return", "new", "delete", "throw", "emit",
"case", "sizeof", "alignof", "decltype", "typeof",
"co_return", "co_yield",
};
if (kExprKeywords.count(preceding))
continue;
// If the preceding word is a C++ type qualifier, the
// matched identifier is a TYPE NAME, not a method name.
// e.g. "const Byte (&data)[]" — Byte is a type, not a
// method; the '(' belongs to the reference declarator.
static const std::unordered_set<std::string> kTypeQualifiers = {
"const", "volatile", "unsigned", "signed",
};
if (kTypeQualifiers.count(preceding))
continue;
// If the preceding word is itself preceded by '::',
// the full context is a scoped type used as an expression
// (e.g. std::chrono::milliseconds(...)) — not a declaration.
if (i >= 2) {
size_t j = i;
while (j > 0 && std::isspace(static_cast<unsigned char>(body[j-1])))
--j;
if (j >= 2 && body[j-1] == ':' && body[j-2] == ':')
continue;
}
}
}
}
// Skip operator overloads.
{
size_t i = matchPos;
while (i > 0 && std::isspace(static_cast<unsigned char>(body[i-1])))
--i;
if (i >= 8 && body.substr(i - 8, 8) == "operator")
continue;
}
// Skip template and friend declarations: scan backward from matchPos
// to the previous statement boundary (';', '{', '}', lone ':').
// If the preceding text contains "template" → template method (skip:
// we cannot construct its arguments from raw bytes).
// If it contains "friend" → friend function declared in class body
// (skip: it is NOT a member method, e.g. friend size_t qHash(...)).
{
size_t scanPos = matchPos;
while (scanPos > 0) {
char c2 = body[scanPos - 1];
if (c2 == ';' || c2 == '{' || c2 == '}')
break;
if (c2 == ':') {
// '::' — scope resolution, skip both colons and continue.
if (scanPos >= 2 && body[scanPos - 2] == ':')
{ scanPos -= 2; continue; }
break; // lone ':' — access label
}
--scanPos;
}
static const std::regex kTplRe(R"(\btemplate\s*<)", std::regex::optimize);
static const std::regex kFriendRe(R"(\bfriend\b)", std::regex::optimize);
std::string before = body.substr(scanPos, matchPos - scanPos);
if (std::regex_search(before, kTplRe) || std::regex_search(before, kFriendRe))
continue;
}
// Find the full parameter list.
size_t parenOpen = matchPos + m[0].length() - 1;
size_t parenClose = std::string::npos;
{
int depth = 1;
for (size_t i = parenOpen + 1; i < body.size(); ++i) {
if (body[i] == '(') {
++depth;
} else if (body[i] == ')') {
--depth;
if (depth == 0) {
parenClose = i;
break;
}
}
}
}
if (parenClose == std::string::npos)
continue;
std::string paramList = trim(body.substr(parenOpen + 1,
parenClose - parenOpen - 1));
// Raw check for QPrivateSignal BEFORE parsing — signals have it as
// the last param and it must be skipped at the raw string level
// because parseParam may not see it clearly after stripping defaults.
if (paramList.find("QPrivateSignal") != std::string::npos)
continue;
// Skip variadic template methods (Args&&..., typename... etc.)
if (paramList.find("...") != std::string::npos)
continue;
// Skip pure virtuals.
{
size_t after = parenClose + 1;
while (after < body.size() && std::isspace(static_cast<unsigned char>(body[after])))
++after;
static const std::regex kTrailRe(
R"(^(?:(?:const|noexcept|override|final|Q_DECL_\w+)\s*)*(=\s*0))",
std::regex::optimize);
std::string afterStr = body.substr(after, std::min<size_t>(64, body.size() - after));
if (std::regex_search(afterStr, kTrailRe))
continue;
}
// Check if const method.
bool isConst = false;
{
size_t after = parenClose + 1;
while (after < body.size() && std::isspace(static_cast<unsigned char>(body[after])))
++after;
static const std::regex kConstRe(R"(^const\b)", std::regex::optimize);
std::string afterStr = body.substr(after, std::min<size_t>(8, body.size() - after));
isConst = std::regex_search(afterStr, kConstRe);
}
// Parse parameter list.
std::vector<MethodParam> params;
bool anyDefault = false;
bool skipMethod = false;
if (!paramList.empty() && paramList != "void") {
for (const auto &rawParam : splitParams(paramList)) {
if (rawParam.empty())
continue;
// Raw string checks on each param before parsing.
if (rawParam.find("QPrivateSignal") != std::string::npos) {
skipMethod = true; break;
}
// Skip function pointer params.
if (rawParam.find("(*") != std::string::npos) {
skipMethod = true; break;
}
// Skip initializer_list params — cannot be cast from a byte.
if (rawParam.find("initializer_list") != std::string::npos) {
skipMethod = true; break;
}
// Skip params containing 'Func' or 'Callback' (function pointer typedefs).
{
bool hasEq = rawParam.find('=') != std::string::npos;
std::string declPart = hasEq
? trim(rawParam.substr(0, rawParam.find('=')))
: rawParam;
if (declPart.find("Func") != std::string::npos
|| declPart.find("func") != std::string::npos
|| declPart.find("Callback") != std::string::npos) {
skipMethod = true; break;
}
}
bool hasEq = rawParam.find('=') != std::string::npos;
if (hasEq)
anyDefault = true;
MethodParam mp = parseParam(rawParam);
// If the parsed type contains '(' or ')' it means a complex
// expression (template instantiation, function call in default
// arg, etc.) leaked into the type — skip the whole method.
if (mp.type.find('(') != std::string::npos
|| mp.type.find(')') != std::string::npos) {
skipMethod = true;
break;
}
// Skip template methods: if the base type (stripped of
// const/&/*/whitespace) is a single uppercase letter it is a
// template type parameter (T, U, V …) — not a real type.
// Such overloads cannot be called from generated code.
{
std::string base = mp.type;
while (!base.empty()
&& (base.back() == '&' || base.back() == '*'
|| base.back() == ' '))
base.pop_back();
if (base.size() >= 6 && base.substr(0, 6) == "const ")
base = base.substr(6);
while (!base.empty() && base.back() == ' ')
base.pop_back();
if (base.size() == 1
&& std::isupper(static_cast<unsigned char>(base[0]))) {
skipMethod = true;
break;
}
}
// If the raw param had no '=' (not a default arg) but the
// parsed type equals the full raw param (no name extracted),
// and the type looks like "Namespace::EnumValue" (scoped but
// no known type suffix), this is likely a leaked enum value
// from a macro-stripped default arg. Skip the method.
if (!hasEq && mp.name.empty() && mp.type.find("::") != std::string::npos
&& mp.type.find('<') == std::string::npos) {
// A real type like Qt::FocusReason would have a param name.
// A leaked value like "Qt::OtherFocusReason" has no name.
skipMethod = true;
break;
}
// Detect non-const reference params — these require a named
// local variable in the generated code (can't bind rvalue).
{
std::string t = mp.type;
while (!t.empty() && t.back() == ' ')
t.pop_back();
bool isRef = !t.empty() && t.back() == '&';
bool isConst = t.find("const ") != std::string::npos
|| t.find(" const") != std::string::npos;
mp.isNonConstRef = isRef && !isConst;
// Skip methods that take a reference to a likely
// forward-declared type — we cannot construct it.
if (isRef && isLikelyForwardDeclared(t)) {
skipMethod = true;
break;
}
}
// Qualify nested types: if the base type name (stripped of
// const/&/*/whitespace) matches a type declared inside this
// class body (e.g. YearMonthDay in QCalendar), prepend
// "ClassName::" so the generated code compiles.
// Only qualify if the type doesn't already have a '::'.
if (mp.type.find("::") == std::string::npos) {
std::string base = mp.type;
while (!base.empty()
&& (base.back() == '&' || base.back() == '*'
|| base.back() == ' '))
base.pop_back();
const std::string constPfx = "const ";
if (base.size() >= constPfx.size()
&& base.substr(0, constPfx.size()) == constPfx)
base = base.substr(constPfx.size());
while (!base.empty() && base.back() == ' ')
base.pop_back();
// Nested class types (iterators, handles …) cannot be
// trivially constructed from raw bytes — skip the method.
if (!base.empty() && nestedClasses.count(base)) {
skipMethod = true;
break;
}
// Nested struct types are simple value types — qualify
// them so the generated code compiles:
// e.g. YearMonthDay → QCalendar::YearMonthDay
if (!base.empty() && nestedStructs.count(base)) {
// Replace bare 'base' with 'ClassName::base' in the type.
size_t p = mp.type.find(base);
while (p != std::string::npos) {
// Only replace whole-word occurrences.
bool leftOk = (p == 0 || !std::isalnum(static_cast<unsigned char>(mp.type[p-1])));
bool rightOk = (p + base.size() >= mp.type.size()
|| !std::isalnum(static_cast<unsigned char>(mp.type[p+base.size()])));
if (leftOk && rightOk) {
mp.type.replace(p, base.size(), className + "::" + base);
mp.isNestedStruct = true;
break; // type name appears at most once
}
p = mp.type.find(base, p + 1);
}
}
}
params.push_back(mp);
}
}
if (skipMethod)
continue;
// If the param list was non-empty but we ended up with zero params,
// all params were individually rejected (e.g. all were forward-declared
// refs). The method cannot be called with zero args — skip it.
if (params.empty() && !paramList.empty() && paramList != "void")
continue;
// Build unique key.
std::string key = name + "(";
for (const auto &p : params)
key += p.type + ",";
key += ")";
if (!seen.insert(key).second)
continue;
// Extract the raw return type by scanning backward from matchPos.
// Stop at ';', '{', '}', or a lone ':' (not part of '::').
// This gives us the text between the previous statement boundary
// and the method name, from which we strip modifier keywords to
// recover the return type.
std::string rawReturnType;
{
size_t scanPos = matchPos;
while (scanPos > 0) {
char c = body[scanPos - 1];
if (c == ';' || c == '{' || c == '}')
break;
if (c == ':') {
// '::' (scope resolution) — skip both colons and continue.
if (scanPos >= 2 && body[scanPos - 2] == ':') {
scanPos -= 2;
continue;
}
// Lone ':' — access label boundary.
break;
}
--scanPos;
}
if (scanPos < matchPos) {
std::string decl = body.substr(scanPos, matchPos - scanPos);
// Trim leading/trailing whitespace.
size_t first = decl.find_first_not_of(" \t\n\r");
if (first != std::string::npos) {
size_t last = decl.find_last_not_of(" \t\n\r");
decl = decl.substr(first, last - first + 1);
// Strip modifier keywords that precede the return type.
static const std::vector<std::string> kReturnMods = {
"[[nodiscard]]", "Q_REQUIRED_RESULT", "Q_INVOKABLE",
"Q_SLOT", "Q_SIGNAL", "Q_SCRIPTABLE", "QT6_ONLY",
"virtual", "static", "inline", "explicit",
"constexpr", "consteval", "constinit",
};
for (const auto &mod : kReturnMods) {
size_t p;
while ((p = decl.find(mod)) != std::string::npos)
decl.replace(p, mod.size(), " ");
}
// Normalise whitespace.
std::string norm;
bool lastWasSpace = true;
for (char ch : decl) {
if (std::isspace(static_cast<unsigned char>(ch))) {
if (!lastWasSpace) {
norm += ' ';
lastWasSpace = true;
}
} else {
norm += ch;
lastWasSpace = false;
}
}
while (!norm.empty() && norm.back() == ' ')
norm.pop_back();
rawReturnType = norm;
}
}
}
MethodSignature sig;
sig.name = name;
sig.returnType = rawReturnType;
sig.params = std::move(params);
sig.isConst = isConst;
sig.hasDefault = anyDefault;
result.push_back(std::move(sig));
}
// Post-process: remove 0-param entries when a same-named entry with
// params also exists. These are false 0-param matches from macro-stripped
// headers that coexist with the real signature.
{
std::unordered_set<std::string> namesWithParams;
for (const auto &s : result) {
if (!s.params.empty())
namesWithParams.insert(s.name);
}
result.erase(
std::remove_if(result.begin(), result.end(),
[&](const MethodSignature &s) {
return s.params.empty() && namesWithParams.count(s.name);
}),
result.end());
}
return result;
}
// ---------------------------------------------------------------------------
// Constructor detection
// ---------------------------------------------------------------------------
bool isNotAConstructor(const std::string &body, size_t matchStart)
{
if (matchStart == 0)
return false;
size_t i = matchStart;
while (i > 0 && std::isspace(static_cast<unsigned char>(body[i - 1])))
--i;
if (i == 0)
return false;
if (std::isalnum(static_cast<unsigned char>(body[i - 1])) || body[i - 1] == '_') {
size_t wordEnd = i;
while (i > 0 && (std::isalnum(static_cast<unsigned char>(body[i - 1]))
|| body[i - 1] == '_'))
--i;
std::string word = body.substr(i, wordEnd - i);
if (word == "explicit" || word == "virtual" || word == "inline"
|| word == "constexpr" || word == "Q_INVOKABLE")
return false;
return true;
}
const char prev = body[i - 1];
return prev == '(' || prev == '~';
}
bool bodyHasDefaultConstructor(const std::string &source,
size_t classPos,
const std::string &className)
{
size_t pos = source.find('{', classPos);
if (pos == std::string::npos)
return true;
std::string body;
int depth = 0;
size_t i = pos;
while (i < source.size()) {
const char c = source[i];
if (c == '{') {
++depth;
body += (depth > 1) ? ' ' : '{';
} else if (c == '}') {
if (depth == 0)
break;
--depth;
if (depth == 0)
break;
body += (depth == 1) ? ';' : ' ';
} else if (depth == 1) {
// Strip // line comments — text like "// prevent construction"
// before "ClassName()" must not confuse isNotAConstructor() into
// thinking the word immediately before the ctor name is a regular
// identifier (which would make it look like a non-constructor call).
if (c == '/' && i + 1 < source.size() && source[i + 1] == '/') {
while (i < source.size() && source[i] != '\n') {
body += ' ';
++i;
}
continue;
}
// Strip preprocessor directives (#ifdef, #else, #endif, etc.).
// Without this, a constructor preceded by e.g. "#ifdef Q_QDOC"
// causes isNotAConstructor() to see "Q_QDOC" as the word before
// the constructor name and incorrectly reject it as non-constructor,
// leading to the class being marked as default-constructible when
// it is not (e.g. QApplication).
if (c == '#') {
while (i < source.size() && source[i] != '\n') {
body += ' ';
++i;
}
continue;
}
body += c;
} else {
body += ' ';
}
++i;
}
static const std::regex kAccessRe(
R"(\b(public|protected|private)\s*(?:Q_SLOTS|Q_SIGNALS|signals|slots)?\s*:)",
std::regex::optimize);
struct AccessLabel { size_t pos; bool isPublic; };
std::vector<AccessLabel> labels;
{
auto it = std::sregex_iterator(body.begin(), body.end(), kAccessRe);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
labels.push_back({ static_cast<size_t>((*it).position()),
(*it)[1].str() == "public" });
}
}
const auto isPublicAt = [&](size_t p) -> bool {
bool cur = false;
for (const auto &lbl : labels) {
if (lbl.pos >= p)
break;
cur = lbl.isPublic;
}
return cur;
};
const std::regex ctorRe(
"\\b" + className + R"(\s*\(([^;{]*))",
std::regex::optimize);
bool foundPublicCtor = false;
bool foundAnyExplicitCtor = false;
auto it = std::sregex_iterator(body.begin(), body.end(), ctorRe);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
auto matchStart = static_cast<size_t>((*it).position());
if (isNotAConstructor(body, matchStart))
continue;
foundAnyExplicitCtor = true;
if (!isPublicAt(matchStart))
continue;
foundPublicCtor = true;
std::string args = (*it)[1].str();
auto last = args.find_last_not_of(" \t\n\r");
if (last == std::string::npos)
return true;
args = args.substr(0, last + 1);
// Strip trailing "= default" / "= delete" specifiers that follow the
// closing ')' (e.g. "Foo(const Foo &) = default" → capture includes
// "const Foo &) = default"; we must remove the trailer before parsing).
// A deleted constructor (= delete) is never callable, so skip it entirely.
bool isDeleted = false;
for (const char *trailer : { "= delete", "= default" }) {
const size_t tlen = std::strlen(trailer);
if (args.size() >= tlen && args.compare(args.size() - tlen, tlen, trailer) == 0) {
isDeleted = (std::strcmp(trailer, "= delete") == 0);
args.resize(args.size() - tlen);
last = args.find_last_not_of(" \t\n\r");
if (last == std::string::npos) {
args.clear();
break;
}
args = args.substr(0, last + 1);
break;
}
}
if (isDeleted)
continue; // deleted ctor cannot be default-constructed
if (!args.empty() && args.back() == ')')
args.pop_back();
last = args.find_last_not_of(" \t\n\r");
if (last == std::string::npos)
return true;
args = args.substr(0, last + 1);
if (args.empty())
return true;
bool allDefaulted = true;
std::istringstream ss(args);
std::string param;
while (std::getline(ss, param, ',')) {
if (param.find('=') == std::string::npos) {
allDefaulted = false;
break;
}
}
if (allDefaulted)
return true;
}
if (foundPublicCtor)
return false;
return !foundAnyExplicitCtor;
}
// ---------------------------------------------------------------------------
// Platform flag detection
// ---------------------------------------------------------------------------
static const std::unordered_map<std::string, Platform> kPlatformMacros = {
{ "Q_OS_LINUX", Platform::Linux }, { "Q_OS_UNIX", Platform::Linux },
{ "Q_OS_WIN", Platform::Windows }, { "Q_OS_WIN32", Platform::Windows },
{ "Q_OS_WIN64", Platform::Windows }, { "Q_OS_WINRT", Platform::Windows },
{ "Q_OS_MACOS", Platform::macOS }, { "Q_OS_IOS", Platform::iOS },
{ "Q_OS_TVOS", Platform::iOS }, { "Q_OS_WATCHOS", Platform::iOS },
{ "Q_OS_ANDROID", Platform::Android }, { "Q_OS_WASM", Platform::WASM },
};
Platform platformsInCondition(const std::string &condition)
{
Platform result = Platform::None;
for (const auto &[macro, flag] : kPlatformMacros) {
if (condition.find(macro) != std::string::npos)
result = result | flag;
}
return result;
}
Platform detectAvailablePlatforms(const std::string &stripped, size_t classPos)
{
std::vector<std::string> lines;
{
std::istringstream ss(stripped.substr(0, classPos));
std::string line;
while (std::getline(ss, line))
lines.push_back(line);
}
static const std::regex kIfRe(
R"(#\s*ifdef\b(.*))", std::regex::optimize);
static const std::regex kIfDefinedRe(
R"(#\s*if\b(.*))", std::regex::optimize);
static const std::regex kIfndefRe(
R"(#\s*ifndef\b(.*))", std::regex::optimize);
static const std::regex kEndifRe(
R"(#\s*endif\b)", std::regex::optimize);
int pendingEndifs = 0;
Platform accumulated = Platform::None;
bool foundAny = false;
for (auto it = lines.rbegin(); it != lines.rend(); ++it) {
size_t first = it->find_first_not_of(" \t");
if (first == std::string::npos)
continue;
const std::string trimmed = it->substr(first);
if (std::regex_search(trimmed, kEndifRe)) {
++pendingEndifs;
continue;
}
if (std::regex_search(trimmed, kIfndefRe)) {
if (pendingEndifs > 0)
--pendingEndifs;
continue;
}
std::smatch m;
const bool isIfdef = std::regex_search(trimmed, m, kIfRe);
const bool isIf = !isIfdef && std::regex_search(trimmed, m, kIfDefinedRe);
if (!isIfdef && !isIf)
continue;
if (pendingEndifs > 0) {
--pendingEndifs;
continue;
}
Platform flags = platformsInCondition(m[1].str());
if (flags != Platform::None) {
accumulated = accumulated | flags;
foundAny = true;
}
}
return foundAny ? accumulated : Platform::All;
}
// ---------------------------------------------------------------------------
// Class match extraction
// ---------------------------------------------------------------------------
struct ClassMatch {
std::string name;
std::vector<std::string> baseNames; // Q-prefixed bases only (may be empty)
size_t pos;
bool isExported = false; // true when a Q_..._EXPORT macro is present
std::string exportMacro; // e.g. "Q_LOTTIE_EXPORT" (empty if not exported)
};
bool isTemplateClass(const std::string &source, size_t classPos)
{
if (classPos == 0)
return false;
size_t i = classPos - 1;
while (i > 0 && std::isspace(static_cast<unsigned char>(source[i])))
--i;
if (source[i] != '>')
return false;
int depth = 0;
while (i > 0) {
const char c = source[i];
if (c == '>') {
++depth;
} else if (c == '<') {
--depth;
if (depth == 0)
break;
}
--i;
}
if (depth != 0)
return false;
size_t anglePos = i;
while (anglePos > 0 && std::isspace(static_cast<unsigned char>(source[anglePos - 1])))
--anglePos;
if (anglePos < 8)
return false;
return source.substr(anglePos - 8, 8) == "template";
}
std::vector<std::string> extractBaseNames(const std::string &baseListText)
{
static const std::regex kBaseRe(R"(\bQ[A-Z]\w*\b)", std::regex::optimize);
std::vector<std::string> result;
auto it = std::sregex_iterator(baseListText.begin(), baseListText.end(), kBaseRe);
auto end = std::sregex_iterator();
for (; it != end; ++it)
result.push_back((*it)[0].str());
return result;
}
// Extracts all Q-prefixed class declarations from a stripped header source.
// Unlike the former extractQObjectSubclassMatches, this function accepts
// classes with any inheritance (including none) — there is no requirement
// for a QObject ancestor.
std::vector<ClassMatch> extractClassMatches(const std::string &stripped)
{
// Match classes with optional inheritance:
// class [EXPORT_MACRO] ClassName [: bases] {
// Group 1: export macro (e.g. Q_CORE_EXPORT) — present on public API classes.
// Group 2: ClassName (required, must start with uppercase)
// Group 3: base list (optional, captured after ':')
// (?!::) rejects scoped definitions like "class QMetaObject::Connection {".
static const std::regex kClassRe(
R"(\bclass\s+(Q_\w+_EXPORT\s+)?([A-Z]\w*)(?!::)\s*(?::([^{;]*))?(?=\{))",
std::regex::optimize);
std::vector<ClassMatch> result;
std::set<std::string> seen;
auto it = std::sregex_iterator(stripped.begin(), stripped.end(), kClassRe);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
const std::smatch &m = *it;
bool isExp = m[1].matched && !m[1].str().empty();
std::string exportMacroStr;
if (isExp) {
exportMacroStr = m[1].str();
// trim trailing whitespace
while (!exportMacroStr.empty()
&& std::isspace(static_cast<unsigned char>(exportMacroStr.back())))
exportMacroStr.pop_back();
}
std::string name = m[2].str();
if (name.empty() || name[0] != 'Q')
continue;
if (name.size() > 7 && name.substr(name.size() - 7) == "Private")
continue;
if (name.size() > 4 && name.substr(name.size() - 4) == "Impl")
continue;
if (name.find("_p") != std::string::npos)
continue;
size_t pos = static_cast<size_t>(m.position());
if (isTemplateClass(stripped, pos))
continue;
if (!seen.insert(name).second)
continue;
// Extract Q-prefixed base names for abstract propagation.
// The base list may be empty for standalone classes (e.g. QPoint).
std::vector<std::string> bases;
if (m[3].matched)
bases = extractBaseNames(m[3].str());
result.push_back({ name, bases, pos, isExp, exportMacroStr });
}
return result;
}
std::string firstComponent(const fs::path &rel)
{
if (rel.empty())
return { };
return rel.begin()->string();
}
// ---------------------------------------------------------------------------
// Universe construction — all Q-prefixed classes
// ---------------------------------------------------------------------------
struct UniverseEntry {
std::vector<std::string> baseNames;
bool isAbstract = false;
bool isDefaultConstructible = true;
bool hasQObject = false;
Platform availableOn = Platform::All;
std::unordered_set<std::string> ownPureVirtualNames;
std::unordered_set<std::string> ownDeclaredNames;
std::vector<MethodSignature> publicMethods;
};
// Scans all public headers under srcRoot and builds a map of every
// Q-prefixed class encountered. No ancestry filter is applied — all
// Q-prefixed classes, regardless of their base classes, are admitted.
// When includeQmlPrivate is true, _p.h / /private/ files that contain
// QML macros are also scanned.
std::unordered_map<std::string, UniverseEntry>
buildUniverseMap(const fs::path &srcRoot, bool verbose, bool includeQmlPrivate = false)
{
std::unordered_map<std::string, UniverseEntry> universe;
std::set<std::string> seen;
std::error_code ec;
for (const auto &entry : fs::recursive_directory_iterator(srcRoot, ec)) {
if (ec) {
ec.clear();
continue;
}
if (!entry.is_regular_file())
continue;
const fs::path &p = entry.path();
if (p.extension() != ".h")
continue;
{
std::string pathStr = p.string();
bool isPrivatePath = pathStr.find("/private/") != std::string::npos;
bool isPrivateHeader = pathStr.find("_p.h") != std::string::npos;
bool isPrivateName = pathStr.find("private.h") != std::string::npos;
if (isPrivatePath || isPrivateHeader || isPrivateName) {
if (!includeQmlPrivate)
continue;
if (isPrivateName)
continue;
}
}
std::string source = readFile(p);
if (source.empty())
continue;
{
std::string pathStr = p.string();
bool isPrivatePath = pathStr.find("/private/") != std::string::npos;
bool isPrivateHeader = pathStr.find("_p.h") != std::string::npos;
if ((isPrivatePath || isPrivateHeader) && !hasQmlMarker(source))
continue;
}
const std::string stripped = stripBlockComments(source);
for (const auto &[name, baseNames, matchPos, isExported, exportMacro] : extractClassMatches(stripped)) {
(void)isExported; (void)exportMacro; // universe includes all classes for abstract propagation
if (!seen.insert(name).second)
continue;
const auto pvNames = collectPureVirtualNames(source, matchPos);
const auto declNames = collectDeclaredMethodNames(stripped, matchPos);
const auto pubMethods = collectPublicMethods(stripped, matchPos, name);
const bool isAbstract = !pvNames.empty();
const bool isDefaultConstructible = bodyHasDefaultConstructor(stripped, matchPos, name);
const bool hasQObject = bodyHasQObjectMacro(stripped, matchPos);
Platform availableOn = detectAvailablePlatforms(stripped, matchPos);
if (verbose) {
if (isAbstract) {
std::cout << "[Scanner/universe] " << name
<< " has own pure virtuals:";
for (const auto &pv : pvNames)
std::cout << " " << pv;
std::cout << "\n";
}
if (!isDefaultConstructible) {
std::cout << "[Scanner/universe] " << name
<< " not default-constructible\n";
}
if (availableOn != Platform::All) {
std::cout << "[Scanner/universe] " << name
<< " platform restricted (mask=0x"
<< std::hex << static_cast<uint32_t>(availableOn)
<< std::dec << ")\n";
}
if (hasQObject) {
std::cout << "[Scanner/universe] " << name
<< " has Q_OBJECT\n";
}
if (!pubMethods.empty()) {
std::cout << "[Scanner/universe] " << name
<< " has " << pubMethods.size()
<< " public method(s)\n";
}
}
universe[name] = UniverseEntry{ baseNames,
isAbstract,
isDefaultConstructible,
hasQObject,
availableOn,
std::move(pvNames),
std::move(declNames),
std::move(pubMethods) };
}
}
if (verbose) {
std::cout << "[Scanner/universe] Universe size: " << universe.size()
<< " Q-prefixed classes.\n";
}
return universe;
}
std::unordered_set<std::string>
collectUnresolvedPureVirtuals(
const std::string &className,
const std::unordered_map<std::string, UniverseEntry> &universe,
std::unordered_map<std::string, std::unordered_set<std::string>> &cache)
{
auto cit = cache.find(className);
if (cit != cache.end())
return cit->second;
cache[className] = {};
auto uit = universe.find(className);
if (uit == universe.end())
return {};
const UniverseEntry &entry = uit->second;
std::unordered_set<std::string> unresolved = entry.ownPureVirtualNames;
for (const auto &base : entry.baseNames) {
auto baseUnresolved = collectUnresolvedPureVirtuals(
base, universe, cache);
for (const auto &pvName : baseUnresolved) {
if (!entry.ownDeclaredNames.count(pvName))
unresolved.insert(pvName);
}
}
cache[className] = unresolved;
return unresolved;
}
void propagateAbstract(
std::unordered_map<std::string, UniverseEntry> &universe,
bool verbose)
{
std::unordered_map<std::string, std::unordered_set<std::string>> cache;
for (auto &[name, entry] : universe) {
if (entry.isAbstract)
continue;
const auto unresolved = collectUnresolvedPureVirtuals(name, universe, cache);
if (!unresolved.empty()) {
entry.isAbstract = true;
if (verbose) {
std::cout << "[Scanner/propagate] " << name
<< " marked abstract (unresolved:";
for (const auto &pv : unresolved)
std::cout << " " << pv;
std::cout << ")\n";
}
}
}
}
} // anonymous namespace
// ---------------------------------------------------------------------------
bool classIsAbstract(const std::string &source, const std::string &className)
{
static const std::regex kDeclRe(
R"(\bclass\s+(?:Q_\w+_EXPORT\s+)?([A-Z]\w*)\s*:)",
std::regex::optimize);
size_t searchFrom = 0;
while (true) {
size_t pos = source.find("class ", searchFrom);
if (pos == std::string::npos)
return false;
std::smatch m;
auto sub = source.substr(pos, std::min<size_t>(256, source.size() - pos));
if (std::regex_search(sub, m, kDeclRe) && m[1].str() == className)
return bodyHasPureVirtual(source, pos);
searchFrom = pos + 1;
}
}
bool classHasQObjectMacro(const std::string &source, const std::string &className)
{
const std::string stripped = stripBlockComments(source);
static const std::regex kDeclRe(
R"(\bclass\s+(?:Q_\w+_EXPORT\s+)?([A-Z]\w*)\s*(?:[:{]))",
std::regex::optimize);
size_t searchFrom = 0;
while (true) {
size_t pos = stripped.find("class ", searchFrom);
if (pos == std::string::npos)
return false;
std::smatch m;
auto sub = stripped.substr(pos, std::min<size_t>(256, stripped.size() - pos));
if (std::regex_search(sub, m, kDeclRe) && m[1].str() == className)
return bodyHasQObjectMacro(stripped, pos);
searchFrom = pos + 1;
}
}
// Finds the '}' matching the first '{' at or after searchFrom, tracking
// brace depth over the raw text (same string-and-comment-blind approach
// bodyHasQObjectMacro already uses for class bodies — accepted here for
// the same reason: a stray unbalanced brace inside a string/char literal
// or comment is vanishingly rare in Qt's own class/method bodies).
// Returns std::string::npos if no '{' follows, or braces are unbalanced.
size_t matchingCloseBrace(const std::string &source, size_t searchFrom)
{
const size_t pos = source.find('{', searchFrom);
if (pos == std::string::npos)
return std::string::npos;
int depth = 0;
for (size_t i = pos; i < source.size(); ++i) {
if (source[i] == '{') {
++depth;
} else if (source[i] == '}') {
--depth;
if (depth == 0)
return i;
}
}
return std::string::npos;
}
int lineNumberAt(const std::string &source, size_t pos)
{
return 1 + static_cast<int>(std::count(source.begin(),
source.begin() + static_cast<ptrdiff_t>(pos), '\n'));
}
std::vector<ClassSpan> classDeclarationSpansIn(const std::string &headerContent)
{
const std::string stripped = stripBlockComments(headerContent);
std::vector<ClassSpan> result;
for (const auto &match : extractClassMatches(stripped)) {
const size_t closePos = matchingCloseBrace(stripped, match.pos);
if (closePos == std::string::npos)
continue;
result.push_back({ match.name, lineNumberAt(stripped, match.pos),
lineNumberAt(stripped, closePos) });
}
return result;
}
std::vector<std::string> classesDeclaredIn(const std::string &headerContent)
{
std::vector<std::string> result;
std::set<std::string> seen;
for (const auto &span : classDeclarationSpansIn(headerContent)) {
if (seen.insert(span.className).second)
result.push_back(span.className);
}
return result;
}
// Returns true if the character at pos is escaped by an odd number of
// immediately preceding backslashes. A single preceding backslash escapes
// it ("\\\""), but a preceding *pair* of backslashes is itself an escaped
// backslash and does not escape pos ("\\\\\"" — literal backslash followed
// by an unescaped, string-terminating quote): the escaping backslashes
// must be counted, not merely checked for presence one character back.
bool isEscaped(const std::string &s, size_t pos)
{
size_t backslashes = 0;
while (pos > backslashes && s[pos - 1 - backslashes] == '\\')
++backslashes;
return (backslashes % 2) == 1;
}
std::vector<ClassSpan> classDefinitionSpansIn(const std::string &sourceContent)
{
const std::string stripped = stripBlockComments(sourceContent);
// Build a copy where all text at brace depth > 0, inside string/char
// literals, or in a "//" line comment is blanked out, so the definition
// regex below can only match genuine depth-0 out-of-line definitions —
// never code inside a function body, a string literal, or a comment.
// Positions found in depth0 index the *same* offsets in stripped (both
// strings are built character-for-character, same length), so once a
// match's position is known, the body-and-span search below continues
// directly against stripped.
std::string depth0;
depth0.reserve(stripped.size());
int depth = 0;
bool inString = false;
bool inChar = false;
size_t i = 0;
while (i < stripped.size()) {
const char c = stripped[i];
if (!inChar && c == '"' && !isEscaped(stripped, i)) {
inString = !inString;
depth0 += (depth == 0) ? c : ' ';
++i;
continue;
}
if (!inString && c == '\'' && !isEscaped(stripped, i)) {
inChar = !inChar;
depth0 += (depth == 0) ? c : ' ';
++i;
continue;
}
if (inString || inChar) {
depth0 += (depth == 0) ? c : ' ';
++i;
continue;
}
if (c == '/' && i + 1 < stripped.size() && stripped[i + 1] == '/') {
while (i < stripped.size() && stripped[i] != '\n') {
depth0 += ' ';
++i;
}
continue; // leave the '\n' itself to be handled next iteration
}
if (c == '{') {
++depth;
depth0 += ' ';
++i;
continue;
}
if (c == '}') {
if (depth > 0)
--depth;
depth0 += ' ';
++i;
continue;
}
depth0 += (depth == 0) ? c : ' ';
++i;
}
static const std::regex kDefRe(
R"(\b(Q[A-Za-z0-9_]*)::[A-Za-z_~]\w*\s*\()",
std::regex::optimize);
std::vector<ClassSpan> result;
auto it = std::sregex_iterator(depth0.begin(), depth0.end(), kDefRe);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
const std::smatch &m = *it;
const std::string name = m[1].str();
const size_t matchPos = static_cast<size_t>(m.position());
const size_t parenOpen = matchPos + m[0].length() - 1; // position of '('
// Find the matching ')' for the parameter list in `stripped` (not
// depth0 — depth0 blanks everything past depth 0, but the
// parameter list is itself still depth-0 text, since it precedes
// the body's opening '{').
int parenDepth = 1;
size_t parenClose = std::string::npos;
for (size_t p = parenOpen + 1; p < stripped.size(); ++p) {
if (stripped[p] == '(') {
++parenDepth;
} else if (stripped[p] == ')') {
--parenDepth;
if (parenDepth == 0) {
parenClose = p;
break;
}
}
}
if (parenClose == std::string::npos)
continue;
// Scan forward past trailing modifiers (const, noexcept(...),
// override, …) — skipping any nested parens so noexcept(expr)
// doesn't confuse the search — to the first depth-0 '{', ';', or
// ':'. A real out-of-line definition is followed by '{' (the body)
// or ':' (a constructor's initializer list, itself eventually
// followed by '{'). Anything else — ';' for a bare call statement
// or "= default;"/"= delete;" — means this is not a definition
// with a body, so it contributes no span.
size_t j = parenClose + 1;
int localParenDepth = 0;
while (j < stripped.size()) {
const char c = stripped[j];
if (c == '(') {
++localParenDepth;
++j;
} else if (c == ')') {
if (localParenDepth > 0)
--localParenDepth;
++j;
} else if (localParenDepth > 0) {
++j;
} else if (c == '{' || c == ';' || c == ':') {
break;
} else {
++j;
}
}
if (j < stripped.size() && stripped[j] == ':') {
// Constructor initializer list: find the '{' that follows it,
// skipping nested parens (member initializer arguments).
size_t k = j + 1;
int pd = 0;
bool foundBrace = false;
while (k < stripped.size()) {
const char c2 = stripped[k];
if (c2 == '(') {
++pd;
++k;
} else if (c2 == ')') {
if (pd > 0)
--pd;
++k;
} else if (pd > 0) {
++k;
} else if (c2 == '{') {
foundBrace = true;
break;
} else if (c2 == ';') {
break; // no body after all
} else {
++k;
}
}
j = foundBrace ? k : stripped.size();
}
if (j >= stripped.size() || stripped[j] != '{')
continue; // not a definition with a body — skip
const size_t closePos = matchingCloseBrace(stripped, j);
if (closePos == std::string::npos)
continue;
result.push_back({ name, lineNumberAt(stripped, matchPos),
lineNumberAt(stripped, closePos) });
}
return result;
}
std::vector<std::string> classesDefinedIn(const std::string &sourceContent)
{
std::vector<std::string> result;
std::set<std::string> seen;
for (const auto &span : classDefinitionSpansIn(sourceContent)) {
if (seen.insert(span.className).second)
result.push_back(span.className);
}
return result;
}
// ---------------------------------------------------------------------------
std::optional<std::pair<std::string, ModuleInfo>>
moduleForHeader(const fs::path &headerPath, const fs::path &submoduleRoot)
{
std::error_code ec;
fs::path rel = fs::relative(headerPath, submoduleRoot / "src", ec);
if (ec || rel.empty())
return std::nullopt;
std::string dirName = firstComponent(rel);
if (dirName.empty())
return std::nullopt;
std::string lower = dirName;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
// "core" is ambiguous across submodules: it means WebEngineCore in
// qtwebengine (src/core), but e.g. qtdeclarative also has a src/core
// (the QmlCore QML plugin, a private, non-linkable target). Only honor
// the WebEngineCore mapping when actually scanning qtwebengine; treat it
// as unmapped otherwise so those classes are skipped like any other
// directory with no resolvable public target.
if (lower == "core") {
std::string moduleRootName = submoduleRoot.filename().string();
std::transform(moduleRootName.begin(), moduleRootName.end(), moduleRootName.begin(),
::tolower);
if (moduleRootName.find("webengine") == std::string::npos)
return std::nullopt;
}
const ModuleInfo *mi = findModuleByDir(lower);
if (mi)
return std::make_pair(lower, *mi);
return std::nullopt; // Directory not in module map — skip all classes under it.
}
std::optional<fs::path> findHeader(const std::string &className,
const fs::path &submoduleRoot,
bool includeQmlPrivate)
{
std::string lower = className;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
const std::string filename = lower + ".h";
const std::string filenameP = lower + "_p.h"; // private header variant
std::error_code ec;
for (const auto &entry : fs::recursive_directory_iterator(submoduleRoot, ec)) {
if (ec) {
ec.clear();
continue;
}
if (!entry.is_regular_file())
continue;
const std::string fn = entry.path().filename().string();
if (fn == filename)
return entry.path();
if (includeQmlPrivate && fn == filenameP) {
// Only return the private header if it has QML markers.
const std::string src = readFile(entry.path());
if (!src.empty() && hasQmlMarker(src))
return entry.path();
}
}
return std::nullopt;
}
std::vector<DiscoveredClass> scanClasses(const fs::path &submoduleRoot,
const std::vector<std::string> &moduleFilter, bool verbose,
const SkipList &skipList, bool includeQmlPrivate)
{
const fs::path srcRoot = submoduleRoot / "src";
if (!fs::exists(srcRoot)) {
std::cerr << "[Scanner] ERROR: " << srcRoot << " does not exist.\n";
return {};
}
if (verbose)
std::cout << "[Scanner] Phase 1: scanning all headers for Q-prefixed classes...\n";
auto universe = buildUniverseMap(srcRoot, verbose, includeQmlPrivate);
if (verbose)
std::cout << "[Scanner] Propagating abstract flags...\n";
propagateAbstract(universe, verbose);
if (verbose)
std::cout << "[Scanner] Phase 3: collecting results for requested modules...\n";
std::set<std::string> filterSet(moduleFilter.begin(), moduleFilter.end());
std::vector<DiscoveredClass> result;
std::set<std::string> seen;
std::error_code ec;
size_t filesScanned = 0;
size_t abstractCount = 0;
size_t notConstructibleCount = 0;
size_t platformCount = 0;
size_t withQObjectCount = 0;
size_t directOnlyCount = 0;
size_t skippedCount = 0;
for (const auto &entry : fs::recursive_directory_iterator(srcRoot, ec)) {
if (ec) {
ec.clear();
continue;
}
if (!entry.is_regular_file())
continue;
const fs::path &p = entry.path();
if (p.extension() != ".h")
continue;
auto modOpt = moduleForHeader(p, submoduleRoot);
if (!modOpt)
continue;
auto &[srcDir, mod] = *modOpt;
if (!filterSet.empty() && filterSet.find(srcDir) == filterSet.end())
continue;
// Determine if this is a private header
bool isPrivateFile = false;
{
std::string pathStr = p.string();
bool isPrivatePath = pathStr.find("/private/") != std::string::npos;
bool isPrivateHeader = pathStr.find("_p.h") != std::string::npos;
bool isPrivateName = pathStr.find("private.h") != std::string::npos;
if (isPrivateName)
continue; // always skip "private.h" files
if (isPrivatePath || isPrivateHeader) {
if (!includeQmlPrivate)
continue;
isPrivateFile = true;
}
}
std::string source = readFile(p);
if (source.empty())
continue;
// For private headers in QML mode: only proceed if file has QML markers
if (isPrivateFile && !hasQmlMarker(source))
continue;
++filesScanned;
const std::string stripped = stripBlockComments(source);
// Compute the lowercase stem of this header file once per file.
// Qt's public API convention: class QFooBar lives in qfoobar.h or
// qfoo.h (the header stem is a prefix of the lowercased class name).
// Any class whose header stem is NOT such a prefix is an internal
// helper that has no corresponding public <ClassName> module header;
// including it would produce an unresolvable #include.
std::string stemLower = p.stem().string();
for (char &c : stemLower)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
// For _p.h files, strip the trailing _p suffix for the prefix check.
std::string stemLowerForCheck = stemLower;
if (isPrivateFile && stemLowerForCheck.size() > 2
&& stemLowerForCheck.substr(stemLowerForCheck.size() - 2) == "_p") {
stemLowerForCheck.resize(stemLowerForCheck.size() - 2);
}
for (const auto &[name, baseNames, matchPos, isExported, exportMacro] : extractClassMatches(stripped)) {
if (!isExported)
continue;
// Check header-stem prefix rule.
{
std::string cnameLower = name;
for (char &c : cnameLower)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
// stemLowerForCheck must be a prefix of cnameLower, otherwise this class
// is defined in an unrelated header (e.g. QXmlString in qxmlutils.h)
// and has no public <ClassName> module header.
if (cnameLower.size() < stemLowerForCheck.size()
|| cnameLower.substr(0, stemLowerForCheck.size()) != stemLowerForCheck) {
continue;
}
}
if (!seen.insert(name).second)
continue;
const auto it = universe.find(name);
if (it == universe.end())
continue;
bool isAbstract = it->second.isAbstract;
bool isDefaultConstructible = it->second.isDefaultConstructible;
bool hasQObject = it->second.hasQObject;
Platform availableOn = it->second.availableOn;
if (isAbstract)
++abstractCount;
if (!isDefaultConstructible)
++notConstructibleCount;
if (availableOn != Platform::All)
++platformCount;
if (hasQObject)
++withQObjectCount;
else
++directOnlyCount;
// For private QML headers, the module inferred from path may be wrong.
// Try to recover via the export macro as fallback.
ModuleInfo effectiveMod = mod;
std::string effectiveSrcDir = srcDir;
if (isPrivateFile && !exportMacro.empty()) {
// Extract module part: strip "Q_" prefix and "_EXPORT" suffix, lowercase
std::string macroLower = exportMacro;
for (char &c : macroLower)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (macroLower.size() > 2 && macroLower.substr(0, 2) == "q_")
macroLower = macroLower.substr(2);
const std::string kSuffix = "_export";
if (macroLower.size() > kSuffix.size()
&& macroLower.substr(macroLower.size() - kSuffix.size()) == kSuffix) {
macroLower.resize(macroLower.size() - kSuffix.size());
}
const ModuleInfo *mi = findModuleByComponent(macroLower);
if (mi) {
effectiveMod = *mi;
for (const auto &[dir, info] : moduleMap()) {
if (info.component == mi->component) {
effectiveSrcDir = dir;
break;
}
}
}
}
if (verbose) {
std::cout << "[Scanner] " << name
<< " (" << effectiveSrcDir << ") " << p.filename();
if (isAbstract)
std::cout << " [abstract]";
if (!isDefaultConstructible)
std::cout << " [not-default-constructible]";
if (availableOn != Platform::All) {
std::cout << " [platform-restricted 0x"
<< std::hex << static_cast<uint32_t>(availableOn)
<< std::dec << "]";
}
if (isPrivateFile)
std::cout << " [qml-private]";
std::cout << (hasQObject ? " [Q_OBJECT]" : " [direct-only]")
<< "\n";
}
// Skip classes that appear in the skip list.
if (skipList.isClassSkipped(name)) {
++skippedCount;
if (verbose)
std::cout << "[Scanner] Skipping (skiplist): " << name << "\n";
continue;
}
// Remove individually skipped methods.
// Copy name to a plain local to avoid capturing a structured binding
// (C++17 limitation; structured bindings are not variables).
const std::string className = name;
std::vector<MethodSignature> methods = it->second.publicMethods;
{
auto end =
std::remove_if(methods.begin(), methods.end(),
[&className, &skipList](const MethodSignature &sig) {
return skipList.isFunctionSkipped(className, sig.name);
});
methods.erase(end, methods.end());
}
std::string primaryBase = baseNames.empty() ? "" : baseNames.front();
DiscoveredClass dc{ name, primaryBase, p, effectiveSrcDir, effectiveMod, isAbstract,
isDefaultConstructible, hasQObject, availableOn,
std::move(methods) };
if (isPrivateFile) {
dc.isQmlExposed = true;
dc.privateHeaderInclude = "Qt" + effectiveMod.component
+ "/private/" + p.filename().string();
}
result.push_back(std::move(dc));
}
}
if (verbose) {
std::cout << "[Scanner] Scanned " << filesScanned << " headers, found " << result.size()
<< " classes (" << abstractCount << " abstract, " << notConstructibleCount
<< " not default-constructible, " << platformCount << " platform-restricted, "
<< withQObjectCount << " with Q_OBJECT, " << directOnlyCount
<< " direct-call only, " << skippedCount << " in skip list).\n";
}
std::sort(result.begin(), result.end(),
[](const DiscoveredClass &a, const DiscoveredClass &b) {
if (a.moduleSrcDir != b.moduleSrcDir)
return a.moduleSrcDir < b.moduleSrcDir;
return a.className < b.className;
});
return result;
}
std::optional<DiscoveredClass> scanSingleClass(const std::string &className,
const fs::path &submoduleRoot,
bool includeQmlPrivate)
{
// Fast path: filename == <lowercase(class)>.h (works for QDir → qdir.h, etc.)
// Also try <lowercase(class)>_p.h when in QML mode.
fs::path headerPath;
if (auto h = findHeader(className, submoduleRoot, includeQmlPrivate))
headerPath = *h;
// Slow path: multiple classes can share one header (e.g. QDomDocument,
// QDomElement, … all live in qdom.h). Apply the same stem-prefix rule
// that scanClasses() uses: a header whose lowercase stem is a prefix of
// the lowercased class name is a candidate.
if (headerPath.empty()) {
std::string cnameLower = className;
for (char &c : cnameLower)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
std::error_code ec;
for (const auto &entry : fs::recursive_directory_iterator(submoduleRoot / "src", ec)) {
if (ec) {
ec.clear();
continue;
}
if (!entry.is_regular_file())
continue;
const fs::path &p = entry.path();
if (p.extension() != ".h")
continue;
const std::string ps = p.string();
bool isPrivatePath = ps.find("/private/") != std::string::npos;
bool isPrivateHeader = ps.find("_p.h") != std::string::npos;
bool isPrivateName = ps.find("private.h") != std::string::npos;
if (isPrivateName)
continue;
if (isPrivatePath || isPrivateHeader) {
if (!includeQmlPrivate)
continue;
}
std::string stemLower = p.stem().string();
for (char &c : stemLower)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
// Strip _p suffix for private headers
std::string stemLowerCheck = stemLower;
if ((isPrivatePath || isPrivateHeader) && stemLowerCheck.size() > 2
&& stemLowerCheck.substr(stemLowerCheck.size() - 2) == "_p") {
stemLowerCheck.resize(stemLowerCheck.size() - 2);
}
// stemLower must be a non-empty prefix of cnameLower
if (stemLowerCheck.empty() || cnameLower.size() < stemLowerCheck.size()
|| cnameLower.substr(0, stemLowerCheck.size()) != stemLowerCheck)
continue;
const std::string src = readFile(p);
if (src.empty())
continue;
if ((isPrivatePath || isPrivateHeader) && !hasQmlMarker(src))
continue;
const std::string stripped2 = stripBlockComments(src);
for (const auto &[name, bases, matchPos, isExp, expMacro] : extractClassMatches(stripped2)) {
(void)isExp; (void)expMacro;
if (name == className) {
headerPath = p;
break;
}
}
if (!headerPath.empty())
break;
}
}
if (headerPath.empty())
return std::nullopt;
const std::string source = readFile(headerPath);
if (source.empty())
return std::nullopt;
const std::string stripped = stripBlockComments(source);
// Check if this is a private/QML header
const std::string hPathStr = headerPath.string();
bool isPrivateFile = hPathStr.find("/private/") != std::string::npos
|| hPathStr.find("_p.h") != std::string::npos;
// Locate the class declaration inside the header.
size_t classPos = std::string::npos;
std::vector<std::string> baseNames;
std::string foundExportMacro;
for (const auto &[name, bases, matchPos, isExp, expMacro] : extractClassMatches(stripped)) {
if (name == className) {
classPos = matchPos;
baseNames = bases;
foundExportMacro = expMacro;
break;
}
}
if (classPos == std::string::npos)
return std::nullopt;
auto modOpt = moduleForHeader(headerPath, submoduleRoot);
if (!modOpt)
return std::nullopt;
auto [srcDir, mod] = *modOpt;
// For private QML headers, try to recover module via export macro
if (isPrivateFile && !foundExportMacro.empty()) {
std::string macroLower = foundExportMacro;
for (char &c : macroLower)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (macroLower.size() > 2 && macroLower.substr(0, 2) == "q_")
macroLower = macroLower.substr(2);
const std::string kSuffix = "_export";
if (macroLower.size() > kSuffix.size()
&& macroLower.substr(macroLower.size() - kSuffix.size()) == kSuffix) {
macroLower.resize(macroLower.size() - kSuffix.size());
}
const ModuleInfo *mi = findModuleByComponent(macroLower);
if (mi) {
mod = *mi;
for (const auto &[dir, info] : moduleMap()) {
if (info.component == mi->component) {
srcDir = dir;
break;
}
}
}
}
const bool hasQObject = bodyHasQObjectMacro(stripped, classPos);
const bool isAbstract = bodyHasPureVirtual(source, classPos);
const bool isDefaultConstructible = bodyHasDefaultConstructor(stripped, classPos, className);
const Platform availableOn = detectAvailablePlatforms(stripped, classPos);
auto publicMethods = collectPublicMethods(stripped, classPos, className);
const std::string primaryBase = baseNames.empty() ? "" : baseNames.front();
DiscoveredClass dc{ className, primaryBase, headerPath, srcDir, mod,
isAbstract, isDefaultConstructible, hasQObject,
availableOn, std::move(publicMethods) };
if (isPrivateFile) {
dc.isQmlExposed = true;
dc.privateHeaderInclude = "Qt" + mod.component
+ "/private/" + headerPath.filename().string();
}
return dc;
}
} // namespace QtFuzz