Skip to content

Commit c3e1c98

Browse files
committed
Additional instruction context actions (goto dec for type/field/methods, goto labels for switches, number radix conversion)
1 parent 6f40b62 commit c3e1c98

2 files changed

Lines changed: 286 additions & 36 deletions

File tree

recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAssemblerContextMenuProviderFactory.java

Lines changed: 285 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@
77
import javafx.scene.control.ContextMenu;
88
import javafx.scene.control.Menu;
99
import me.darknet.assembler.ast.ASTElement;
10+
import me.darknet.assembler.ast.primitive.ASTArray;
1011
import me.darknet.assembler.ast.primitive.ASTIdentifier;
1112
import me.darknet.assembler.ast.primitive.ASTInstruction;
1213
import me.darknet.assembler.ast.primitive.ASTLabel;
14+
import me.darknet.assembler.ast.primitive.ASTNumber;
15+
import me.darknet.assembler.ast.primitive.ASTObject;
1316
import me.darknet.assembler.ast.specific.ASTMethod;
17+
import me.darknet.assembler.parser.BytecodeFormat;
1418
import me.darknet.assembler.query.AssemblyQueries;
1519
import me.darknet.assembler.query.AssemblyUtils;
1620
import me.darknet.assembler.query.LabelQueryResult;
17-
import me.darknet.assembler.query.LabelReferenceKind;
18-
import me.darknet.assembler.query.LabelUsage;
1921
import me.darknet.assembler.query.VariableQueryResult;
20-
import me.darknet.assembler.query.VariableUsage;
2122
import me.darknet.assembler.query.resolution.ClassAnnotationResolution;
2223
import me.darknet.assembler.query.resolution.ClassExtends;
2324
import me.darknet.assembler.query.resolution.ClassImplements;
@@ -38,12 +39,14 @@
3839
import me.darknet.assembler.util.Location;
3940
import me.darknet.assembler.util.Range;
4041
import org.fxmisc.richtext.CodeArea;
42+
import org.objectweb.asm.Type;
4143
import org.slf4j.Logger;
4244
import software.coley.collections.Unchecked;
4345
import software.coley.recaf.analytics.logging.Logging;
4446
import software.coley.recaf.info.ClassInfo;
4547
import software.coley.recaf.info.JvmClassInfo;
4648
import software.coley.recaf.path.AssemblerPathData;
49+
import software.coley.recaf.path.ClassMemberPathNode;
4750
import software.coley.recaf.path.ClassPathNode;
4851
import software.coley.recaf.path.IncompletePathException;
4952
import software.coley.recaf.services.cell.icon.IconProviderService;
@@ -52,29 +55,28 @@
5255
import software.coley.recaf.ui.control.ActionMenuItem;
5356
import software.coley.recaf.ui.control.FontIconView;
5457
import software.coley.recaf.ui.control.richtext.Editor;
58+
import software.coley.recaf.util.Lang;
5559
import software.coley.recaf.util.SVG;
5660
import software.coley.recaf.workspace.model.Workspace;
5761
import software.coley.recaf.workspace.model.bundle.ClassBundle;
5862
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
5963
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
6064

65+
import java.math.BigInteger;
66+
import java.util.ArrayList;
6167
import java.util.Comparator;
6268
import java.util.IdentityHashMap;
6369
import java.util.List;
70+
import java.util.Locale;
6471
import java.util.Map;
6572
import java.util.Objects;
6673

6774
import static org.kordamp.ikonli.carbonicons.CarbonIcons.ARROW_RIGHT;
6875
import static org.kordamp.ikonli.carbonicons.CarbonIcons.HEALTH_CROSS;
6976
import static org.kordamp.ikonli.carbonicons.CarbonIcons.LIST_BOXES;
70-
import static org.objectweb.asm.Opcodes.CHECKCAST;
7177
import static org.objectweb.asm.Opcodes.IFEQ;
72-
import static org.objectweb.asm.Opcodes.INSTANCEOF;
7378
import static org.objectweb.asm.Opcodes.JSR;
74-
import static org.objectweb.asm.Opcodes.NEW;
75-
import static software.coley.collections.Unchecked.runnable;
76-
import static software.coley.recaf.util.Menus.action;
77-
import static software.coley.recaf.util.Menus.menu;
79+
import static software.coley.recaf.util.Menus.*;
7880

7981
/**
8082
* Basic implementation for {@link AssemblerContextMenuProviderFactory}.
@@ -195,35 +197,19 @@ private static <T extends Resolution> void register(@Nonnull Class<T> type, @Non
195197
// No items
196198
});
197199
register(InstructionResolution.class, (provider, menu, editor, workspace, resolution) -> {
198-
// TODO:
199-
// - Different actions for different instructions (only differentiable by instruction name string atm)
200-
// - Convert integer representations (Hex, binary, decimal)
201-
// - Goto declaration for type/field/method references
202-
// - Goto declaration for jump instructions (with case hint)
203200
ASTInstruction instruction = resolution.instruction();
204201
ASTIdentifier identifier = instruction.identifier();
205-
if (identifier != null && identifier.content() != null) {
206-
int opcode = JvmOpcodes.opcode(identifier.content());
207-
if (opcode >= IFEQ && opcode <= JSR && !instruction.arguments().isEmpty()) {
208-
String labelName = instruction.arguments().getLast().content();
209-
if (labelName != null)
210-
menu.getItems().add(action("menu.goto.label", ARROW_RIGHT, () ->
211-
gotoLabelDeclaration(resolution.method(), editor.getCodeArea(), labelName)));
212-
} else if (opcode == NEW || opcode == CHECKCAST || opcode == INSTANCEOF) {
213-
if (instruction.arguments().getLast() instanceof ASTIdentifier typeIdentifier) {
214-
String typeName = typeIdentifier.content();
215-
if (typeName != null) {
216-
ClassPathNode typePath = workspace.findClass(typeName);
217-
if (typePath != null) {
218-
// TODO: We want to show the type that is targeted with this, similar to how we have in
219-
// other context menus. This code is also messy and there's probably some abstractions
220-
// we can make to reduce the indentation hell.
221-
menu.getItems().add(action("menu.goto.class", ARROW_RIGHT, runnable(() -> provider.actions.gotoDeclaration(typePath))));
222-
}
223-
}
224-
}
225-
}
226-
}
202+
String instructionName = identifier == null ? null : identifier.content();
203+
if (instructionName == null)
204+
return;
205+
206+
CodeArea area = editor.getCodeArea();
207+
ASTElement selectedElement = instruction.pick(area.getCaretPosition());
208+
addIntegerConversionActions(menu, area, selectedElement);
209+
addMemberReferenceAction(provider, menu, workspace, instructionName, instruction);
210+
addTypeReferenceAction(provider, menu, workspace, instructionName, instruction);
211+
addFlowLabelAction(menu, area, resolution.method(), instructionName, instruction);
212+
addSwitchTargetActions(menu, area, resolution.method(), instructionName, instruction, selectedElement);
227213
});
228214
register(VariableDeclarationResolution.class, (provider, menu, editor, workspace, resolution) ->
229215
addUsageMenu(menu, editor, variableUsageTargets(resolution.method(), resolution.variable().identity().name())));
@@ -251,6 +237,120 @@ private static <T extends Resolution> void register(@Nonnull Class<T> type, @Non
251237
});
252238
}
253239

240+
private static void addFlowLabelAction(@Nonnull ContextMenu menu, @Nonnull CodeArea area, @Nonnull ASTMethod method,
241+
@Nonnull String instructionName, @Nonnull ASTInstruction instruction) {
242+
int opcode = JvmOpcodes.opcode(instructionName);
243+
if (opcode < IFEQ || opcode > JSR || instruction.arguments().isEmpty())
244+
return;
245+
246+
String labelName = instruction.arguments().getLast().content();
247+
if (labelName != null) {
248+
menu.getItems().add(action("menu.goto.label", ARROW_RIGHT, () ->
249+
gotoLabelDeclaration(method, area, labelName)));
250+
}
251+
}
252+
253+
private static void addTypeReferenceAction(@Nonnull AbstractContextMenuProviderFactory provider, @Nonnull ContextMenu menu,
254+
@Nonnull Workspace workspace, @Nonnull String instructionName,
255+
@Nonnull ASTInstruction instruction) {
256+
if (!AssemblyUtils.isTypeReferenceInstruction(BytecodeFormat.JVM, instructionName))
257+
return;
258+
259+
int argumentIndex = AssemblyUtils.typeReferenceArgumentIndex(BytecodeFormat.JVM, instructionName);
260+
if (argumentIndex < 0 || argumentIndex >= instruction.arguments().size())
261+
return;
262+
263+
String typeName = normalizeTypeReference(instruction.arguments().get(argumentIndex).content());
264+
if (typeName == null)
265+
return;
266+
267+
ClassPathNode typePath = workspace.findClass(typeName);
268+
ActionMenuItem gotoAction = action("menu.goto.class", ARROW_RIGHT, () -> {
269+
try {
270+
provider.actions.gotoDeclaration(Objects.requireNonNull(typePath));
271+
} catch (IncompletePathException ex) {
272+
logger.error("Cannot go to class due to incomplete path", ex);
273+
}
274+
});
275+
if (typePath == null) gotoAction.setDisable(true);
276+
menu.getItems().add(gotoAction);
277+
}
278+
279+
private static void addMemberReferenceAction(@Nonnull AbstractContextMenuProviderFactory provider, @Nonnull ContextMenu menu,
280+
@Nonnull Workspace workspace, @Nonnull String instructionName,
281+
@Nonnull ASTInstruction instruction) {
282+
MemberReference memberReference = parseMemberReference(instructionName, instruction);
283+
if (memberReference == null)
284+
return;
285+
286+
ClassPathNode ownerPath = workspace.findClass(memberReference.owner());
287+
ClassMemberPathNode memberPath = ownerPath == null ? null : ownerPath.child(memberReference.name(), memberReference.descriptor());
288+
String key = memberReference.isMethod() ? "menu.goto.method" : "menu.goto.field";
289+
ActionMenuItem gotoAction = action(key, ARROW_RIGHT, () -> {
290+
try {
291+
provider.actions.gotoDeclaration(Objects.requireNonNull(memberPath));
292+
} catch (IncompletePathException ex) {
293+
logger.error("Cannot go to member due to incomplete path", ex);
294+
}
295+
});
296+
if (memberPath == null) gotoAction.setDisable(true);
297+
menu.getItems().add(gotoAction);
298+
}
299+
300+
private static void addIntegerConversionActions(@Nonnull ContextMenu menu, @Nonnull CodeArea area,
301+
@Nullable ASTElement selectedElement) {
302+
if (!(selectedElement instanceof ASTNumber number) || number.isFloatingPoint())
303+
return;
304+
305+
String content = number.content();
306+
Number value = number.number();
307+
if (content == null || value == null)
308+
return;
309+
310+
long longValue = value.longValue();
311+
boolean wide = content.endsWith("L") || content.endsWith("l") || value instanceof Long;
312+
int currentRadix = integerLiteralRadix(content);
313+
if (currentRadix != 10)
314+
menu.getItems().add(actionLiteral("Convert to decimal (%s)".formatted(formatIntegerLiteral(longValue, wide, 10)),
315+
ARROW_RIGHT, () -> replaceAstText(area, number, formatIntegerLiteral(longValue, wide, 10))));
316+
if (currentRadix != 16)
317+
menu.getItems().add(actionLiteral("Convert to hex (%s)".formatted(formatIntegerLiteral(longValue, wide, 16)),
318+
ARROW_RIGHT, () -> replaceAstText(area, number, formatIntegerLiteral(longValue, wide, 16))));
319+
if (currentRadix != 2)
320+
menu.getItems().add(actionLiteral("Convert to binary (%s)".formatted(formatIntegerLiteral(longValue, wide, 2)),
321+
ARROW_RIGHT, () -> replaceAstText(area, number, formatIntegerLiteral(longValue, wide, 2))));
322+
}
323+
324+
private static void addSwitchTargetActions(@Nonnull ContextMenu menu, @Nonnull CodeArea area, @Nonnull ASTMethod method,
325+
@Nonnull String instructionName, @Nonnull ASTInstruction instruction,
326+
@Nullable ASTElement selectedElement) {
327+
if (!"lookupswitch".equals(instructionName) && !"tableswitch".equals(instructionName))
328+
return;
329+
330+
List<SwitchTarget> targets = parseSwitchTargets(instructionName, instruction);
331+
if (targets.isEmpty())
332+
return;
333+
334+
if (selectedElement != null) {
335+
for (SwitchTarget target : targets) {
336+
if (target.matches(selectedElement)) {
337+
menu.getItems().add(action("menu.goto.label", ARROW_RIGHT, () ->
338+
gotoLabelDeclaration(method, area, target.labelName())));
339+
break;
340+
}
341+
}
342+
}
343+
344+
Menu switchMenu = new Menu();
345+
switchMenu.textProperty().bind(Lang.getBinding("menu.goto.switch.title"));
346+
switchMenu.setGraphic(new FontIconView(ARROW_RIGHT));
347+
for (SwitchTarget target : targets) {
348+
switchMenu.getItems().add(actionLiteral(target.description(), ARROW_RIGHT, () ->
349+
gotoLabelDeclaration(method, area, target.labelName())));
350+
}
351+
menu.getItems().add(switchMenu);
352+
}
353+
254354
@Nonnull
255355
private static List<UsageTarget> variableUsageTargets(@Nonnull ASTMethod method, @Nonnull String variableName) {
256356
VariableQueryResult queryResult = AssemblyQueries.variables(method);
@@ -330,8 +430,157 @@ private static void gotoLabelDeclaration(@Nonnull ASTMethod method, @Nonnull Cod
330430
gotoAstElement(area, label);
331431
}
332432

433+
// TODO: Surely some of this can be pulled up into JASM upstream...
434+
435+
private static void replaceAstText(@Nonnull CodeArea area, @Nonnull ASTElement element, @Nonnull String replacement) {
436+
Range range = element.range();
437+
area.replaceText(range.start(), range.end(), replacement);
438+
}
439+
440+
@Nullable
441+
private static String normalizeTypeReference(@Nullable String typeReference) {
442+
if (typeReference == null || typeReference.isBlank())
443+
return null;
444+
if (typeReference.charAt(0) != '[' && typeReference.charAt(0) != 'L')
445+
return typeReference;
446+
447+
Type type = Type.getType(typeReference);
448+
while (type.getSort() == Type.ARRAY) type = type.getElementType();
449+
return type.getSort() == Type.OBJECT ? type.getInternalName() : null;
450+
}
451+
452+
@Nullable
453+
private static MemberReference parseMemberReference(@Nonnull String instructionName, @Nonnull ASTInstruction instruction) {
454+
if ((!isFieldReferenceInstruction(instructionName) && !isMethodReferenceInstruction(instructionName)) ||
455+
instruction.arguments().size() < 2)
456+
return null;
457+
458+
String ownerAndName = instruction.arguments().get(0).content();
459+
String descriptor = instruction.arguments().get(1).content();
460+
if (ownerAndName == null || descriptor == null)
461+
return null;
462+
463+
int split = ownerAndName.lastIndexOf('.');
464+
if (split <= 0 || split >= ownerAndName.length() - 1)
465+
return null;
466+
467+
return new MemberReference(
468+
ownerAndName.substring(0, split),
469+
ownerAndName.substring(split + 1),
470+
descriptor,
471+
isMethodReferenceInstruction(instructionName)
472+
);
473+
}
474+
475+
private static boolean isFieldReferenceInstruction(@Nonnull String instructionName) {
476+
return "getfield".equals(instructionName) || "getstatic".equals(instructionName) ||
477+
"putfield".equals(instructionName) || "putstatic".equals(instructionName);
478+
}
479+
480+
private static boolean isMethodReferenceInstruction(@Nonnull String instructionName) {
481+
return instructionName.startsWith("invoke") && !"invokedynamic".equals(instructionName);
482+
}
483+
484+
@Nonnull
485+
private static List<SwitchTarget> parseSwitchTargets(@Nonnull String instructionName, @Nonnull ASTInstruction instruction) {
486+
if (instruction.arguments().isEmpty())
487+
return List.of();
488+
489+
ASTObject switchObject = instruction.argumentObject(instruction.arguments().size() - 1);
490+
if (switchObject == null)
491+
return List.of();
492+
493+
return "lookupswitch".equals(instructionName) ? parseLookupSwitchTargets(switchObject) : parseTableSwitchTargets(switchObject);
494+
}
495+
496+
@Nonnull
497+
private static List<SwitchTarget> parseLookupSwitchTargets(@Nonnull ASTObject switchObject) {
498+
List<SwitchTarget> targets = new ArrayList<>();
499+
for (int i = 0; i < switchObject.values().size(); i++) {
500+
ASTIdentifier key = switchObject.values().key(i);
501+
ASTElement value = switchObject.values().get(i);
502+
String context = key == null ? null : key.literal();
503+
String labelName = value == null ? null : value.content();
504+
if (context == null || labelName == null)
505+
continue;
506+
507+
targets.add(new SwitchTarget(
508+
context,
509+
labelName,
510+
"default".equals(context) ? "Default -> " + labelName : "Case " + context + " -> " + labelName,
511+
key,
512+
value
513+
));
514+
}
515+
return targets;
516+
}
517+
518+
@Nonnull
519+
private static List<SwitchTarget> parseTableSwitchTargets(@Nonnull ASTObject switchObject) {
520+
List<SwitchTarget> targets = new ArrayList<>();
521+
ASTElement defaultValue = switchObject.value("default");
522+
String defaultLabel = defaultValue == null ? null : defaultValue.content();
523+
if (defaultLabel != null)
524+
targets.add(new SwitchTarget("default", defaultLabel, "Default -> " + defaultLabel,
525+
switchObject.values().key("default"), defaultValue));
526+
527+
long min = 0;
528+
ASTElement minValue = switchObject.value("min");
529+
if (minValue instanceof ASTNumber number)
530+
min = number.asLong();
531+
532+
533+
ASTArray cases = switchObject.value("cases");
534+
if (cases == null)
535+
return targets;
536+
537+
long currentValue = min;
538+
for (ASTElement caseTarget : cases.values()) {
539+
String labelName = caseTarget.content();
540+
if (labelName == null || labelName.isBlank())
541+
continue;
542+
543+
String caseContext = Long.toString(currentValue++);
544+
targets.add(new SwitchTarget(caseContext, labelName, "Case " + caseContext + " -> " + labelName,
545+
caseTarget, caseTarget));
546+
}
547+
return targets;
548+
}
549+
550+
private static int integerLiteralRadix(@Nonnull String content) {
551+
int offset = content.startsWith("-") || content.startsWith("+") ? 1 : 0;
552+
if (content.regionMatches(true, offset, "0x", 0, 2))
553+
return 16;
554+
if (content.regionMatches(true, offset, "0b", 0, 2))
555+
return 2;
556+
return 10;
557+
}
558+
559+
@Nonnull
560+
private static String formatIntegerLiteral(long value, boolean wide, int radix) {
561+
BigInteger magnitude = BigInteger.valueOf(value).abs();
562+
String sign = value < 0 ? "-" : "";
563+
String text = switch (radix) {
564+
case 16 -> sign + "0x" + magnitude.toString(16).toUpperCase(Locale.ROOT);
565+
case 2 -> sign + "0b" + magnitude.toString(2);
566+
default -> Long.toString(value);
567+
};
568+
return wide ? text + 'L' : text;
569+
}
570+
333571
private record UsageTarget(@Nonnull String description, @Nonnull ASTElement element, @Nullable String iconPath) {}
334572

573+
private record MemberReference(@Nonnull String owner, @Nonnull String name, @Nonnull String descriptor,
574+
boolean isMethod) {}
575+
576+
private record SwitchTarget(@Nonnull String context, @Nonnull String labelName, @Nonnull String description,
577+
@Nullable ASTElement contextElement, @Nullable ASTElement labelElement) {
578+
private boolean matches(@Nonnull ASTElement element) {
579+
return element == contextElement || element == labelElement ||
580+
context.equals(element.content()) || labelName.equals(element.content());
581+
}
582+
}
583+
335584
/**
336585
* @param <R>
337586
* Resolution impl type.

0 commit comments

Comments
 (0)