|
7 | 7 | import javafx.scene.control.ContextMenu; |
8 | 8 | import javafx.scene.control.Menu; |
9 | 9 | import me.darknet.assembler.ast.ASTElement; |
| 10 | +import me.darknet.assembler.ast.primitive.ASTArray; |
10 | 11 | import me.darknet.assembler.ast.primitive.ASTIdentifier; |
11 | 12 | import me.darknet.assembler.ast.primitive.ASTInstruction; |
12 | 13 | import me.darknet.assembler.ast.primitive.ASTLabel; |
| 14 | +import me.darknet.assembler.ast.primitive.ASTNumber; |
| 15 | +import me.darknet.assembler.ast.primitive.ASTObject; |
13 | 16 | import me.darknet.assembler.ast.specific.ASTMethod; |
| 17 | +import me.darknet.assembler.parser.BytecodeFormat; |
14 | 18 | import me.darknet.assembler.query.AssemblyQueries; |
15 | 19 | import me.darknet.assembler.query.AssemblyUtils; |
16 | 20 | import me.darknet.assembler.query.LabelQueryResult; |
17 | | -import me.darknet.assembler.query.LabelReferenceKind; |
18 | | -import me.darknet.assembler.query.LabelUsage; |
19 | 21 | import me.darknet.assembler.query.VariableQueryResult; |
20 | | -import me.darknet.assembler.query.VariableUsage; |
21 | 22 | import me.darknet.assembler.query.resolution.ClassAnnotationResolution; |
22 | 23 | import me.darknet.assembler.query.resolution.ClassExtends; |
23 | 24 | import me.darknet.assembler.query.resolution.ClassImplements; |
|
38 | 39 | import me.darknet.assembler.util.Location; |
39 | 40 | import me.darknet.assembler.util.Range; |
40 | 41 | import org.fxmisc.richtext.CodeArea; |
| 42 | +import org.objectweb.asm.Type; |
41 | 43 | import org.slf4j.Logger; |
42 | 44 | import software.coley.collections.Unchecked; |
43 | 45 | import software.coley.recaf.analytics.logging.Logging; |
44 | 46 | import software.coley.recaf.info.ClassInfo; |
45 | 47 | import software.coley.recaf.info.JvmClassInfo; |
46 | 48 | import software.coley.recaf.path.AssemblerPathData; |
| 49 | +import software.coley.recaf.path.ClassMemberPathNode; |
47 | 50 | import software.coley.recaf.path.ClassPathNode; |
48 | 51 | import software.coley.recaf.path.IncompletePathException; |
49 | 52 | import software.coley.recaf.services.cell.icon.IconProviderService; |
|
52 | 55 | import software.coley.recaf.ui.control.ActionMenuItem; |
53 | 56 | import software.coley.recaf.ui.control.FontIconView; |
54 | 57 | import software.coley.recaf.ui.control.richtext.Editor; |
| 58 | +import software.coley.recaf.util.Lang; |
55 | 59 | import software.coley.recaf.util.SVG; |
56 | 60 | import software.coley.recaf.workspace.model.Workspace; |
57 | 61 | import software.coley.recaf.workspace.model.bundle.ClassBundle; |
58 | 62 | import software.coley.recaf.workspace.model.bundle.JvmClassBundle; |
59 | 63 | import software.coley.recaf.workspace.model.resource.WorkspaceResource; |
60 | 64 |
|
| 65 | +import java.math.BigInteger; |
| 66 | +import java.util.ArrayList; |
61 | 67 | import java.util.Comparator; |
62 | 68 | import java.util.IdentityHashMap; |
63 | 69 | import java.util.List; |
| 70 | +import java.util.Locale; |
64 | 71 | import java.util.Map; |
65 | 72 | import java.util.Objects; |
66 | 73 |
|
67 | 74 | import static org.kordamp.ikonli.carbonicons.CarbonIcons.ARROW_RIGHT; |
68 | 75 | import static org.kordamp.ikonli.carbonicons.CarbonIcons.HEALTH_CROSS; |
69 | 76 | import static org.kordamp.ikonli.carbonicons.CarbonIcons.LIST_BOXES; |
70 | | -import static org.objectweb.asm.Opcodes.CHECKCAST; |
71 | 77 | import static org.objectweb.asm.Opcodes.IFEQ; |
72 | | -import static org.objectweb.asm.Opcodes.INSTANCEOF; |
73 | 78 | 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.*; |
78 | 80 |
|
79 | 81 | /** |
80 | 82 | * Basic implementation for {@link AssemblerContextMenuProviderFactory}. |
@@ -195,35 +197,19 @@ private static <T extends Resolution> void register(@Nonnull Class<T> type, @Non |
195 | 197 | // No items |
196 | 198 | }); |
197 | 199 | 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) |
203 | 200 | ASTInstruction instruction = resolution.instruction(); |
204 | 201 | 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); |
227 | 213 | }); |
228 | 214 | register(VariableDeclarationResolution.class, (provider, menu, editor, workspace, resolution) -> |
229 | 215 | 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 |
251 | 237 | }); |
252 | 238 | } |
253 | 239 |
|
| 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 | + |
254 | 354 | @Nonnull |
255 | 355 | private static List<UsageTarget> variableUsageTargets(@Nonnull ASTMethod method, @Nonnull String variableName) { |
256 | 356 | VariableQueryResult queryResult = AssemblyQueries.variables(method); |
@@ -330,8 +430,157 @@ private static void gotoLabelDeclaration(@Nonnull ASTMethod method, @Nonnull Cod |
330 | 430 | gotoAstElement(area, label); |
331 | 431 | } |
332 | 432 |
|
| 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 | + |
333 | 571 | private record UsageTarget(@Nonnull String description, @Nonnull ASTElement element, @Nullable String iconPath) {} |
334 | 572 |
|
| 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 | + |
335 | 584 | /** |
336 | 585 | * @param <R> |
337 | 586 | * Resolution impl type. |
|
0 commit comments