mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: enum/bitfield editing, MCP guard rails, PDB anonymous type inlining
- Enum inline editing: name/value commit handling, auto-sort by value - Bitfield support in PDB import with proper container nodes - Per-member hover/selection highlighting (kMemberBit encoding) - Context menu fixes for enum/bitfield member lines - MCP pagination (limit/offset), includeMembers param, tree.search tool - MCP status bar activity indicator for tool calls - PDB anonymous type inlining: inline <unnamed-tag> types as children - Skip anonymous pointer targets to prevent root orphans - Enum import diagnostics for debugging missing enums
This commit is contained in:
@@ -21,7 +21,7 @@ static QString buildCommandRow(const Provider& prov, uint64_t baseAddress) {
|
||||
QString src = buildSourceLabel(prov);
|
||||
QString addr = QStringLiteral("0x") +
|
||||
QString::number(baseAddress, 16).toUpper();
|
||||
return QStringLiteral(" %1 \u00B7 %2").arg(src, addr);
|
||||
return QStringLiteral(" %1 %2").arg(src, addr);
|
||||
}
|
||||
|
||||
// -- Replicate commandRowSrcSpan for testing
|
||||
@@ -32,17 +32,13 @@ struct TestColumnSpan {
|
||||
};
|
||||
|
||||
static TestColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (idx < 0) return {};
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = 0;
|
||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
||||
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||
if (start >= idx) return {};
|
||||
// Exclude trailing ▾ from the editable span
|
||||
int end = idx;
|
||||
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
if (start >= arrow) return {};
|
||||
return {start, arrow, true};
|
||||
}
|
||||
|
||||
class TestCommandRow : public QObject {
|
||||
@@ -77,13 +73,13 @@ private slots:
|
||||
void row_nullProvider() {
|
||||
NullProvider p;
|
||||
QString row = buildCommandRow(p, 0);
|
||||
QCOMPARE(row, QStringLiteral(" source\u25BE \u00B7 0x0"));
|
||||
QCOMPARE(row, QStringLiteral(" source\u25BE 0x0"));
|
||||
}
|
||||
|
||||
void row_fileProvider() {
|
||||
BufferProvider p(QByteArray(4, '\0'), "test.bin");
|
||||
QString row = buildCommandRow(p, 0x140000000ULL);
|
||||
QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE \u00B7 0x140000000"));
|
||||
QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE 0x140000000"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
@@ -110,7 +106,7 @@ private slots:
|
||||
void span_processProvider_simulated() {
|
||||
// Simulate a process provider without needing Windows APIs
|
||||
// by building the string directly
|
||||
QString row = QStringLiteral(" 'notepad.exe'\u25BE \u00B7 0x7FF600000000");
|
||||
QString row = QStringLiteral(" 'notepad.exe'\u25BE 0x7FF600000000");
|
||||
auto span = commandRowSrcSpan(row);
|
||||
QVERIFY(span.valid);
|
||||
QString extracted = row.mid(span.start, span.end - span.start);
|
||||
|
||||
@@ -1924,7 +1924,7 @@ private slots:
|
||||
|
||||
void testCommandRowRootNameSpan() {
|
||||
// Name span should cover the class name in the merged command row
|
||||
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {";
|
||||
QString text = "source\u25BE 0x0 struct MyClass {";
|
||||
ColumnSpan nameSpan = commandRowRootNameSpan(text);
|
||||
QVERIFY(nameSpan.valid);
|
||||
|
||||
@@ -2173,8 +2173,8 @@ private slots:
|
||||
QVERIFY(result.text.contains("Blue"));
|
||||
QVERIFY(result.text.contains("= 0"));
|
||||
QVERIFY(result.text.contains("= 2"));
|
||||
// Header should contain "enum"
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
// Header should contain the type name
|
||||
QVERIFY(result.text.contains("Color"));
|
||||
}
|
||||
|
||||
void testEnumCollapsed() {
|
||||
@@ -2205,8 +2205,7 @@ private slots:
|
||||
// Collapsed: members should NOT appear
|
||||
QVERIFY(!result.text.contains("= 0"));
|
||||
QVERIFY(!result.text.contains("= 1"));
|
||||
// But header should still show
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
// But header should still show the type name
|
||||
QVERIFY(result.text.contains("Flags"));
|
||||
}
|
||||
|
||||
@@ -2351,6 +2350,91 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
void testBitfieldMembers() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("Test");
|
||||
root.structTypeName = QStringLiteral("Test");
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node bf;
|
||||
bf.kind = NodeKind::Struct;
|
||||
bf.classKeyword = QStringLiteral("bitfield");
|
||||
bf.name = QStringLiteral("flags");
|
||||
bf.elementKind = NodeKind::Hex32;
|
||||
bf.parentId = rootId;
|
||||
bf.offset = 0;
|
||||
bf.collapsed = false;
|
||||
bf.bitfieldMembers = {
|
||||
{QStringLiteral("Valid"), 0, 1},
|
||||
{QStringLiteral("Dirty"), 1, 1},
|
||||
{QStringLiteral("PageNum"), 2, 20}
|
||||
};
|
||||
tree.addNode(bf);
|
||||
|
||||
NullProvider prov;
|
||||
auto result = compose(tree, prov);
|
||||
|
||||
// Should contain bitfield member names
|
||||
QVERIFY(result.text.contains(QStringLiteral("Valid")));
|
||||
QVERIFY(result.text.contains(QStringLiteral("Dirty")));
|
||||
QVERIFY(result.text.contains(QStringLiteral("PageNum")));
|
||||
// Should contain : width = value format
|
||||
QVERIFY(result.text.contains(QStringLiteral(": 1 =")));
|
||||
QVERIFY(result.text.contains(QStringLiteral(": 20 =")));
|
||||
// Member lines should have isMemberLine set
|
||||
bool foundMemberLine = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.isMemberLine) {
|
||||
foundMemberLine = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundMemberLine);
|
||||
}
|
||||
|
||||
void testBitfieldJsonRoundtrip() {
|
||||
Node n;
|
||||
n.id = 42;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = NodeKind::Hex64;
|
||||
n.bitfieldMembers = {
|
||||
{QStringLiteral("ExecuteDisable"), 63, 1},
|
||||
{QStringLiteral("PageFrameNumber"), 12, 36}
|
||||
};
|
||||
|
||||
QJsonObject json = n.toJson();
|
||||
Node restored = Node::fromJson(json);
|
||||
|
||||
QCOMPARE(restored.classKeyword, QStringLiteral("bitfield"));
|
||||
QCOMPARE(restored.bitfieldMembers.size(), 2);
|
||||
QCOMPARE(restored.bitfieldMembers[0].name, QStringLiteral("ExecuteDisable"));
|
||||
QCOMPARE(restored.bitfieldMembers[0].bitOffset, (uint8_t)63);
|
||||
QCOMPARE(restored.bitfieldMembers[0].bitWidth, (uint8_t)1);
|
||||
QCOMPARE(restored.bitfieldMembers[1].name, QStringLiteral("PageFrameNumber"));
|
||||
QCOMPARE(restored.bitfieldMembers[1].bitOffset, (uint8_t)12);
|
||||
QCOMPARE(restored.bitfieldMembers[1].bitWidth, (uint8_t)36);
|
||||
}
|
||||
|
||||
void testBitfieldByteSize() {
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = NodeKind::Hex8;
|
||||
QCOMPARE(n.byteSize(), 1);
|
||||
n.elementKind = NodeKind::Hex16;
|
||||
QCOMPARE(n.byteSize(), 2);
|
||||
n.elementKind = NodeKind::Hex32;
|
||||
QCOMPARE(n.byteSize(), 4);
|
||||
n.elementKind = NodeKind::Hex64;
|
||||
QCOMPARE(n.byteSize(), 8);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCompose)
|
||||
|
||||
@@ -481,7 +481,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with an ADDR value (simulates controller.updateCommandRow)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||
|
||||
// BaseAddress should be ALLOWED on CommandRow (ADDR field)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
||||
@@ -816,7 +816,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with ADDR value (simulates controller)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||
|
||||
// Line 0 is CommandRow
|
||||
const LineMeta* lm = m_editor->metaForLine(0);
|
||||
@@ -901,7 +901,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with ADDR value (simulates controller)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||
|
||||
// Begin base address edit on line 0 (CommandRow ADDR field)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
||||
@@ -1038,7 +1038,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with root class (simulates controller.updateCommandRow)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000 struct _PEB64 {"));
|
||||
|
||||
// RootClassName should be allowed on CommandRow (line 0)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0);
|
||||
@@ -1053,7 +1053,7 @@ private slots:
|
||||
|
||||
// Set CommandRow with root class
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000 struct _PEB64 {"));
|
||||
|
||||
// Line 0 is CommandRow
|
||||
const LineMeta* lm = m_editor->metaForLine(0);
|
||||
@@ -1099,7 +1099,7 @@ private slots:
|
||||
|
||||
// Set command row text (simulates controller.updateCommandRow)
|
||||
QString cmdText = QStringLiteral(
|
||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
||||
"source\u25BE 0xD87B5E5000 struct _PEB64 {");
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
QApplication::processEvents();
|
||||
|
||||
@@ -1177,7 +1177,7 @@ private slots:
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
QString cmdText = QStringLiteral(
|
||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
||||
"source\u25BE 0xD87B5E5000 struct _PEB64 {");
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
QApplication::processEvents();
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ private slots:
|
||||
}
|
||||
|
||||
void testFmtPointer64_null() {
|
||||
QCOMPARE(fmt::fmtPointer64(0), QString("-> NULL"));
|
||||
QCOMPARE(fmt::fmtPointer64(0), QString("0x0"));
|
||||
}
|
||||
|
||||
void testFmtPointer64_nonNull() {
|
||||
QString s = fmt::fmtPointer64(0x400000);
|
||||
QVERIFY(s.startsWith("-> 0x"));
|
||||
QVERIFY(s.startsWith("0x"));
|
||||
QVERIFY(s.contains("400000"));
|
||||
}
|
||||
|
||||
|
||||
@@ -780,7 +780,7 @@ void TestImportSource::structPrefixOnType() {
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldSkipped() {
|
||||
// Bitfields emit a hex placeholder covering the group
|
||||
// Bitfields emit a bitfield container with named members
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct BF {\n"
|
||||
" uint32_t normal;\n"
|
||||
@@ -790,12 +790,20 @@ void TestImportSource::bitfieldSkipped() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// normal + Hex16 (16 bits → 2 bytes) + after
|
||||
// normal + bitfield container (16 bits → 2 bytes) + after
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex16);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[kids[1]].resolvedClassKeyword(), QStringLiteral("bitfield"));
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].name, QStringLiteral("bitA"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitWidth, (uint8_t)4);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitOffset, (uint8_t)0);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].name, QStringLiteral("bitB"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitWidth, (uint8_t)12);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitOffset, (uint8_t)4);
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 6);
|
||||
}
|
||||
@@ -812,13 +820,22 @@ void TestImportSource::bitfieldWithOffsetsEmitsHex() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// normal + hex64 (bitfield group: 64 bits) + after = 3
|
||||
// normal + bitfield container (64 bits) + after = 3
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
// Bitfield group emitted as Hex64 at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
|
||||
// Bitfield container at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[kids[1]].resolvedClassKeyword(), QStringLiteral("bitfield"));
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
QCOMPARE(tree.nodes[kids[1]].elementKind, NodeKind::Hex64);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers.size(), 4);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].name, QStringLiteral("Valid"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitWidth, (uint8_t)1);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].name, QStringLiteral("Dirty"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[2].name, QStringLiteral("PageFrameNumber"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[2].bitWidth, (uint8_t)36);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[3].name, QStringLiteral("Reserved"));
|
||||
// after at 0xC
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
|
||||
|
||||
@@ -63,7 +63,7 @@ private slots:
|
||||
// ── Chevron span detection ──
|
||||
|
||||
void testChevronSpanDetected() {
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE 0x1000 struct Alpha {");
|
||||
ColumnSpan span = commandRowChevronSpan(text);
|
||||
QVERIFY(span.valid);
|
||||
QCOMPARE(span.start, 0);
|
||||
@@ -80,7 +80,7 @@ private slots:
|
||||
// ── Existing spans unbroken by chevron prefix ──
|
||||
|
||||
void testSpansWithPrefix() {
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE 0x1000 struct Alpha {");
|
||||
|
||||
ColumnSpan src = commandRowSrcSpan(text);
|
||||
QVERIFY(src.valid);
|
||||
|
||||
Reference in New Issue
Block a user