mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
refactor: rename helpers to static fields, block-style rendering, sibling insert
Rename isHelper/ToggleHelper to isStatic/ToggleStatic across core, compose,
controller, editor, and generator. Static fields now render with block syntax
(static Type name { return expr } → 0xADDR) and support collapsed/expanded
display. Add "Add Static Field" context menu for sibling nodes. Update
expression span parser, completions, C++ generator comments, and all tests.
This commit is contained in:
@@ -2435,9 +2435,9 @@ private slots:
|
||||
QCOMPARE(n.byteSize(), 8);
|
||||
}
|
||||
|
||||
// ── Helper node compose tests ──
|
||||
// ── Static field node compose tests ──
|
||||
|
||||
void testHelperSeparatorLine() {
|
||||
void testStaticFieldHeaderLine() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -2456,27 +2456,27 @@ private slots:
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Helper node
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = "my_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
// Static field node
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_static";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Separator with "helpers" text and box-drawing chars should appear
|
||||
QVERIFY2(result.text.contains(QStringLiteral("helpers")),
|
||||
qPrintable("Expected 'helpers' separator in:\n" + result.text));
|
||||
QVERIFY2(result.text.contains(QStringLiteral("\u2500")),
|
||||
qPrintable("Expected box-drawing separator char in:\n" + result.text));
|
||||
// Header with "static" keyword and opening brace should appear
|
||||
QVERIFY2(result.text.contains(QStringLiteral("static "))
|
||||
&& result.text.contains(QStringLiteral("my_static"))
|
||||
&& result.text.contains(QStringLiteral("{")),
|
||||
qPrintable("Expected static field header in:\n" + result.text));
|
||||
}
|
||||
|
||||
void testHelperDoesNotAffectStructSize() {
|
||||
void testStaticFieldDoesNotAffectStructSize() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -2494,24 +2494,24 @@ private slots:
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Struct span without helper
|
||||
// Struct span without static field
|
||||
int spanBefore = tree.structSpan(rootId);
|
||||
|
||||
// Add helper
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Struct;
|
||||
helper.name = "helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + 100");
|
||||
tree.addNode(helper);
|
||||
// Add static field
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Struct;
|
||||
sf.name = "static_field";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + 100");
|
||||
tree.addNode(sf);
|
||||
|
||||
int spanAfter = tree.structSpan(rootId);
|
||||
QCOMPARE(spanAfter, spanBefore);
|
||||
}
|
||||
|
||||
void testHelperIsHelperLineFlag() {
|
||||
void testStaticFieldIsStaticLineFlag() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -2529,30 +2529,30 @@ private slots:
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = "my_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_static";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// At least one line should have isHelperLine set
|
||||
bool foundHelper = false;
|
||||
// At least one line should have isStaticLine set
|
||||
bool foundStaticField = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.isHelperLine) {
|
||||
foundHelper = true;
|
||||
if (lm.isStaticLine) {
|
||||
foundStaticField = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundHelper, "Expected at least one LineMeta with isHelperLine=true");
|
||||
QVERIFY2(foundStaticField, "Expected at least one LineMeta with isStaticLine=true");
|
||||
}
|
||||
|
||||
void testHelperCollapsedByDefault() {
|
||||
void testStaticFieldCollapsed() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -2563,42 +2563,42 @@ private slots:
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Helper struct with a child (should still appear collapsed)
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Struct;
|
||||
helper.name = "inner";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
helper.collapsed = true;
|
||||
int hi = tree.addNode(helper);
|
||||
uint64_t helperId = tree.nodes[hi].id;
|
||||
// Static field struct with a child (should still appear collapsed)
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Struct;
|
||||
sf.name = "inner";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
sf.collapsed = true;
|
||||
int hi = tree.addNode(sf);
|
||||
uint64_t sfId = tree.nodes[hi].id;
|
||||
|
||||
Node hChild;
|
||||
hChild.kind = NodeKind::UInt32;
|
||||
hChild.name = "x";
|
||||
hChild.parentId = helperId;
|
||||
hChild.offset = 0;
|
||||
tree.addNode(hChild);
|
||||
Node sfChild;
|
||||
sfChild.kind = NodeKind::UInt32;
|
||||
sfChild.name = "x";
|
||||
sfChild.parentId = sfId;
|
||||
sfChild.offset = 0;
|
||||
tree.addNode(sfChild);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// The helper's child should NOT have a visible line (it's collapsed)
|
||||
// The static field's child should NOT have a visible line (it's collapsed)
|
||||
bool foundChildLine = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.nodeIdx >= 0 && lm.nodeIdx < tree.nodes.size()
|
||||
&& tree.nodes[lm.nodeIdx].name == QStringLiteral("x")
|
||||
&& tree.nodes[lm.nodeIdx].parentId == helperId) {
|
||||
&& tree.nodes[lm.nodeIdx].parentId == sfId) {
|
||||
foundChildLine = true;
|
||||
}
|
||||
}
|
||||
QVERIFY2(!foundChildLine,
|
||||
"Helper's children should not be visible when collapsed");
|
||||
"Static field's children should not be visible when collapsed");
|
||||
}
|
||||
|
||||
void testHelperExpressionShownInText() {
|
||||
void testStaticFieldExpressionShownInText() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -2609,14 +2609,14 @@ private slots:
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = "my_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + 0x10");
|
||||
tree.addNode(helper);
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_static";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + 0x10");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
@@ -668,179 +668,179 @@ private slots:
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32);
|
||||
}
|
||||
// ── Helper node controller tests ──
|
||||
// ── Static field node controller tests ──
|
||||
|
||||
void testAddHelper() {
|
||||
void testAddStaticField() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int origSize = m_doc->tree.nodes.size();
|
||||
|
||||
// Simulate "Add Helper" — same code as context menu action
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
// Simulate "Add Static Field" — same code as context menu action
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
const auto& h = m_doc->tree.nodes.back();
|
||||
QCOMPARE(h.isHelper, true);
|
||||
QCOMPARE(h.isStatic, true);
|
||||
QCOMPARE(h.offsetExpr, QStringLiteral("base"));
|
||||
QCOMPARE(h.name, QStringLiteral("helper"));
|
||||
QCOMPARE(h.name, QStringLiteral("static_field"));
|
||||
QCOMPARE(h.parentId, rootId);
|
||||
}
|
||||
|
||||
void testAddHelperUndo() {
|
||||
void testAddStaticFieldUndo() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int origSize = m_doc->tree.nodes.size();
|
||||
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
|
||||
// Undo: helper should be gone
|
||||
// Undo: static field should be gone
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize);
|
||||
|
||||
// Redo: helper should be back
|
||||
// Redo: static field should be back
|
||||
m_doc->undoStack.redo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
|
||||
QCOMPARE(m_doc->tree.nodes.back().isHelper, true);
|
||||
QCOMPARE(m_doc->tree.nodes.back().isStatic, true);
|
||||
}
|
||||
|
||||
void testChangeHelperExpression() {
|
||||
void testChangeStaticFieldExpression() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Add a helper
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
// Add a static field
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Change expression
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::ChangeOffsetExpr{helperId, QStringLiteral("base"), QStringLiteral("base + 0x10")}));
|
||||
cmd::ChangeOffsetExpr{sfId, QStringLiteral("base"), QStringLiteral("base + 0x10")}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(helperId);
|
||||
int idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + 0x10"));
|
||||
|
||||
// Undo: old expression restored
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
idx = m_doc->tree.indexOfId(helperId);
|
||||
idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
|
||||
}
|
||||
|
||||
void testDeleteHelperPreservesStructSize() {
|
||||
void testDeleteStaticFieldPreservesStructSize() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int spanBefore = m_doc->tree.structSpan(rootId);
|
||||
|
||||
// Add a helper
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
// Add a static field
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Struct size unchanged after adding helper
|
||||
// Struct size unchanged after adding static field
|
||||
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
|
||||
|
||||
// Remove helper
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{helperId}));
|
||||
// Remove static field
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{sfId}));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Struct size still unchanged
|
||||
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
|
||||
}
|
||||
|
||||
void testHelperRenamePreservesExpression() {
|
||||
void testStaticFieldRenamePreservesExpression() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Add a helper
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("my_helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + field_u32");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
// Add a static field
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("my_static");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + field_u32");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Rename the helper
|
||||
// Rename the static field
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::Rename{helperId, QStringLiteral("my_helper"), QStringLiteral("renamed_helper")}));
|
||||
cmd::Rename{sfId, QStringLiteral("my_static"), QStringLiteral("renamed_static")}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(helperId);
|
||||
int idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QStringLiteral("renamed_helper"));
|
||||
QCOMPARE(m_doc->tree.nodes[idx].name, QStringLiteral("renamed_static"));
|
||||
// Expression should be preserved
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + field_u32"));
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isHelper, true);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
||||
}
|
||||
|
||||
void testHelperTypeChangePreservesFlags() {
|
||||
void testStaticFieldTypeChangePreservesFlags() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
Node helper;
|
||||
helper.id = m_doc->tree.m_nextId++;
|
||||
helper.kind = NodeKind::Hex64;
|
||||
helper.name = QStringLiteral("helper");
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}}));
|
||||
Node sf;
|
||||
sf.id = m_doc->tree.m_nextId++;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = QStringLiteral("static_field");
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
|
||||
QApplication::processEvents();
|
||||
|
||||
uint64_t helperId = m_doc->tree.nodes.back().id;
|
||||
uint64_t sfId = m_doc->tree.nodes.back().id;
|
||||
|
||||
// Change kind to UInt32
|
||||
m_doc->undoStack.push(new RcxCommand(m_ctrl,
|
||||
cmd::ChangeKind{helperId, NodeKind::Hex64, NodeKind::UInt32}));
|
||||
cmd::ChangeKind{sfId, NodeKind::Hex64, NodeKind::UInt32}));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = m_doc->tree.indexOfId(helperId);
|
||||
int idx = m_doc->tree.indexOfId(sfId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
|
||||
// Helper flags must survive type change
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isHelper, true);
|
||||
// Static field flags must survive type change
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -672,9 +672,9 @@ private slots:
|
||||
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
|
||||
}
|
||||
|
||||
// ── Helper node serialization ──
|
||||
// ── Static field node serialization ──
|
||||
|
||||
void testHelperJsonRoundTrip() {
|
||||
void testStaticFieldJsonRoundTrip() {
|
||||
rcx::NodeTree tree;
|
||||
tree.baseAddress = 0x14000000;
|
||||
|
||||
@@ -692,27 +692,27 @@ private slots:
|
||||
field.offset = 0x3C;
|
||||
tree.addNode(field);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Struct;
|
||||
helper.name = "nt_hdr";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + e_lfanew");
|
||||
tree.addNode(helper);
|
||||
rcx::Node sf;
|
||||
sf.kind = rcx::NodeKind::Struct;
|
||||
sf.name = "nt_hdr";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + e_lfanew");
|
||||
tree.addNode(sf);
|
||||
|
||||
QJsonObject json = tree.toJson();
|
||||
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
|
||||
|
||||
QCOMPARE(tree2.nodes.size(), 3);
|
||||
const auto& h = tree2.nodes[2];
|
||||
QCOMPARE(h.isHelper, true);
|
||||
QCOMPARE(h.isStatic, true);
|
||||
QCOMPARE(h.offsetExpr, QStringLiteral("base + e_lfanew"));
|
||||
QCOMPARE(h.name, QStringLiteral("nt_hdr"));
|
||||
}
|
||||
|
||||
void testHelperJsonBackwardCompat() {
|
||||
// Old JSON without isHelper/offsetExpr should load with defaults
|
||||
void testStaticFieldJsonBackwardCompat() {
|
||||
// Old JSON without isStatic/offsetExpr should load with defaults
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
@@ -723,11 +723,11 @@ private slots:
|
||||
QJsonObject json = tree.toJson();
|
||||
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
|
||||
|
||||
QCOMPARE(tree2.nodes[0].isHelper, false);
|
||||
QCOMPARE(tree2.nodes[0].isStatic, false);
|
||||
QCOMPARE(tree2.nodes[0].offsetExpr, QString());
|
||||
}
|
||||
|
||||
void testStructSpanExcludesHelpers() {
|
||||
void testStructSpanExcludesStaticFields() {
|
||||
using namespace rcx;
|
||||
NodeTree tree;
|
||||
|
||||
@@ -754,27 +754,27 @@ private slots:
|
||||
f2.offset = 4;
|
||||
tree.addNode(f2);
|
||||
|
||||
// Helper: should NOT affect span
|
||||
Node helper;
|
||||
helper.kind = NodeKind::Struct;
|
||||
helper.name = "helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
// Static field: should NOT affect span
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Struct;
|
||||
sf.name = "static_field";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
// Span should be max(0+4, 4+8) = 12, same as without helper
|
||||
// Span should be max(0+4, 4+8) = 12, same as without static field
|
||||
QCOMPARE(tree.structSpan(rootId), 12);
|
||||
}
|
||||
|
||||
void testHelperExprSpanFor() {
|
||||
void testStaticExprSpanFor() {
|
||||
using namespace rcx;
|
||||
// Simulate a helper header line: " ▸ struct NT_HEADERS nt_hdr = base + e_lfanew → 0x1400000E8"
|
||||
// Simulate a static field body line: " return base + e_lfanew → 0x1400000E8"
|
||||
LineMeta lm;
|
||||
lm.isHelperLine = true;
|
||||
QString lineText = QStringLiteral(" \u25B8 struct NT_HEADERS nt_hdr = base + e_lfanew \u2192 0x1400000E8");
|
||||
ColumnSpan span = helperExprSpanFor(lm, lineText);
|
||||
lm.isStaticLine = true;
|
||||
QString lineText = QStringLiteral(" return base + e_lfanew \u2192 0x1400000E8");
|
||||
ColumnSpan span = staticExprSpanFor(lm, lineText);
|
||||
QVERIFY(span.valid);
|
||||
QString expr = lineText.mid(span.start, span.end - span.start);
|
||||
QCOMPARE(expr.trimmed(), QStringLiteral("base + e_lfanew"));
|
||||
|
||||
@@ -2556,6 +2556,218 @@ private slots:
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||
}
|
||||
// ── Static field: name must be editable (it's a function name, not hex label) ──
|
||||
|
||||
void testStaticFieldNameEditable() {
|
||||
// Build a tree with one regular field + one static field
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f;
|
||||
f.kind = NodeKind::UInt32;
|
||||
f.name = "field_a";
|
||||
f.parentId = rootId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_target";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the static field header line
|
||||
int headerLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].isStaticLine && result.meta[i].lineKind == LineKind::Header) {
|
||||
headerLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(headerLine >= 0, "Should have a static field header line");
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(headerLine);
|
||||
QVERIFY(lm);
|
||||
QVERIFY(lm->isStaticLine);
|
||||
|
||||
// Verify the header text contains the name
|
||||
QString text = m_editor->textWithMargins();
|
||||
QStringList lines = text.split('\n');
|
||||
QVERIFY2(headerLine < lines.size(), "header line in range");
|
||||
QString hdrText = lines[headerLine];
|
||||
QVERIFY2(hdrText.contains("my_target"), qPrintable("Header line should contain name: " + hdrText));
|
||||
|
||||
// The name should be inline-editable despite being a hex node kind
|
||||
int nameStart = kFoldCol + lm->depth * 3 + lm->effectiveTypeW + kSepWidth;
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Name, headerLine, nameStart);
|
||||
QVERIFY2(ok, qPrintable(QString("Static field name must be editable. line=%1 col=%2 depth=%3 typeW=%4 text='%5'")
|
||||
.arg(headerLine).arg(nameStart).arg(lm->depth).arg(lm->effectiveTypeW).arg(hdrText)));
|
||||
m_editor->cancelInlineEdit();
|
||||
}
|
||||
|
||||
// ── Static field: type in header triggers type picker, not inline edit ──
|
||||
|
||||
void testStaticFieldTypeClickable() {
|
||||
// Build same tree as above
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f;
|
||||
f.kind = NodeKind::UInt32;
|
||||
f.name = "field_a";
|
||||
f.parentId = rootId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "my_target";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the static field header line
|
||||
int headerLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].isStaticLine && result.meta[i].lineKind == LineKind::Header) {
|
||||
headerLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(headerLine >= 0);
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(headerLine);
|
||||
QVERIFY(lm);
|
||||
|
||||
// Scroll to ensure visible
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_ENSUREVISIBLE, (unsigned long)headerLine);
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_GOTOLINE, (unsigned long)headerLine);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Hover over the type column (after "static " prefix) — should be PointingHandCursor
|
||||
// "static " is 7 chars, so the actual type starts at indent + 7
|
||||
int typeCol = kFoldCol + lm->depth * 3 + 7;
|
||||
QPoint typePos = colToViewport(m_editor->scintilla(), headerLine, typeCol + 1);
|
||||
if (typePos.y() > 0) {
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), typePos);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Static field: body line expression is editable ──
|
||||
|
||||
void testStaticFieldExprEditable() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "target";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + 0x10");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the body line (Field with isStaticLine)
|
||||
int bodyLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].isStaticLine && result.meta[i].lineKind == LineKind::Field) {
|
||||
bodyLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(bodyLine >= 0, "Should have a static field body line");
|
||||
|
||||
// The expression should be editable via StaticExpr target
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::StaticExpr, bodyLine);
|
||||
QVERIFY2(ok, "Static field expression must be editable");
|
||||
m_editor->cancelInlineEdit();
|
||||
}
|
||||
|
||||
// ── No separator line for static fields ──
|
||||
|
||||
void testStaticFieldNoSeparator() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f;
|
||||
f.kind = NodeKind::UInt32;
|
||||
f.name = "a";
|
||||
f.parentId = rootId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "target";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// No separator line with box-drawing characters should exist
|
||||
QStringList lines = result.text.split('\n');
|
||||
for (const auto& line : lines) {
|
||||
QVERIFY2(!line.contains(QStringLiteral("\u2500\u2500\u2500\u2500 static \u2500\u2500\u2500\u2500")),
|
||||
"Static fields should not have a separator line");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestEditor)
|
||||
|
||||
@@ -758,9 +758,9 @@ private slots:
|
||||
QVERIFY(!result.contains("struct _LIST_ENTRY\n{"));
|
||||
QVERIFY(!result.contains("uint8_t _pad"));
|
||||
}
|
||||
// ── Helper node generator tests ──
|
||||
// ── Static field node generator tests ──
|
||||
|
||||
void testHelperNotInStructBody() {
|
||||
void testStaticFieldNotInStructBody() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node root;
|
||||
@@ -778,32 +778,32 @@ private slots:
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Struct;
|
||||
helper.name = "nt_hdr";
|
||||
helper.structTypeName = "IMAGE_NT_HEADERS";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + e_lfanew");
|
||||
tree.addNode(helper);
|
||||
rcx::Node sf;
|
||||
sf.kind = rcx::NodeKind::Struct;
|
||||
sf.name = "nt_hdr";
|
||||
sf.structTypeName = "IMAGE_NT_HEADERS";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + e_lfanew");
|
||||
tree.addNode(sf);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Helper should NOT appear as a member in the struct body
|
||||
// Static field should NOT appear as a member in the struct body
|
||||
QVERIFY2(!result.contains("IMAGE_NT_HEADERS nt_hdr;"),
|
||||
qPrintable("Helper should not be in struct body:\n" + result));
|
||||
qPrintable("Static field should not be in struct body:\n" + result));
|
||||
|
||||
// Helper SHOULD appear as a comment
|
||||
QVERIFY2(result.contains("// helper:"),
|
||||
qPrintable("Helper comment missing:\n" + result));
|
||||
// Static field SHOULD appear as a comment
|
||||
QVERIFY2(result.contains("// static:"),
|
||||
qPrintable("Static field comment missing:\n" + result));
|
||||
QVERIFY2(result.contains("nt_hdr"),
|
||||
qPrintable("Helper name missing from comment:\n" + result));
|
||||
qPrintable("Static field name missing from comment:\n" + result));
|
||||
QVERIFY2(result.contains("base + e_lfanew"),
|
||||
qPrintable("Helper expression missing from comment:\n" + result));
|
||||
qPrintable("Static field expression missing from comment:\n" + result));
|
||||
}
|
||||
|
||||
void testHelperCommentFormat() {
|
||||
void testStaticFieldCommentFormat() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node root;
|
||||
@@ -821,26 +821,26 @@ private slots:
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Hex64;
|
||||
helper.name = "ptr";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base + 0xFF");
|
||||
tree.addNode(helper);
|
||||
rcx::Node sf;
|
||||
sf.kind = rcx::NodeKind::Hex64;
|
||||
sf.name = "ptr";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + 0xFF");
|
||||
tree.addNode(sf);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// The regular field should be in the struct body
|
||||
QVERIFY(result.contains("uint64_t base_field;"));
|
||||
|
||||
// Helper emitted as comment after struct body
|
||||
QVERIFY(result.contains("// helper:"));
|
||||
// Static field emitted as comment after struct body
|
||||
QVERIFY(result.contains("// static:"));
|
||||
QVERIFY(result.contains("@ base + 0xFF"));
|
||||
}
|
||||
|
||||
void testStructSizeUnchangedByHelper() {
|
||||
void testStructSizeUnchangedByStaticField() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node root;
|
||||
@@ -858,14 +858,14 @@ private slots:
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node helper;
|
||||
helper.kind = rcx::NodeKind::Struct;
|
||||
helper.name = "big_helper";
|
||||
helper.parentId = rootId;
|
||||
helper.offset = 0;
|
||||
helper.isHelper = true;
|
||||
helper.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(helper);
|
||||
rcx::Node sf;
|
||||
sf.kind = rcx::NodeKind::Struct;
|
||||
sf.name = "big_static";
|
||||
sf.parentId = rootId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId, nullptr, true);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user