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:
IChooseYou
2026-02-28 08:21:00 -07:00
committed by IChooseYou
parent 6a51c904de
commit 95faf027a9
12 changed files with 685 additions and 367 deletions

View File

@@ -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);

View File

@@ -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"));
}
};

View File

@@ -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"));

View File

@@ -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)

View File

@@ -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);