Merge remote-tracking branch

'origin/GP-6326-dragonmacher-symbol-tree-convert-ns-bug--SQUASHED'
(Closes #8869)
This commit is contained in:
Ryan Kurtz
2026-01-20 19:20:02 -05:00
6 changed files with 118 additions and 58 deletions
@@ -71,20 +71,22 @@ public class ConvertToClassAction extends SymbolTreeContextAction {
Symbol symbol = ((SymbolNode) node).getSymbol(); Symbol symbol = ((SymbolNode) node).getSymbol();
Namespace namespace = (Namespace) symbol.getObject(); Namespace namespace = (Namespace) symbol.getObject();
if (namespace != null) { if (namespace == null) {
return;
}
String name = namespace.getName(); String name = namespace.getName();
convertToClass(program, namespace); convertToClass(program, namespace);
program.flushEvents(); program.flushEvents();
GTreeNode classesNode = root.getChild(SymbolCategory.CLASS_CATEGORY.getName()); GTreeNode classesNode = root.getChild(SymbolCategory.CLASS_CATEGORY.getName());
if (classesNode != null) { if (classesNode == null) {
context.getSymbolTree().startEditing(classesNode, name);
}
else {
Msg.showInfo(this, null, "Classes Filtered Out of View", Msg.showInfo(this, null, "Classes Filtered Out of View",
"New class node is filtered out of view"); "New class node is filtered out of view");
return;
} }
}
tree.startEditing(classesNode, name);
} }
private static void convertToClass(Program program, Namespace ns) { private static void convertToClass(Program program, Namespace ns) {
@@ -64,20 +64,22 @@ public class CreateClassAction extends SymbolTreeContextAction {
if (object instanceof ClassCategoryNode) { if (object instanceof ClassCategoryNode) {
return true; return true;
} }
else if (object instanceof SymbolNode) { if (!(object instanceof SymbolNode symbolNode)) {
SymbolNode symbolNode = (SymbolNode) object; return false;
}
Symbol symbol = symbolNode.getSymbol(); Symbol symbol = symbolNode.getSymbol();
SymbolType symbolType = symbol.getSymbolType(); SymbolType symbolType = symbol.getSymbolType();
if (symbolType == SymbolType.NAMESPACE) { if (symbolType == SymbolType.NAMESPACE) {
// allow SymbolType to perform additional checks Namespace parent = (Namespace) symbol.getObject();
Namespace parentNamespace = (Namespace) symbol.getObject(); if (parent == null) {
return SymbolType.CLASS.isValidParent(context.getProgram(), parentNamespace, return false; // the symbol has been deleted, but the tree has not updated
Address.NO_ADDRESS, parentNamespace.isExternal()); }
return SymbolType.CLASS.isValidParent(context.getProgram(), parent,
Address.NO_ADDRESS, parent.isExternal());
} }
return (symbolType == SymbolType.CLASS || symbolType == SymbolType.LIBRARY); return (symbolType == SymbolType.CLASS || symbolType == SymbolType.LIBRARY);
} }
return false;
}
private void createNewClass(SymbolTreeActionContext context) { private void createNewClass(SymbolTreeActionContext context) {
@@ -85,7 +87,6 @@ public class CreateClassAction extends SymbolTreeContextAction {
Program program = context.getProgram(); Program program = context.getProgram();
Namespace parent = program.getGlobalNamespace(); Namespace parent = program.getGlobalNamespace();
GTreeNode node = (GTreeNode) selectionPaths[0].getLastPathComponent(); GTreeNode node = (GTreeNode) selectionPaths[0].getLastPathComponent();
if (node instanceof SymbolNode) { if (node instanceof SymbolNode) {
Symbol symbol = ((SymbolNode) node).getSymbol(); Symbol symbol = ((SymbolNode) node).getSymbol();
parent = (Namespace) symbol.getObject(); parent = (Namespace) symbol.getObject();
@@ -99,14 +100,16 @@ public class CreateClassAction extends SymbolTreeContextAction {
// error occurred // error occurred
return; return;
} }
program.flushEvents(); program.flushEvents();
context.getSymbolTree().startEditing(node, newClassName); context.getSymbolTree().startEditing(node, newClassName);
} }
private String createClass(Program program, Namespace parent) { private String createClass(Program program, Namespace parent) {
return program.withTransaction("Create Class", () -> {
String newClassName = "NewClass"; String newClassName = "NewClass";
int transactionID = program.startTransaction("Create Class");
try {
SymbolTable symbolTable = program.getSymbolTable(); SymbolTable symbolTable = program.getSymbolTable();
int oneUp = 0; int oneUp = 0;
Namespace namespace = null; Namespace namespace = null;
@@ -123,11 +126,8 @@ public class CreateClassAction extends SymbolTreeContextAction {
return null; return null;
} }
} }
}
finally {
program.endTransaction(transactionID, true);
}
return newClassName; return newClassName;
});
} }
} }
@@ -146,17 +146,18 @@ public class ClassCategoryNode extends SymbolCategoryNode {
} }
SymbolNode key = SymbolNode.createKeyNode(symbol, oldName, program); SymbolNode key = SymbolNode.createKeyNode(symbol, oldName, program);
Namespace parentNs = symbol.getParentNamespace();
if (parentNs == globalNamespace) { // See if the class lives on this Classes node. This can happen when the class is the child
// no need to search for the class in the tree; the class only lives at the top // of a non-class namespace.
GTreeNode symbolNode = findNode(this, key, false, monitor); GTreeNode symbolNode = findNode(this, key, false, monitor);
if (symbolNode != null) { if (symbolNode != null) {
removeNode(symbolNode); removeNode(symbolNode);
}
return; return;
} }
// set getAllClassNodes() for a description of the map // We could not find the node. See if it is under another class node.
// (See getAllClassNodes() for a description of the map.)
Namespace parentNs = symbol.getParentNamespace();
Map<GTreeNode, List<Namespace>> classNodes = getAllClassNodes(symbol, parentNs, monitor); Map<GTreeNode, List<Namespace>> classNodes = getAllClassNodes(symbol, parentNs, monitor);
removeSymbol(key, classNodes, monitor); removeSymbol(key, classNodes, monitor);
} }
@@ -175,12 +176,12 @@ public class ClassCategoryNode extends SymbolCategoryNode {
// parent for the given symbol // parent for the given symbol
GTreeNode classNode = entry.getKey(); GTreeNode classNode = entry.getKey();
List<Namespace> parentPath = entry.getValue(); List<Namespace> parentPath = entry.getValue();
GTreeNode symbolParent = GTreeNode symbolParent = getNamespaceNode(classNode, parentPath, false, monitor);
getNamespaceNode(classNode, parentPath, false, monitor); if (symbolParent == null) {
GTreeNode symbolNode = findNode(symbolParent, key, false, monitor); continue;
if (symbolParent != null) {
symbolParent.removeNode(symbolNode);
} }
GTreeNode symbolNode = findNode(symbolParent, key, false, monitor);
symbolParent.removeNode(symbolNode);
} }
} }
@@ -316,6 +316,55 @@ public class SymbolTreePlugin2Test extends AbstractGhidraHeadedIntegrationTest {
waitForCondition(tree::isEditing); waitForCondition(tree::isEditing);
} }
@Test
public void testClassNestedUnderNonClassNamespace_RenameClass() throws Exception {
/*
The Classes folder flattens classes so every class appears at the top level. Because
users can expand classes, top level classes may also appear nested under other classes.
Classes
Class1
Namespaces
FooNs
Class1
*/
Namespace fooNs = createNamespace("FooNs");
GhidraClass class1 = createClass(fooNs, "Class1");
expandClasses();
expandNamesapces();
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1"
);
//@formatter:on
renameSymbol(class1.getSymbol(), "Class1.renamed");
//@formatter:off
assertNamespaceNodes(
"FooNs::Class1.renamed"
);
//@formatter:on
//@formatter:off
assertClassNodes(
"Class1.renamed"
);
//@formatter:on
}
@Test @Test
public void testClassCategoryDuplicates_NestedClass_RenameLabel() throws Exception { public void testClassCategoryDuplicates_NestedClass_RenameLabel() throws Exception {
@@ -733,7 +782,8 @@ public class SymbolTreePlugin2Test extends AbstractGhidraHeadedIntegrationTest {
convertToClassAction = getAction(plugin, "Convert to Class"); convertToClassAction = getAction(plugin, "Convert to Class");
assertNotNull(convertToClassAction); assertNotNull(convertToClassAction);
navigateIncomingAction = (ToggleDockingAction) getAction(plugin, NavigateOnIncomingAction.NAME); navigateIncomingAction =
(ToggleDockingAction) getAction(plugin, NavigateOnIncomingAction.NAME);
assertNotNull(navigateIncomingAction); assertNotNull(navigateIncomingAction);
} }
@@ -111,6 +111,10 @@ public class GTreeModel implements TreeModel {
@Override @Override
public void valueForPathChanged(TreePath path, Object newValue) { public void valueForPathChanged(TreePath path, Object newValue) {
if (path == null) {
// this can happen when we try to edit a node and it doesn't work due to filtering
return;
}
GTreeNode node = (GTreeNode) path.getLastPathComponent(); GTreeNode node = (GTreeNode) path.getLastPathComponent();
node.valueChanged(newValue); node.valueChanged(newValue);
} }
@@ -31,12 +31,12 @@ import ghidra.util.task.TaskMonitor;
public class GTreeStartEditingTask extends GTreeTask { public class GTreeStartEditingTask extends GTreeTask {
private final GTreeNode modelParent; private final GTreeNode modelParent;
private final GTreeNode editNode; private final GTreeNode modelEditNode;
public GTreeStartEditingTask(GTree gTree, JTree jTree, GTreeNode editNode) { public GTreeStartEditingTask(GTree gTree, JTree jTree, GTreeNode editNode) {
super(gTree); super(gTree);
this.modelParent = tree.getModelNode(editNode.getParent()); this.modelParent = tree.getModelNode(editNode.getParent());
this.editNode = editNode; this.modelEditNode = tree.getModelNode(editNode);
} }
@Override @Override
@@ -55,13 +55,16 @@ public class GTreeStartEditingTask extends GTreeTask {
} }
private void edit() { private void edit() {
TreePath path = editNode.getTreePath();
GTreeNode viewEditNode = tree.getViewNode(modelEditNode);
TreePath path = viewEditNode.getTreePath();
CellEditor cellEditor = tree.getCellEditor(); CellEditor cellEditor = tree.getCellEditor();
cellEditor.addCellEditorListener(new CellEditorListener() { cellEditor.addCellEditorListener(new CellEditorListener() {
@Override @Override
public void editingCanceled(ChangeEvent e) { public void editingCanceled(ChangeEvent e) {
cellEditor.removeCellEditorListener(this); cellEditor.removeCellEditorListener(this);
tree.setSelectedNode(editNode); // reselect the node on cancel tree.setSelectedNode(viewEditNode); // reselect the node on cancel
} }
@Override @Override
@@ -71,7 +74,7 @@ public class GTreeStartEditingTask extends GTreeTask {
// NOTE: there may be cases where this node search fails to correctly // NOTE: there may be cases where this node search fails to correctly
// identify the renamed node when name and node class is insufficient to match. // identify the renamed node when name and node class is insufficient to match.
Class<?> nodeClass = editNode.getClass(); Class<?> nodeClass = viewEditNode.getClass();
Predicate<GTreeNode> nodeMatches = n -> { Predicate<GTreeNode> nodeMatches = n -> {
return nodeClass == n.getClass() && n.getName().equals(newName); return nodeClass == n.getClass() && n.getName().equals(newName);
}; };
@@ -82,7 +85,7 @@ public class GTreeStartEditingTask extends GTreeTask {
} }
}); });
tree.setNodeEditable(editNode); tree.setNodeEditable(viewEditNode);
jTree.startEditingAtPath(path); jTree.startEditingAtPath(path);
} }