From 7ef9b65638cd08b490e3d2918329cb27f2663130 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 4 Sep 2022 03:25:52 +0200 Subject: [PATCH 01/19] Add Call Graph with Pane --- .../main/java/me/coley/recaf/Services.java | 16 +- .../recaf/graph/call/CallGraphRegistry.java | 114 ++++++++++ .../recaf/graph/call/CallGraphVertex.java | 14 ++ .../graph/call/MutableCallGraphVertex.java | 33 +++ .../recaf/graph/call/UnresolvedMethod.java | 62 ++++++ .../{ => inheritance}/InheritanceGraph.java | 4 +- .../{ => inheritance}/InheritanceVertex.java | 2 +- .../coley/recaf/mapping/MappingsAdapter.java | 4 +- .../recaf/mapping/gen/MappingGenerator.java | 4 +- .../recaf/workspace/resource/Resources.java | 3 +- .../recaf/graph/InheritanceGraphTests.java | 2 + .../recaf/mapping/MappingGeneratorTests.java | 2 +- .../java/me/coley/recaf/ui/ClassView.java | 43 ++-- .../coley/recaf/ui/control/NavigationBar.java | 20 +- .../recaf/ui/pane/ClassHierarchyPane.java | 2 +- .../me/coley/recaf/ui/pane/HierarchyPane.java | 4 +- .../recaf/ui/pane/MethodCallGraphPane.java | 194 ++++++++++++++++++ .../java/me/coley/recaf/ui/util/Icons.java | 2 +- .../util/WorkspaceInheritanceChecker.java | 2 +- 19 files changed, 493 insertions(+), 34 deletions(-) create mode 100644 recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java create mode 100644 recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java create mode 100644 recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java create mode 100644 recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedMethod.java rename recaf-core/src/main/java/me/coley/recaf/graph/{ => inheritance}/InheritanceGraph.java (99%) rename recaf-core/src/main/java/me/coley/recaf/graph/{ => inheritance}/InheritanceVertex.java (99%) create mode 100644 recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java diff --git a/recaf-core/src/main/java/me/coley/recaf/Services.java b/recaf-core/src/main/java/me/coley/recaf/Services.java index 98ec45b9e..caafc68fc 100644 --- a/recaf-core/src/main/java/me/coley/recaf/Services.java +++ b/recaf-core/src/main/java/me/coley/recaf/Services.java @@ -2,7 +2,8 @@ import me.coley.recaf.compile.CompilerManager; import me.coley.recaf.decompile.DecompileManager; -import me.coley.recaf.graph.InheritanceGraph; +import me.coley.recaf.graph.call.CallGraphRegistry; +import me.coley.recaf.graph.inheritance.InheritanceGraph; import me.coley.recaf.mapping.MappingsManager; import me.coley.recaf.parse.JavaParserHelper; import me.coley.recaf.parse.WorkspaceSymbolSolver; @@ -10,6 +11,8 @@ import me.coley.recaf.util.WorkspaceTreeService; import me.coley.recaf.workspace.Workspace; +import javax.annotation.Nullable; + /** * Wrapper of multiple services that are provided by a controller. * Placing them in here keeps the actual {@link Controller} class minimal. @@ -25,6 +28,7 @@ public class Services { private InheritanceGraph inheritanceGraph; private WorkspaceSymbolSolver symbolSolver; private JavaParserHelper javaParserHelper; + private CallGraphRegistry callGraphRegistry; /** * Initialize services. @@ -95,6 +99,14 @@ public SsvmIntegration getSsvmIntegration() { */ public WorkspaceTreeService getTreeService() { return treeService; + } + + /** + * @return Registry for all calls made mby all methods of the {@link Controller#getWorkspace() current workspace}. + * If no workspace is set, then this will be {@code null}. + */ + public @Nullable CallGraphRegistry getCallGraphRegistry() { + return callGraphRegistry; } /** @@ -114,12 +126,14 @@ void updateWorkspace(Workspace workspace) { javaParserHelper = null; ssvmIntegration = null; treeService = null; + callGraphRegistry = null; } else { inheritanceGraph = new InheritanceGraph(workspace); symbolSolver = WorkspaceSymbolSolver.create(workspace); javaParserHelper = JavaParserHelper.create(symbolSolver); ssvmIntegration = new SsvmIntegration(workspace); treeService = new WorkspaceTreeService(workspace); + callGraphRegistry = CallGraphRegistry.createAndLoad(workspace); } } } diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java new file mode 100644 index 000000000..eca6216e4 --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -0,0 +1,114 @@ +package me.coley.recaf.graph.call; + +import me.coley.recaf.code.ClassInfo; +import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.workspace.Workspace; +import me.coley.recaf.workspace.resource.Resources; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public final class CallGraphRegistry { + private Map> methodMap = new HashMap<>(); + private final Map vertexMap = new HashMap<>(); + private final Workspace workspace; + + public CallGraphRegistry(Workspace workspace) { + this.workspace = workspace; + } + + public static CallGraphRegistry createAndLoad(Workspace workspace) { + CallGraphRegistry registry = new CallGraphRegistry(workspace); + registry.load(); + return registry; + } + + public @Nullable CallGraphVertex getVertex(MethodInfo info) { + return vertexMap.get(info); + } + + public void load() { + Resources resources = workspace.getResources(); + resources.getClasses().forEach(this::visitClass); + methodMap = null; + } + + private Map getMethodMap(ClassInfo info) { + return methodMap.computeIfAbsent(info, k -> + k.getMethods() + .stream() + .collect(Collectors.toMap( + Descriptor::new, + Function.identity() + )) + ); + } + + private void visitClass(ClassInfo info) { + Map methodMap = getMethodMap(info); + info.getClassReader().accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodInfo info = methodMap.get(new Descriptor(name, descriptor)); + Map vertexMap = CallGraphRegistry.this.vertexMap; + MutableCallGraphVertex vertex = vertexMap.computeIfAbsent(info, MutableCallGraphVertex::new); + if (!vertex.visited) { + vertex.visited = true; + return new MethodVisitor(Opcodes.ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + ClassInfo info = workspace.getResources().getClass(owner); + if (info == null) { + return; + } + MethodInfo call = getMethodMap(info).get(new Descriptor(name, descriptor)); + MutableCallGraphVertex nestedVertex = vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); + vertex.getCalls().add(nestedVertex); + nestedVertex.getCallers().add(vertex); + } + }; + } + return null; + } + }, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); + } + + private static final class Descriptor { + private final String name, desc; + + Descriptor(String name, String desc) { + this.name = name; + this.desc = desc; + } + + Descriptor(MethodInfo info) { + this.name = info.getName(); + this.desc = info.getDescriptor(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Descriptor that = (Descriptor) o; + + if (!name.equals(that.name)) return false; + return desc.equals(that.desc); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + desc.hashCode(); + return result; + } + } +} diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java new file mode 100644 index 000000000..a150928e7 --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java @@ -0,0 +1,14 @@ +package me.coley.recaf.graph.call; + +import me.coley.recaf.code.MethodInfo; + +import java.util.Collection; + +public interface CallGraphVertex { + + MethodInfo getMethodInfo(); + + Collection getCallers(); + + Collection getCalls(); +} diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java new file mode 100644 index 000000000..f6ed21a7a --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java @@ -0,0 +1,33 @@ +package me.coley.recaf.graph.call; + +import me.coley.recaf.code.MethodInfo; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public final class MutableCallGraphVertex implements CallGraphVertex { + private final Set callers = new HashSet<>(); + private final Set calls = new HashSet<>(); + private final MethodInfo methodInfo; + boolean visited; + + public MutableCallGraphVertex(MethodInfo methodInfo) { + this.methodInfo = methodInfo; + } + + @Override + public MethodInfo getMethodInfo() { + return methodInfo; + } + + @Override + public Collection getCallers() { + return callers; + } + + @Override + public Collection getCalls() { + return calls; + } +} diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedMethod.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedMethod.java new file mode 100644 index 000000000..547fb5be4 --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedMethod.java @@ -0,0 +1,62 @@ +package me.coley.recaf.graph.call; + +import me.coley.recaf.code.ClassInfo; +import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.workspace.Workspace; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +class UnresolvedMethod { + @Nonnull + final String owner; + @Nonnull + final String name; + @Nonnull + final String descriptor; + + public UnresolvedMethod(@Nonnull String owner, @Nonnull String name, @Nonnull String descriptor) { + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UnresolvedMethod that = (UnresolvedMethod) o; + + if (!Objects.equals(owner, that.owner)) return false; + if (!name.equals(that.name)) return false; + return descriptor.equals(that.descriptor); + } + + @Override + public int hashCode() { + int result = owner.hashCode(); + result = 31 * result + name.hashCode(); + result = 31 * result + descriptor.hashCode(); + return result; + } + + public boolean is(MethodInfo cm) { + return Objects.equals(owner, cm.getOwner()) && name.equals(cm.getName()) && descriptor.equals(cm.getDescriptor()); + } + + public @Nullable MethodInfo resolve(Workspace workspace) { + final ClassInfo classInfo = workspace.getResources().getClass(owner); + if (classInfo == null) return null; + return resolve(classInfo); + } + + public @Nullable MethodInfo resolve(@Nonnull ClassInfo classInfo) { + return classInfo.getMethods().stream().filter(this::is).findFirst().orElse(null); + } + + public static UnresolvedMethod of(MethodInfo methodInfo) { + return new UnresolvedMethod(methodInfo.getOwner(), methodInfo.getName(), methodInfo.getDescriptor()); + } +} diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/InheritanceGraph.java b/recaf-core/src/main/java/me/coley/recaf/graph/inheritance/InheritanceGraph.java similarity index 99% rename from recaf-core/src/main/java/me/coley/recaf/graph/InheritanceGraph.java rename to recaf-core/src/main/java/me/coley/recaf/graph/inheritance/InheritanceGraph.java index 8ef59f8f4..d35d233dd 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/InheritanceGraph.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/inheritance/InheritanceGraph.java @@ -1,4 +1,4 @@ -package me.coley.recaf.graph; +package me.coley.recaf.graph.inheritance; import me.coley.recaf.code.ClassInfo; import me.coley.recaf.code.CommonClassInfo; @@ -259,4 +259,4 @@ private void onUpdateClassImpl(CommonClassInfo oldValue, CommonClassInfo newValu if (vertex != null) vertex.setValue(newValue); } -} \ No newline at end of file +} diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/InheritanceVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/inheritance/InheritanceVertex.java similarity index 99% rename from recaf-core/src/main/java/me/coley/recaf/graph/InheritanceVertex.java rename to recaf-core/src/main/java/me/coley/recaf/graph/inheritance/InheritanceVertex.java index 3122e082d..17b8f0605 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/InheritanceVertex.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/inheritance/InheritanceVertex.java @@ -1,4 +1,4 @@ -package me.coley.recaf.graph; +package me.coley.recaf.graph.inheritance; import me.coley.recaf.code.CommonClassInfo; import me.coley.recaf.code.FieldInfo; diff --git a/recaf-core/src/main/java/me/coley/recaf/mapping/MappingsAdapter.java b/recaf-core/src/main/java/me/coley/recaf/mapping/MappingsAdapter.java index 640065083..75a121f81 100644 --- a/recaf-core/src/main/java/me/coley/recaf/mapping/MappingsAdapter.java +++ b/recaf-core/src/main/java/me/coley/recaf/mapping/MappingsAdapter.java @@ -1,7 +1,7 @@ package me.coley.recaf.mapping; -import me.coley.recaf.graph.InheritanceGraph; -import me.coley.recaf.graph.InheritanceVertex; +import me.coley.recaf.graph.inheritance.InheritanceGraph; +import me.coley.recaf.graph.inheritance.InheritanceVertex; import me.coley.recaf.mapping.data.*; import me.coley.recaf.mapping.format.IntermediateMappings; diff --git a/recaf-core/src/main/java/me/coley/recaf/mapping/gen/MappingGenerator.java b/recaf-core/src/main/java/me/coley/recaf/mapping/gen/MappingGenerator.java index 79ad10286..5eab3bbd8 100644 --- a/recaf-core/src/main/java/me/coley/recaf/mapping/gen/MappingGenerator.java +++ b/recaf-core/src/main/java/me/coley/recaf/mapping/gen/MappingGenerator.java @@ -5,8 +5,8 @@ import me.coley.recaf.code.CommonClassInfo; import me.coley.recaf.code.FieldInfo; import me.coley.recaf.code.MethodInfo; -import me.coley.recaf.graph.InheritanceGraph; -import me.coley.recaf.graph.InheritanceVertex; +import me.coley.recaf.graph.inheritance.InheritanceGraph; +import me.coley.recaf.graph.inheritance.InheritanceVertex; import me.coley.recaf.mapping.Mappings; import me.coley.recaf.mapping.MappingsAdapter; import me.coley.recaf.util.AccessFlag; diff --git a/recaf-core/src/main/java/me/coley/recaf/workspace/resource/Resources.java b/recaf-core/src/main/java/me/coley/recaf/workspace/resource/Resources.java index 5d39f4594..66ce83f07 100644 --- a/recaf-core/src/main/java/me/coley/recaf/workspace/resource/Resources.java +++ b/recaf-core/src/main/java/me/coley/recaf/workspace/resource/Resources.java @@ -4,6 +4,7 @@ import me.coley.recaf.code.DexClassInfo; import me.coley.recaf.code.FileInfo; +import javax.annotation.Nullable; import java.util.*; import java.util.function.Predicate; import java.util.stream.Stream; @@ -103,7 +104,7 @@ public Stream getFiles() { * * @return Class wrapper, if present. Otherwise {@code null}. */ - public ClassInfo getClass(String name) { + public @Nullable ClassInfo getClass(String name) { // Check primary resource for class ClassInfo info = primary.getClasses().get(name); if (info != null) diff --git a/recaf-core/src/test/java/me/coley/recaf/graph/InheritanceGraphTests.java b/recaf-core/src/test/java/me/coley/recaf/graph/InheritanceGraphTests.java index 8748f138c..bdb29a8bc 100644 --- a/recaf-core/src/test/java/me/coley/recaf/graph/InheritanceGraphTests.java +++ b/recaf-core/src/test/java/me/coley/recaf/graph/InheritanceGraphTests.java @@ -1,6 +1,8 @@ package me.coley.recaf.graph; import me.coley.recaf.TestUtils; +import me.coley.recaf.graph.inheritance.InheritanceGraph; +import me.coley.recaf.graph.inheritance.InheritanceVertex; import me.coley.recaf.workspace.Workspace; import me.coley.recaf.workspace.resource.Resource; import me.coley.recaf.workspace.resource.Resources; diff --git a/recaf-core/src/test/java/me/coley/recaf/mapping/MappingGeneratorTests.java b/recaf-core/src/test/java/me/coley/recaf/mapping/MappingGeneratorTests.java index cee36e505..14e21ebae 100644 --- a/recaf-core/src/test/java/me/coley/recaf/mapping/MappingGeneratorTests.java +++ b/recaf-core/src/test/java/me/coley/recaf/mapping/MappingGeneratorTests.java @@ -5,7 +5,7 @@ import me.coley.recaf.code.CommonClassInfo; import me.coley.recaf.code.FieldInfo; import me.coley.recaf.code.MethodInfo; -import me.coley.recaf.graph.InheritanceGraph; +import me.coley.recaf.graph.inheritance.InheritanceGraph; import me.coley.recaf.mapping.gen.MappingGenerator; import me.coley.recaf.mapping.gen.NameGenerator; import me.coley.recaf.mapping.gen.NameGeneratorFilter; diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java b/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java index 6abbe2826..8fca4b095 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java @@ -1,5 +1,6 @@ package me.coley.recaf.ui; +import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; import javafx.geometry.Side; import javafx.scene.Node; @@ -9,18 +10,18 @@ import javafx.scene.control.TabPane; import javafx.scene.layout.BorderPane; import me.coley.recaf.RecafUI; -import me.coley.recaf.code.ClassInfo; -import me.coley.recaf.code.CommonClassInfo; -import me.coley.recaf.code.DexClassInfo; -import me.coley.recaf.code.MemberInfo; +import me.coley.recaf.code.*; import me.coley.recaf.config.Configs; +import me.coley.recaf.scripting.impl.WorkspaceAPI; import me.coley.recaf.ui.behavior.*; import me.coley.recaf.ui.control.CollapsibleTabPane; +import me.coley.recaf.ui.control.NavigationBar; import me.coley.recaf.ui.control.hex.HexClassView; import me.coley.recaf.ui.pane.DecompilePane; import me.coley.recaf.ui.pane.HierarchyPane; -import me.coley.recaf.ui.pane.outline.OutlinePane; +import me.coley.recaf.ui.pane.MethodCallGraphPane; import me.coley.recaf.ui.pane.SmaliAssemblerPane; +import me.coley.recaf.ui.pane.outline.OutlinePane; import me.coley.recaf.ui.util.Icons; import me.coley.recaf.ui.util.Lang; import me.coley.recaf.workspace.Workspace; @@ -39,6 +40,7 @@ public class ClassView extends BorderPane implements ClassRepresentation, ToolSi private final BorderPane mainViewWrapper = new BorderPane(); private final CollapsibleTabPane sideTabs = new CollapsibleTabPane(); private final SplitPane contentSplit = new SplitPane(); + private final MethodCallGraphPane methodCallGraph; private ClassViewMode mode = Configs.editor().defaultClassMode; private ClassRepresentation mainView; private CommonClassInfo info; @@ -51,8 +53,13 @@ public ClassView(CommonClassInfo info) { this.info = info; outline = new OutlinePane(this); hierarchy = new HierarchyPane(); + methodCallGraph = new MethodCallGraphPane(WorkspaceAPI.getWorkspace()); // Setup main view mainView = createViewForClass(info); + methodCallGraph.currentMethodProperty().bind(Bindings.createObjectBinding(() -> { + final ItemInfo itemInfo = NavigationBar.getInstance().currentItemProperty().get(); + return itemInfo instanceof MethodInfo ? (MethodInfo) itemInfo : null; + }, NavigationBar.getInstance().currentItemProperty())); mainViewWrapper.setCenter(mainView.getNodeRepresentation()); contentSplit.getItems().add(mainViewWrapper); contentSplit.getStyleClass().add("view-split-pane"); @@ -106,6 +113,7 @@ public void onUpdate(CommonClassInfo newValue) { info = newValue; outline.onUpdate(newValue); hierarchy.onUpdate(newValue); + methodCallGraph.onUpdate(newValue); if (mainView != null) { mainView.onUpdate(newValue); } @@ -199,7 +207,8 @@ public void installSideTabs(CollapsibleTabPane tabPane) { public void populateSideTabs(CollapsibleTabPane tabPane) { tabPane.getTabs().addAll( createOutlineTab(), - createHierarchyTab() + createHierarchyTab(), + createCallGraphTab() ); if (mainView instanceof ToolSideTabbed) { ((ToolSideTabbed) mainView).populateSideTabs(tabPane); @@ -252,20 +261,24 @@ public void refreshView() { sideTabs.setup(); } - private Tab createOutlineTab() { + private static Tab createTab(String translationKey, String iconPath, Node node) { Tab tab = new Tab(); - tab.textProperty().bind(Lang.getBinding("outline.title")); - tab.setGraphic(Icons.getIconView(Icons.T_STRUCTURE)); - tab.setContent(outline); + tab.textProperty().bind(Lang.getBinding(translationKey)); + tab.setGraphic(Icons.getIconView(iconPath)); + tab.setContent(node); return tab; } + private Tab createOutlineTab() { + return createTab("outline.title", Icons.T_STRUCTURE, outline); + } + private Tab createHierarchyTab() { - Tab tab = new Tab(); - tab.textProperty().bind(Lang.getBinding("hierarchy.title")); - tab.setGraphic(Icons.getIconView(Icons.T_TREE)); - tab.setContent(hierarchy); - return tab; + return createTab("hierarchy.title", Icons.T_TREE, hierarchy); + } + + private Tab createCallGraphTab() { + return createTab("callgraph.title", Icons.T_TREE, methodCallGraph); } private static Resource getPrimary() { diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/control/NavigationBar.java b/recaf-ui/src/main/java/me/coley/recaf/ui/control/NavigationBar.java index 95abbf97a..355ac0fa8 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/control/NavigationBar.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/control/NavigationBar.java @@ -2,6 +2,8 @@ import javafx.animation.Interpolator; import javafx.animation.Transition; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Pos; import javafx.geometry.Side; import javafx.scene.Node; @@ -18,10 +20,7 @@ import javafx.scene.paint.Color; import javafx.util.Duration; import me.coley.recaf.RecafUI; -import me.coley.recaf.code.CommonClassInfo; -import me.coley.recaf.code.FieldInfo; -import me.coley.recaf.code.MemberInfo; -import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.code.*; import me.coley.recaf.config.Configs; import me.coley.recaf.parse.ParseHitResult; import me.coley.recaf.ui.CommonUX; @@ -49,6 +48,8 @@ public class NavigationBar extends HBox { private boolean lastShownState; private CommonClassInfo lastClassInfo; + private final ObjectProperty currentItem = new SimpleObjectProperty<>(); + /** * Deny public construction. */ @@ -150,6 +151,9 @@ private CommonClassInfo getDeclarator(CommonClassInfo fallback, MemberInfo membe */ public void update(CommonClassInfo classInfo, MemberInfo memberInfo) { this.lastClassInfo = classInfo; + if(classInfo == null || !shouldShow()) currentItem.set(null); + else if (memberInfo != null) currentItem.set(memberInfo); + else currentItem.set(classInfo); // Hide if config disables displaying the navigation bar if (!shouldShow()) { @@ -256,6 +260,14 @@ public static NavigationBar getInstance() { return instance; } + public ItemInfo getCurrentItem() { + return currentItem.get(); + } + + public ObjectProperty currentItemProperty() { + return currentItem; + } + /** * Simple separator drawn as ">" * diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/ClassHierarchyPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/ClassHierarchyPane.java index b47e2834d..f4045009a 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/ClassHierarchyPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/ClassHierarchyPane.java @@ -18,7 +18,7 @@ import javafx.scene.layout.Region; import me.coley.recaf.RecafUI; import me.coley.recaf.code.*; -import me.coley.recaf.graph.InheritanceVertex; +import me.coley.recaf.graph.inheritance.InheritanceVertex; import me.coley.recaf.ui.behavior.ClassRepresentation; import me.coley.recaf.ui.behavior.SaveResult; import me.coley.recaf.ui.context.ContextBuilder; diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/HierarchyPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/HierarchyPane.java index 7f305a593..4e7b2f9bd 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/HierarchyPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/HierarchyPane.java @@ -14,8 +14,8 @@ import me.coley.recaf.code.ClassInfo; import me.coley.recaf.code.CommonClassInfo; import me.coley.recaf.code.DexClassInfo; -import me.coley.recaf.graph.InheritanceGraph; -import me.coley.recaf.graph.InheritanceVertex; +import me.coley.recaf.graph.inheritance.InheritanceGraph; +import me.coley.recaf.graph.inheritance.InheritanceVertex; import me.coley.recaf.ui.CommonUX; import me.coley.recaf.ui.behavior.Updatable; import me.coley.recaf.ui.context.ContextBuilder; diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java new file mode 100644 index 000000000..82a8424bd --- /dev/null +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -0,0 +1,194 @@ +package me.coley.recaf.ui.pane; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.scene.control.Label; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import me.coley.recaf.RecafUI; +import me.coley.recaf.code.ClassInfo; +import me.coley.recaf.code.CommonClassInfo; +import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.graph.call.CallGraphRegistry; +import me.coley.recaf.graph.call.CallGraphVertex; +import me.coley.recaf.ui.CommonUX; +import me.coley.recaf.ui.behavior.Updatable; +import me.coley.recaf.ui.context.ContextBuilder; +import me.coley.recaf.ui.util.Icons; +import me.coley.recaf.util.AccessFlag; +import me.coley.recaf.util.TextDisplayUtil; +import me.coley.recaf.util.threading.FxThreadUtil; +import me.coley.recaf.util.threading.ThreadUtil; +import me.coley.recaf.workspace.Workspace; + +import java.util.*; +import java.util.stream.Collectors; + +public class MethodCallGraphPane extends BorderPane implements Updatable { + public static final int MAX_TREE_DEPTH = 10; + private final Workspace workspace; + private CommonClassInfo classInfo; + private final ObjectProperty currentMethod = new SimpleObjectProperty<>(); + private final CallGraphTreeView graphTreeView = new CallGraphTreeView(); + + public MethodCallGraphPane(Workspace workspace) { + this.workspace = workspace; + currentMethod.addListener((ChangeListener) this::onUpdateMethod); + graphTreeView.onUpdate(classInfo); + setCenter(graphTreeView); + } + + private void onUpdateMethod( + ObservableValue observable, + MethodInfo oldValue, + MethodInfo newValue + ) { + graphTreeView.onUpdate(classInfo); + } + + @Override + public void onUpdate(CommonClassInfo newValue) { + classInfo = newValue; + + } + + private class CallGraphTreeView extends TreeView implements Updatable { + + public CallGraphTreeView() { + getStyleClass().add("transparent-tree"); + setCellFactory(param -> new CallGraphCell()); + } + + @Override + public void onUpdate(CommonClassInfo newValue) { + CallGraphRegistry callGraph = RecafUI.getController().getServices().getCallGraphRegistry(); + final MethodInfo methodInfo = currentMethod.get(); + if (methodInfo == null) setRoot(null); + else ThreadUtil.run(() -> { + CallGraphItem root = buildCallGraph(methodInfo, callGraph); + root.setExpanded(true); + FxThreadUtil.run(() -> setRoot(root)); + }); + } + + private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry callGraph) { + ArrayDeque visitedMethods = new ArrayDeque<>(); + ArrayDeque> workingStack = new ArrayDeque<>(); + CallGraphItem root; + workingStack.push(new ArrayList<>(Set.of(root = new CallGraphItem(rootMethod, false)))); + int depth = 0; + while (!workingStack.isEmpty()) { + List todo = workingStack.peek(); + if (!todo.isEmpty()) { + final CallGraphItem item = todo.remove(todo.size() - 1); + if (item.recursive) continue; + visitedMethods.push(item.getValue()); + depth++; + final CallGraphVertex vertex = callGraph.getVertex(item.getValue()); + if (vertex != null) { + final List newTodo = vertex.getCalls() + .stream().map(CallGraphVertex::getMethodInfo).sorted(Comparator.comparing(MethodInfo::getName)) + .map(c -> new CallGraphItem(c, visitedMethods.contains(c))) + .filter(i -> { + item.getChildren().add(i); + return !i.recursive; + }).collect(Collectors.toList()); + if (!newTodo.isEmpty() && depth < MAX_TREE_DEPTH) { + workingStack.push(newTodo); + } else visitedMethods.pop(); + } + continue; + } + workingStack.pop(); + if(!visitedMethods.isEmpty()) visitedMethods.pop(); + depth--; + } + return root; + } + } + + + /** + * Item of a class in the hierarchy. + */ + static class CallGraphItem extends TreeItem { + boolean recursive; + + private CallGraphItem(MethodInfo info, boolean recursive) { + super(info); + this.recursive = recursive; + } + + private CallGraphItem(MethodInfo info) { + this(info, false); + } + } + + /** + * Cell of a class in the hierarchy. + */ + class CallGraphCell extends TreeCell { + private EventHandler onClickFilter; + + private CallGraphCell() { + getStyleClass().add("transparent-cell"); + getStyleClass().add("monospace"); + } + + @Override + protected void updateItem(MethodInfo item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + setOnMouseClicked(null); + if (onClickFilter != null) + removeEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); + } else { + onClickFilter = null; + Text classText = new Text(TextDisplayUtil.escapeShortenPath(item.getOwner())); + classText.setFill(Color.CADETBLUE); + Text methodText = new Text(item.getName()); + if (AccessFlag.isStatic(item.getAccess())) methodText.setFill(Color.GREEN); + else methodText.setFill(Color.YELLOW); + HBox box = new HBox(Icons.getMethodIcon(item), new TextFlow(classText, new Label("#"), methodText, new Label(item.getDescriptor()))); + box.setSpacing(5); + if (getTreeItem() instanceof CallGraphItem && ((CallGraphItem) getTreeItem()).recursive) + box.getChildren().add(Icons.getIconView(Icons.REFERENCE)); + setGraphic(box); + // setText(TextDisplayUtil.escapeShortenPath(item.getOwner()) + "#" + item.getName()); + ClassInfo classInfo = workspace.getResources().getClass(item.getOwner()); + if (classInfo != null) { + setContextMenu(ContextBuilder.forMethod(classInfo, item).setDeclaration(false).build()); + // Override the double click behavior to open the class. Doesn't work using the "setOn..." methods. + onClickFilter = (MouseEvent e) -> { + if (e.getClickCount() >= 2 && e.getButton().equals(MouseButton.PRIMARY)) { + e.consume(); + CommonUX.openMember(classInfo, item); + } + }; + addEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); + } + } + } + } + + public MethodInfo getCurrentMethod() { + return currentMethod.get(); + } + + public ObjectProperty currentMethodProperty() { + return currentMethod; + } +} diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java b/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java index 6073a6b91..3ccf02359 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java @@ -8,7 +8,7 @@ import javafx.scene.layout.StackPane; import me.coley.recaf.RecafUI; import me.coley.recaf.code.*; -import me.coley.recaf.graph.InheritanceGraph; +import me.coley.recaf.graph.inheritance.InheritanceGraph; import me.coley.recaf.ui.control.IconView; import me.coley.recaf.ui.control.code.Languages; import me.coley.recaf.util.AccessFlag; diff --git a/recaf-ui/src/main/java/me/coley/recaf/util/WorkspaceInheritanceChecker.java b/recaf-ui/src/main/java/me/coley/recaf/util/WorkspaceInheritanceChecker.java index 069ab844a..d9fe153b7 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/util/WorkspaceInheritanceChecker.java +++ b/recaf-ui/src/main/java/me/coley/recaf/util/WorkspaceInheritanceChecker.java @@ -4,7 +4,7 @@ import me.coley.recaf.RecafUI; import me.coley.recaf.assemble.util.InheritanceChecker; import me.coley.recaf.assemble.util.ReflectiveInheritanceChecker; -import me.coley.recaf.graph.InheritanceGraph; +import me.coley.recaf.graph.inheritance.InheritanceGraph; /** * Type checker that pulls info from the current workspace. From 265183dd959a74e02e8f685472cc30e68e5b2a4e Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Tue, 6 Sep 2022 23:50:18 +0200 Subject: [PATCH 02/19] Make getCalls() return a Set sorted by insertion order --- .../me/coley/recaf/graph/call/MutableCallGraphVertex.java | 8 +++----- .../java/me/coley/recaf/ui/pane/MethodCallGraphPane.java | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java index f6ed21a7a..4028e47fa 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java @@ -2,13 +2,11 @@ import me.coley.recaf.code.MethodInfo; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +import java.util.*; public final class MutableCallGraphVertex implements CallGraphVertex { - private final Set callers = new HashSet<>(); - private final Set calls = new HashSet<>(); + private final Set callers = Collections.newSetFromMap(new LinkedHashMap<>()); + private final Set calls = Collections.newSetFromMap(new LinkedHashMap<>()); private final MethodInfo methodInfo; boolean visited; diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index 82a8424bd..e16470a00 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -98,7 +98,7 @@ private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry ca final CallGraphVertex vertex = callGraph.getVertex(item.getValue()); if (vertex != null) { final List newTodo = vertex.getCalls() - .stream().map(CallGraphVertex::getMethodInfo).sorted(Comparator.comparing(MethodInfo::getName)) + .stream().map(CallGraphVertex::getMethodInfo) .map(c -> new CallGraphItem(c, visitedMethods.contains(c))) .filter(i -> { item.getChildren().add(i); From 2dab3aaf5358a656c9a01d001ca502ff91d5a202 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Thu, 8 Sep 2022 22:24:11 +0200 Subject: [PATCH 03/19] Add Callers Pane and Fix NPE related to CallGraphRegistry --- .../recaf/graph/call/CallGraphRegistry.java | 7 +++-- .../java/me/coley/recaf/ui/ClassView.java | 26 +++++++++++++------ .../recaf/ui/pane/MethodCallGraphPane.java | 25 ++++++++++++++---- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index eca6216e4..550de48c1 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -57,6 +57,8 @@ private void visitClass(ClassInfo info) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodInfo info = methodMap.get(new Descriptor(name, descriptor)); + if (info == null) + return null; Map vertexMap = CallGraphRegistry.this.vertexMap; MutableCallGraphVertex vertex = vertexMap.computeIfAbsent(info, MutableCallGraphVertex::new); if (!vertex.visited) { @@ -65,10 +67,11 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { ClassInfo info = workspace.getResources().getClass(owner); - if (info == null) { + if (info == null) return; - } MethodInfo call = getMethodMap(info).get(new Descriptor(name, descriptor)); + if (call == null) + return; MutableCallGraphVertex nestedVertex = vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); vertex.getCalls().add(nestedVertex); nestedVertex.getCallers().add(vertex); diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java b/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java index 8fca4b095..010dea262 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java @@ -1,6 +1,7 @@ package me.coley.recaf.ui; import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; import javafx.beans.property.IntegerProperty; import javafx.geometry.Side; import javafx.scene.Node; @@ -40,7 +41,8 @@ public class ClassView extends BorderPane implements ClassRepresentation, ToolSi private final BorderPane mainViewWrapper = new BorderPane(); private final CollapsibleTabPane sideTabs = new CollapsibleTabPane(); private final SplitPane contentSplit = new SplitPane(); - private final MethodCallGraphPane methodCallGraph; + private final MethodCallGraphPane methodCallGraphCalls; + private final MethodCallGraphPane methodCallGraphCallers; private ClassViewMode mode = Configs.editor().defaultClassMode; private ClassRepresentation mainView; private CommonClassInfo info; @@ -53,13 +55,16 @@ public ClassView(CommonClassInfo info) { this.info = info; outline = new OutlinePane(this); hierarchy = new HierarchyPane(); - methodCallGraph = new MethodCallGraphPane(WorkspaceAPI.getWorkspace()); + methodCallGraphCalls = new MethodCallGraphPane(WorkspaceAPI.getWorkspace(), MethodCallGraphPane.CallGraphMode.CALLS); + methodCallGraphCallers = new MethodCallGraphPane(WorkspaceAPI.getWorkspace(), MethodCallGraphPane.CallGraphMode.CALLERS); // Setup main view mainView = createViewForClass(info); - methodCallGraph.currentMethodProperty().bind(Bindings.createObjectBinding(() -> { + final ObjectBinding currentMethodBinding = Bindings.createObjectBinding(() -> { final ItemInfo itemInfo = NavigationBar.getInstance().currentItemProperty().get(); return itemInfo instanceof MethodInfo ? (MethodInfo) itemInfo : null; - }, NavigationBar.getInstance().currentItemProperty())); + }, NavigationBar.getInstance().currentItemProperty()); + methodCallGraphCalls.currentMethodProperty().bind(currentMethodBinding); + methodCallGraphCallers.currentMethodProperty().bind(currentMethodBinding); mainViewWrapper.setCenter(mainView.getNodeRepresentation()); contentSplit.getItems().add(mainViewWrapper); contentSplit.getStyleClass().add("view-split-pane"); @@ -113,7 +118,8 @@ public void onUpdate(CommonClassInfo newValue) { info = newValue; outline.onUpdate(newValue); hierarchy.onUpdate(newValue); - methodCallGraph.onUpdate(newValue); + methodCallGraphCalls.onUpdate(newValue); + methodCallGraphCallers.onUpdate(newValue); if (mainView != null) { mainView.onUpdate(newValue); } @@ -208,7 +214,8 @@ public void populateSideTabs(CollapsibleTabPane tabPane) { tabPane.getTabs().addAll( createOutlineTab(), createHierarchyTab(), - createCallGraphTab() + createCallGraphTabCalls(), + createCallGraphTabCallers() ); if (mainView instanceof ToolSideTabbed) { ((ToolSideTabbed) mainView).populateSideTabs(tabPane); @@ -277,8 +284,11 @@ private Tab createHierarchyTab() { return createTab("hierarchy.title", Icons.T_TREE, hierarchy); } - private Tab createCallGraphTab() { - return createTab("callgraph.title", Icons.T_TREE, methodCallGraph); + private Tab createCallGraphTabCalls() { + return createTab("callgraph.calls.title", Icons.T_TREE, methodCallGraphCalls); + } + private Tab createCallGraphTabCallers() { + return createTab("callgraph.callers.title", Icons.T_TREE, methodCallGraphCallers); } private static Resource getPrimary() { diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index e16470a00..bc59db2c5 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -33,16 +33,30 @@ import me.coley.recaf.workspace.Workspace; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; public class MethodCallGraphPane extends BorderPane implements Updatable { - public static final int MAX_TREE_DEPTH = 10; + public static final int MAX_TREE_DEPTH = 20; private final Workspace workspace; private CommonClassInfo classInfo; private final ObjectProperty currentMethod = new SimpleObjectProperty<>(); private final CallGraphTreeView graphTreeView = new CallGraphTreeView(); + private final CallGraphMode mode; - public MethodCallGraphPane(Workspace workspace) { + public enum CallGraphMode { + CALLS(CallGraphVertex::getCalls), + CALLERS(CallGraphVertex::getCallers); + + private final Function> childrenGetter; + + CallGraphMode(Function> getCallers) { + childrenGetter = getCallers; + } + } + + public MethodCallGraphPane(Workspace workspace, CallGraphMode mode) { + this.mode = mode; this.workspace = workspace; currentMethod.addListener((ChangeListener) this::onUpdateMethod); graphTreeView.onUpdate(classInfo); @@ -76,13 +90,13 @@ public void onUpdate(CommonClassInfo newValue) { final MethodInfo methodInfo = currentMethod.get(); if (methodInfo == null) setRoot(null); else ThreadUtil.run(() -> { - CallGraphItem root = buildCallGraph(methodInfo, callGraph); + CallGraphItem root = buildCallGraph(methodInfo, callGraph, mode.childrenGetter); root.setExpanded(true); FxThreadUtil.run(() -> setRoot(root)); }); } - private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry callGraph) { + private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry callGraph, Function> childrenGetter) { ArrayDeque visitedMethods = new ArrayDeque<>(); ArrayDeque> workingStack = new ArrayDeque<>(); CallGraphItem root; @@ -97,10 +111,11 @@ private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry ca depth++; final CallGraphVertex vertex = callGraph.getVertex(item.getValue()); if (vertex != null) { - final List newTodo = vertex.getCalls() + final List newTodo = childrenGetter.apply(vertex) .stream().map(CallGraphVertex::getMethodInfo) .map(c -> new CallGraphItem(c, visitedMethods.contains(c))) .filter(i -> { + if (i.getValue() == null) return false; item.getChildren().add(i); return !i.recursive; }).collect(Collectors.toList()); From 0cfbcc461671d5d2a6d4faa9d1dafc9285c43f01 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 2 Oct 2022 22:04:46 +0200 Subject: [PATCH 04/19] Fix merge error in MethodCallGraphPane --- .../main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index bc59db2c5..af8e96e9a 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -27,7 +27,7 @@ import me.coley.recaf.ui.context.ContextBuilder; import me.coley.recaf.ui.util.Icons; import me.coley.recaf.util.AccessFlag; -import me.coley.recaf.util.TextDisplayUtil; +import me.coley.recaf.util.EscapeUtil; import me.coley.recaf.util.threading.FxThreadUtil; import me.coley.recaf.util.threading.ThreadUtil; import me.coley.recaf.workspace.Workspace; @@ -172,7 +172,7 @@ protected void updateItem(MethodInfo item, boolean empty) { removeEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); } else { onClickFilter = null; - Text classText = new Text(TextDisplayUtil.escapeShortenPath(item.getOwner())); + Text classText = new Text(EscapeUtil.escape(item.getOwner())); classText.setFill(Color.CADETBLUE); Text methodText = new Text(item.getName()); if (AccessFlag.isStatic(item.getAccess())) methodText.setFill(Color.GREEN); From b5754e765924fa1d5d1f5e8f9bc61ebf3c389d18 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Tue, 25 Oct 2022 23:43:33 +0200 Subject: [PATCH 05/19] Add jlinker and Make CallGraph resolve virtual, special and interface calls! Credits also to @xxDark for https://github.com/xxDark/jlinker --- build.gradle | 1 + recaf-core/build.gradle | 1 + .../recaf/graph/call/CallGraphRegistry.java | 148 +++++++++++++++++- .../me/coley/recaf/util/MemoizedFunction.java | 45 ++++++ 4 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 recaf-core/src/main/java/me/coley/recaf/util/MemoizedFunction.java diff --git a/build.gradle b/build.gradle index f06cf5957..96f5c1ad7 100644 --- a/build.gradle +++ b/build.gradle @@ -92,6 +92,7 @@ subprojects { javassist = 'org.javassist:javassist:3.28.0-GA' javax_annos = 'javax.annotation:javax.annotation-api:1.3.2' jelf = 'net.fornwall:jelf:0.7.0' + jlinker = 'com.github.xxDark:jlinker:1.0.3' jphantom = 'com.github.Col-E:jphantom:1.4.3' junit_api = "org.junit.jupiter:junit-jupiter-api:$junitVersion" junit_engine = "org.junit.jupiter:junit-jupiter-engine:$junitVersion" diff --git a/recaf-core/build.gradle b/recaf-core/build.gradle index 82022c7fb..60fd778d2 100644 --- a/recaf-core/build.gradle +++ b/recaf-core/build.gradle @@ -36,6 +36,7 @@ dependencies { api ssvm api procyon_core api procyon_compiler_tools + api jlinker // Android api smali diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index 550de48c1..e4615ba24 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -1,7 +1,11 @@ package me.coley.recaf.graph.call; +import dev.xdark.jlinker.LinkResolver; +import dev.xdark.jlinker.Result; import me.coley.recaf.code.ClassInfo; +import me.coley.recaf.code.FieldInfo; import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.util.MemoizedFunction; import me.coley.recaf.workspace.Workspace; import me.coley.recaf.workspace.resource.Resources; import org.objectweb.asm.ClassReader; @@ -9,9 +13,13 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,7 +44,11 @@ public static CallGraphRegistry createAndLoad(Workspace workspace) { public void load() { Resources resources = workspace.getResources(); - resources.getClasses().forEach(this::visitClass); + Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> workspace.getResources().getClass(path)); + Function> methodMapGetter + = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); + // seems like a hack tho, needs feedback! + resources.getClasses().forEach(info -> visitClass(info, classInfoFromPathResolver, methodMapGetter)); methodMap = null; } @@ -51,12 +63,25 @@ private Map getMethodMap(ClassInfo info) { ); } - private void visitClass(ClassInfo info) { - Map methodMap = getMethodMap(info); + private void visitClass( + ClassInfo info, + Function classInfoFromPathResolver, + Function> otherMethodInfoResolver + ) { + visitClass(info, classInfoFromPathResolver, otherMethodInfoResolver.apply(info), otherMethodInfoResolver); + } + + private void visitClass( + ClassInfo info, + Function classInfoFromPathResolver, + BiFunction thisClassMethodInfoResolver, + Function> otherMethodInfoResolver + ) { + LinkResolver resolver = LinkResolver.jvm(); info.getClassReader().accept(new ClassVisitor(Opcodes.ASM9) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - MethodInfo info = methodMap.get(new Descriptor(name, descriptor)); + MethodInfo info = thisClassMethodInfoResolver.apply(name, descriptor); if (info == null) return null; Map vertexMap = CallGraphRegistry.this.vertexMap; @@ -66,10 +91,31 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str return new MethodVisitor(Opcodes.ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) { @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - ClassInfo info = workspace.getResources().getClass(owner); - if (info == null) + ClassInfo callClassInfo = classInfoFromPathResolver.apply(owner); + if (callClassInfo == null) return; - MethodInfo call = getMethodMap(info).get(new Descriptor(name, descriptor)); + MethodInfo call = otherMethodInfoResolver.apply(callClassInfo).apply(name, descriptor); + if (call == null) { + Result> result; + switch (opcode) { + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKEVIRTUAL: + result = resolver.resolveVirtualMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + break; + case Opcodes.INVOKESTATIC: + result = resolver.resolveStaticMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + break; + case Opcodes.INVOKEINTERFACE: + result = resolver.resolveInterfaceMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + break; + default: + throw new IllegalArgumentException("Opcode in visitMethodInsn must be INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE."); + } + if (result.isSuccess()) { + call = result.value().member().innerValue(); + } + // should it log on else here? or would it be spam? + } if (call == null) return; MutableCallGraphVertex nestedVertex = vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); @@ -114,4 +160,92 @@ public int hashCode() { return result; } } + + private static dev.xdark.jlinker.ClassInfo classInfo(@Nonnull ClassInfo node, Function fn) { + return new dev.xdark.jlinker.ClassInfo<>() { + @Override + public ClassInfo innerValue() { + return node; + } + + @Override + public int accessFlags() { + return node.getAccess(); + } + + @Override + public dev.xdark.jlinker.ClassInfo superClass() { + String superName = node.getSuperName(); + if (superName == null) return null; + final ClassInfo classInfo = fn.apply(superName); + return classInfo == null ? null : classInfo(classInfo, fn); + } + + @Override + public List> interfaces() { + return node.getInterfaces().stream().map(x -> { + final ClassInfo classInfo = fn.apply(x); + return classInfo == null ? null : classInfo(classInfo, fn); + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public dev.xdark.jlinker.MemberInfo getMethod(String name, String descriptor) { + for (MethodInfo method : node.getMethods()) { + if (name.equals(method.getName()) && descriptor.equals(method.getDescriptor())) { + return methodInfo(method); + } + } + return null; + } + + @Override + public dev.xdark.jlinker.MemberInfo getField(String name, String descriptor) { + for (FieldInfo field : node.getFields()) { + if (name.equals(field.getName()) && descriptor.equals(field.getDescriptor())) { + return fieldInfo(field); + } + } + return null; + } + }; + } + + private static dev.xdark.jlinker.MemberInfo methodInfo(MethodInfo node) { + return new dev.xdark.jlinker.MemberInfo<>() { + @Override + public MethodInfo innerValue() { + return node; + } + + @Override + public int accessFlags() { + return node.getAccess(); + } + + @Override + public boolean isPolymorphic() { + return false; + } + }; + } + + private static dev.xdark.jlinker.MemberInfo fieldInfo(FieldInfo node) { + return new dev.xdark.jlinker.MemberInfo<>() { + @Override + public FieldInfo innerValue() { + return node; + } + + @Override + public int accessFlags() { + return node.getAccess(); + } + + @Override + public boolean isPolymorphic() { + return false; + } + }; + } } diff --git a/recaf-core/src/main/java/me/coley/recaf/util/MemoizedFunction.java b/recaf-core/src/main/java/me/coley/recaf/util/MemoizedFunction.java new file mode 100644 index 000000000..155c676fb --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/util/MemoizedFunction.java @@ -0,0 +1,45 @@ +package me.coley.recaf.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class MemoizedFunction { + private static class UniMemoizedFunction implements Function { + private final Map cache = new HashMap<>(); + private final Function function; + + private UniMemoizedFunction(Function function) { + this.function = function; + } + + @Override + public Value apply(Key key) { + return cache.computeIfAbsent(key, function); + } + } + + private static class BiMemoizedFunction implements BiFunction { + private final Map> cache = new HashMap<>(); + private final BiFunction function; + + private BiMemoizedFunction(BiFunction function) { + this.function = function; + } + + + @Override + public Value apply(KeyA keyA, KeyB keyB) { + return cache.computeIfAbsent(keyA, __ -> new HashMap<>()).computeIfAbsent(keyB, k -> function.apply(keyA, keyB)); + } + } + + public static Function memoize(Function function) { + return new UniMemoizedFunction<>(function); + } + + public static BiFunction memoize(BiFunction function) { + return new BiMemoizedFunction<>(function); + } +} From 8f67a5dd87154643b26c9c46487e60e347a2b02c Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Wed, 26 Oct 2022 23:05:09 +0200 Subject: [PATCH 06/19] Move Call Graphs to own Window --- .../java/me/coley/recaf/ui/ClassView.java | 36 ++++------------- .../ui/context/MethodContextBuilder.java | 11 ++++- .../recaf/ui/pane/MethodCallGraphPane.java | 9 ++++- .../recaf/ui/pane/MethodCallGraphsPane.java | 40 +++++++++++++++++++ .../main/resources/translations/en_US.lang | 1 + 5 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java b/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java index 010dea262..3b6cb945c 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/ClassView.java @@ -1,7 +1,5 @@ package me.coley.recaf.ui; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.ObjectBinding; import javafx.beans.property.IntegerProperty; import javafx.geometry.Side; import javafx.scene.Node; @@ -11,16 +9,16 @@ import javafx.scene.control.TabPane; import javafx.scene.layout.BorderPane; import me.coley.recaf.RecafUI; -import me.coley.recaf.code.*; +import me.coley.recaf.code.ClassInfo; +import me.coley.recaf.code.CommonClassInfo; +import me.coley.recaf.code.DexClassInfo; +import me.coley.recaf.code.MemberInfo; import me.coley.recaf.config.Configs; -import me.coley.recaf.scripting.impl.WorkspaceAPI; import me.coley.recaf.ui.behavior.*; import me.coley.recaf.ui.control.CollapsibleTabPane; -import me.coley.recaf.ui.control.NavigationBar; import me.coley.recaf.ui.control.hex.HexClassView; import me.coley.recaf.ui.pane.DecompilePane; import me.coley.recaf.ui.pane.HierarchyPane; -import me.coley.recaf.ui.pane.MethodCallGraphPane; import me.coley.recaf.ui.pane.SmaliAssemblerPane; import me.coley.recaf.ui.pane.outline.OutlinePane; import me.coley.recaf.ui.util.Icons; @@ -35,14 +33,13 @@ * * @author Matt Coley */ -public class ClassView extends BorderPane implements ClassRepresentation, ToolSideTabbed, Cleanable, Undoable, FontSizeChangeable { +public class ClassView extends BorderPane + implements ClassRepresentation, ToolSideTabbed, Cleanable, Undoable, FontSizeChangeable { private final OutlinePane outline; private final HierarchyPane hierarchy; private final BorderPane mainViewWrapper = new BorderPane(); private final CollapsibleTabPane sideTabs = new CollapsibleTabPane(); private final SplitPane contentSplit = new SplitPane(); - private final MethodCallGraphPane methodCallGraphCalls; - private final MethodCallGraphPane methodCallGraphCallers; private ClassViewMode mode = Configs.editor().defaultClassMode; private ClassRepresentation mainView; private CommonClassInfo info; @@ -55,16 +52,8 @@ public ClassView(CommonClassInfo info) { this.info = info; outline = new OutlinePane(this); hierarchy = new HierarchyPane(); - methodCallGraphCalls = new MethodCallGraphPane(WorkspaceAPI.getWorkspace(), MethodCallGraphPane.CallGraphMode.CALLS); - methodCallGraphCallers = new MethodCallGraphPane(WorkspaceAPI.getWorkspace(), MethodCallGraphPane.CallGraphMode.CALLERS); // Setup main view mainView = createViewForClass(info); - final ObjectBinding currentMethodBinding = Bindings.createObjectBinding(() -> { - final ItemInfo itemInfo = NavigationBar.getInstance().currentItemProperty().get(); - return itemInfo instanceof MethodInfo ? (MethodInfo) itemInfo : null; - }, NavigationBar.getInstance().currentItemProperty()); - methodCallGraphCalls.currentMethodProperty().bind(currentMethodBinding); - methodCallGraphCallers.currentMethodProperty().bind(currentMethodBinding); mainViewWrapper.setCenter(mainView.getNodeRepresentation()); contentSplit.getItems().add(mainViewWrapper); contentSplit.getStyleClass().add("view-split-pane"); @@ -118,8 +107,6 @@ public void onUpdate(CommonClassInfo newValue) { info = newValue; outline.onUpdate(newValue); hierarchy.onUpdate(newValue); - methodCallGraphCalls.onUpdate(newValue); - methodCallGraphCallers.onUpdate(newValue); if (mainView != null) { mainView.onUpdate(newValue); } @@ -213,9 +200,7 @@ public void installSideTabs(CollapsibleTabPane tabPane) { public void populateSideTabs(CollapsibleTabPane tabPane) { tabPane.getTabs().addAll( createOutlineTab(), - createHierarchyTab(), - createCallGraphTabCalls(), - createCallGraphTabCallers() + createHierarchyTab() ); if (mainView instanceof ToolSideTabbed) { ((ToolSideTabbed) mainView).populateSideTabs(tabPane); @@ -284,13 +269,6 @@ private Tab createHierarchyTab() { return createTab("hierarchy.title", Icons.T_TREE, hierarchy); } - private Tab createCallGraphTabCalls() { - return createTab("callgraph.calls.title", Icons.T_TREE, methodCallGraphCalls); - } - private Tab createCallGraphTabCallers() { - return createTab("callgraph.callers.title", Icons.T_TREE, methodCallGraphCallers); - } - private static Resource getPrimary() { Workspace workspace = RecafUI.getController().getWorkspace(); if (workspace != null) diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java b/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java index 127fe0b5d..c20dc392b 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java @@ -10,6 +10,7 @@ import me.coley.recaf.code.MethodInfo; import me.coley.recaf.config.Configs; import me.coley.recaf.mapping.MappingsAdapter; +import me.coley.recaf.scripting.impl.WorkspaceAPI; import me.coley.recaf.search.TextMatchMode; import me.coley.recaf.ssvm.SsvmIntegration; import me.coley.recaf.ui.CommonUX; @@ -20,6 +21,7 @@ import me.coley.recaf.ui.docking.DockTab; import me.coley.recaf.ui.docking.RecafDockingManager; import me.coley.recaf.ui.docking.impl.ClassTab; +import me.coley.recaf.ui.pane.MethodCallGraphsPane; import me.coley.recaf.ui.pane.SearchPane; import me.coley.recaf.ui.pane.assembler.AssemblerPane; import me.coley.recaf.ui.pane.graph.MethodGraphPane; @@ -36,7 +38,9 @@ import java.util.Optional; -import static me.coley.recaf.ui.util.Menus.*; +import static me.coley.recaf.ui.util.Menus.action; +import static me.coley.recaf.ui.util.Menus.createHeader; +import static me.coley.recaf.ui.util.Menus.menu; /** * Context menu builder for methods. @@ -77,6 +81,7 @@ public ContextMenu build() { menu.getItems().add(refactor); Menu view = menu("menu.view", Icons.EYE); view.getItems().add(action("menu.view.methodcfg", Icons.CHILDREN, this::graph)); + view.getItems().add(action("menu.view.methodcallgraph", Icons.CHILDREN, this::callGraph)); menu.getItems().add(view); } if (canUseVm()) { @@ -95,6 +100,10 @@ public ContextMenu build() { return menu; } + private void callGraph() { + new GenericWindow(new MethodCallGraphsPane(WorkspaceAPI.getWorkspace(), methodInfo)).show(); + } + @Override public MethodContextBuilder setOwnerInfo(CommonClassInfo info) { this.ownerInfo = info; diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index af8e96e9a..f58881b7f 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -3,6 +3,7 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.scene.control.Label; @@ -32,6 +33,8 @@ import me.coley.recaf.util.threading.ThreadUtil; import me.coley.recaf.workspace.Workspace; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -55,12 +58,16 @@ public enum CallGraphMode { } } - public MethodCallGraphPane(Workspace workspace, CallGraphMode mode) { + public MethodCallGraphPane(@Nonnull Workspace workspace, @Nonnull CallGraphMode mode, @Nullable ObservableObjectValue methodInfoObservable) { this.mode = mode; this.workspace = workspace; currentMethod.addListener((ChangeListener) this::onUpdateMethod); graphTreeView.onUpdate(classInfo); setCenter(graphTreeView); + if (methodInfoObservable != null) currentMethod.bind(methodInfoObservable); + } + public MethodCallGraphPane(@Nonnull Workspace workspace, @Nonnull CallGraphMode mode) { + this(workspace, mode, null); } private void onUpdateMethod( diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java new file mode 100644 index 000000000..2d0c836c6 --- /dev/null +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java @@ -0,0 +1,40 @@ +package me.coley.recaf.ui.pane; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.layout.BorderPane; +import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.ui.docking.DockTab; +import me.coley.recaf.ui.docking.DockingRegion; +import me.coley.recaf.ui.docking.RecafDockingManager; +import me.coley.recaf.workspace.Workspace; + +import javax.annotation.Nonnull; + +public class MethodCallGraphsPane extends BorderPane { + + private final ObjectProperty currentMethodInfo; + + public MethodCallGraphsPane(@Nonnull Workspace workspace, @Nonnull MethodInfo methodInfo) { + currentMethodInfo = new SimpleObjectProperty<>(methodInfo); + DockingWrapperPane wrapper = DockingWrapperPane.builder() + .title(currentMethodInfo.map(method -> "Calls: " + method.getOwner() + "#" + method.getName() + method.getDescriptor())) + .content(new MethodCallGraphPane(workspace, MethodCallGraphPane.CallGraphMode.CALLS, currentMethodInfo)) + .size(600, 300) + .build(); + DockingRegion region = wrapper.getTab().getParent(); + RecafDockingManager.getInstance().createTabIn(region, + () -> new DockTab(currentMethodInfo.map(method -> "Callers: " + method.getOwner() + "#" + method.getName() + method.getDescriptor()), + new MethodCallGraphPane(workspace, MethodCallGraphPane.CallGraphMode.CALLERS, currentMethodInfo))); + region.getDockTabs().forEach(t -> t.setClosable(true)); + setCenter(wrapper); + } + + public MethodInfo getCurrentMethodInfo() { + return currentMethodInfo.get(); + } + + public ObjectProperty currentMethodInfoProperty() { + return currentMethodInfo; + } +} diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index fb9fc03c5..2ce8408d0 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -67,6 +67,7 @@ menu.view.hierarchy=Class hierarchy menu.view.hierarchy.children=Children menu.view.hierarchy.parents=Parents menu.view.methodcfg=Control flow graph +menu.view.methodcallgraph=Call Graph menu.tab.close=Close menu.tab.closeothers=Close others menu.tab.closeall=Close all From 0d250585411ba9bc295dbcdd28b29859b12b0252 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Fri, 28 Oct 2022 00:39:57 +0200 Subject: [PATCH 07/19] Bundle Call Graph in one TabPane for Docking --- .../ui/context/MethodContextBuilder.java | 8 ++++- .../recaf/ui/pane/MethodCallGraphsPane.java | 32 +++++++++---------- .../main/resources/translations/en_US.lang | 2 ++ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java b/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java index c20dc392b..ca6f6d733 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java @@ -21,6 +21,7 @@ import me.coley.recaf.ui.docking.DockTab; import me.coley.recaf.ui.docking.RecafDockingManager; import me.coley.recaf.ui.docking.impl.ClassTab; +import me.coley.recaf.ui.pane.DockingWrapperPane; import me.coley.recaf.ui.pane.MethodCallGraphsPane; import me.coley.recaf.ui.pane.SearchPane; import me.coley.recaf.ui.pane.assembler.AssemblerPane; @@ -101,7 +102,12 @@ public ContextMenu build() { } private void callGraph() { - new GenericWindow(new MethodCallGraphsPane(WorkspaceAPI.getWorkspace(), methodInfo)).show(); + final MethodCallGraphsPane graphsPane = new MethodCallGraphsPane(WorkspaceAPI.getWorkspace(), methodInfo); + new GenericWindow(DockingWrapperPane.builder() + .title(graphsPane.currentMethodInfoProperty().map(method -> method.getOwner() + "#" + method.getName() + method.getDescriptor())) + .content(graphsPane) + .size(600, 300) + .build()).show(); } @Override diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java index 2d0c836c6..6b27a7947 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphsPane.java @@ -2,32 +2,32 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.scene.layout.BorderPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import me.coley.recaf.code.MethodInfo; -import me.coley.recaf.ui.docking.DockTab; -import me.coley.recaf.ui.docking.DockingRegion; -import me.coley.recaf.ui.docking.RecafDockingManager; +import me.coley.recaf.ui.util.Lang; import me.coley.recaf.workspace.Workspace; import javax.annotation.Nonnull; -public class MethodCallGraphsPane extends BorderPane { +public class MethodCallGraphsPane extends TabPane { private final ObjectProperty currentMethodInfo; public MethodCallGraphsPane(@Nonnull Workspace workspace, @Nonnull MethodInfo methodInfo) { currentMethodInfo = new SimpleObjectProperty<>(methodInfo); - DockingWrapperPane wrapper = DockingWrapperPane.builder() - .title(currentMethodInfo.map(method -> "Calls: " + method.getOwner() + "#" + method.getName() + method.getDescriptor())) - .content(new MethodCallGraphPane(workspace, MethodCallGraphPane.CallGraphMode.CALLS, currentMethodInfo)) - .size(600, 300) - .build(); - DockingRegion region = wrapper.getTab().getParent(); - RecafDockingManager.getInstance().createTabIn(region, - () -> new DockTab(currentMethodInfo.map(method -> "Callers: " + method.getOwner() + "#" + method.getName() + method.getDescriptor()), - new MethodCallGraphPane(workspace, MethodCallGraphPane.CallGraphMode.CALLERS, currentMethodInfo))); - region.getDockTabs().forEach(t -> t.setClosable(true)); - setCenter(wrapper); + getTabs().add(creatTab(workspace, MethodCallGraphPane.CallGraphMode.CALLS)); + getTabs().add(creatTab(workspace, MethodCallGraphPane.CallGraphMode.CALLERS)); + } + + @Nonnull + private Tab creatTab(Workspace workspace, + @Nonnull MethodCallGraphPane.CallGraphMode mode) { + Tab tab = new Tab(); + tab.setContent(new MethodCallGraphPane(workspace, mode, currentMethodInfo)); + tab.textProperty().bind(Lang.getBinding("menu.view.methodcallgraph." + mode.name().toLowerCase())); + tab.setClosable(false); + return tab; } public MethodInfo getCurrentMethodInfo() { diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index 2ce8408d0..fe6fadb4a 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -68,6 +68,8 @@ menu.view.hierarchy.children=Children menu.view.hierarchy.parents=Parents menu.view.methodcfg=Control flow graph menu.view.methodcallgraph=Call Graph +menu.view.methodcallgraph.calls=Calls +menu.view.methodcallgraph.callers=Callers menu.tab.close=Close menu.tab.closeothers=Close others menu.tab.closeall=Close all From 5b57ea259fc4b9a971d5bbdbdef4b602d5ea3ddd Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Fri, 28 Oct 2022 01:14:12 +0200 Subject: [PATCH 08/19] Add option to change current method in Call Graph --- .../recaf/ui/pane/MethodCallGraphPane.java | 28 ++++++++----------- .../main/resources/translations/en_US.lang | 1 + 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index f58881b7f..5e554c809 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -3,13 +3,9 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; -import javafx.scene.control.Label; -import javafx.scene.control.TreeCell; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeView; +import javafx.scene.control.*; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; @@ -27,6 +23,7 @@ import me.coley.recaf.ui.behavior.Updatable; import me.coley.recaf.ui.context.ContextBuilder; import me.coley.recaf.ui.util.Icons; +import me.coley.recaf.ui.util.Lang; import me.coley.recaf.util.AccessFlag; import me.coley.recaf.util.EscapeUtil; import me.coley.recaf.util.threading.FxThreadUtil; @@ -58,16 +55,13 @@ public enum CallGraphMode { } } - public MethodCallGraphPane(@Nonnull Workspace workspace, @Nonnull CallGraphMode mode, @Nullable ObservableObjectValue methodInfoObservable) { + public MethodCallGraphPane(@Nonnull Workspace workspace, @Nonnull CallGraphMode mode, @Nullable ObjectProperty methodInfoObservable) { this.mode = mode; this.workspace = workspace; currentMethod.addListener((ChangeListener) this::onUpdateMethod); graphTreeView.onUpdate(classInfo); setCenter(graphTreeView); - if (methodInfoObservable != null) currentMethod.bind(methodInfoObservable); - } - public MethodCallGraphPane(@Nonnull Workspace workspace, @Nonnull CallGraphMode mode) { - this(workspace, mode, null); + if (methodInfoObservable != null) currentMethod.bindBidirectional(methodInfoObservable); } private void onUpdateMethod( @@ -133,7 +127,7 @@ private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry ca continue; } workingStack.pop(); - if(!visitedMethods.isEmpty()) visitedMethods.pop(); + if (!visitedMethods.isEmpty()) visitedMethods.pop(); depth--; } return root; @@ -151,10 +145,6 @@ private CallGraphItem(MethodInfo info, boolean recursive) { super(info); this.recursive = recursive; } - - private CallGraphItem(MethodInfo info) { - this(info, false); - } } /** @@ -192,7 +182,13 @@ protected void updateItem(MethodInfo item, boolean empty) { // setText(TextDisplayUtil.escapeShortenPath(item.getOwner()) + "#" + item.getName()); ClassInfo classInfo = workspace.getResources().getClass(item.getOwner()); if (classInfo != null) { - setContextMenu(ContextBuilder.forMethod(classInfo, item).setDeclaration(false).build()); + final ContextMenu contextMenu = ContextBuilder.forMethod(classInfo, item).setDeclaration(false).build(); + final MenuItem focusItem = new MenuItem(); + // Add Icon: focusItem.setGraphic(Icons.getIconView(Icons.FOCUS)); + focusItem.textProperty().bind(Lang.getBinding("menu.view.methodcallgraph.focus")); + focusItem.setOnAction(e -> currentMethod.set(item)); + contextMenu.getItems().add(1, focusItem); + setContextMenu(contextMenu); // Override the double click behavior to open the class. Doesn't work using the "setOn..." methods. onClickFilter = (MouseEvent e) -> { if (e.getClickCount() >= 2 && e.getButton().equals(MouseButton.PRIMARY)) { diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index fe6fadb4a..0db375ce7 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -70,6 +70,7 @@ menu.view.methodcfg=Control flow graph menu.view.methodcallgraph=Call Graph menu.view.methodcallgraph.calls=Calls menu.view.methodcallgraph.callers=Callers +menu.view.methodcallgraph.focus=Focus on Method menu.tab.close=Close menu.tab.closeothers=Close others menu.tab.closeall=Close all From 4166e87dd72eef2b41f0bad674ccd503d0ca5f7e Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Fri, 28 Oct 2022 01:27:37 +0200 Subject: [PATCH 09/19] Add focus icon --- .../coley/recaf/ui/pane/MethodCallGraphPane.java | 2 +- .../main/java/me/coley/recaf/ui/util/Icons.java | 1 + recaf-ui/src/main/resources/icons/focus.png | Bin 0 -> 3098 bytes 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 recaf-ui/src/main/resources/icons/focus.png diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index 5e554c809..d45df18a7 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -184,7 +184,7 @@ protected void updateItem(MethodInfo item, boolean empty) { if (classInfo != null) { final ContextMenu contextMenu = ContextBuilder.forMethod(classInfo, item).setDeclaration(false).build(); final MenuItem focusItem = new MenuItem(); - // Add Icon: focusItem.setGraphic(Icons.getIconView(Icons.FOCUS)); + focusItem.setGraphic(Icons.getIconView(Icons.FOCUS)); focusItem.textProperty().bind(Lang.getBinding("menu.view.methodcallgraph.focus")); focusItem.setOnAction(e -> currentMethod.set(item)); contextMenu.getItems().add(1, focusItem); diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java b/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java index 3ccf02359..71c9507c4 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/util/Icons.java @@ -33,6 +33,7 @@ */ public class Icons { private static final Image EMPTY_IMAGE = new WritableImage(1, 1); + public static final String FOCUS = "icons/focus.png"; // Class definitions public static final String CLASS = "icons/class/class.png"; public static final String CLASS_ANONYMOUS = "icons/class/class_anonymous.png"; diff --git a/recaf-ui/src/main/resources/icons/focus.png b/recaf-ui/src/main/resources/icons/focus.png new file mode 100644 index 0000000000000000000000000000000000000000..86f0288de29927e738292f8cdf81cfdddc9b632b GIT binary patch literal 3098 zcmV+#4CV8QP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H13#Um$ zK~#90?VW3o6x9{S|L4vO`v8H`R1|T8TIvcW#;CEhlw`rJm|c2%XI3VNqOnquN+}Vo z7_ed)#JrR!Rg_{Se6V~Wm}ckE+q)Z6$c|N5ltx5NQ2{EJ3ZjxkU0&{EX70&{U6ibJ z_wAnN4)gok=bm%t{-vXz@|!ay7r-!#x?oMcM&Mfjv;YVc zIHV-dHYPk%6BHX;T3QYjIG!UV6CfV%o=F1BK)e7zT}e6te9nXyBw^L{nzpMH9Vog0 zrfFU*S=04rCn6yLt|@}Giv6@V!P4VFI+0$7nA8hAV!jb;lOsGtJGy1UO6S>6cX zy9EnUNC>Y5z&EtDwY^)Y5CwY?OITJ|WchWB*TDw>#uZtnHxpf5KPps+yx#$-R4Uki z@bGF7{X9=D_W`sO;Dufq5wMYgtC62~2?lsgdT8*Td_R)%Du7`ab)lO24FI?2;3f;k zOH9}lfb7$C{a*l(FfC6V4eJ^KfN7fNNeavbV?MywB8R)4f%w76lWx4VscF!!kFn%U zfJ7oO6(VQ?nC;WWUI1%@0_!zR+y52+J_R@)%d)0tnHDkg?Et>vv!5@ghicoS(dL6b z+y5VV5ny!i`v6?xqxl|UJapi|wC5HtzASAUBM$-`!!V2q!J7I-1ilx*`Cj~vvLiiI zJ0~9^F3w@zJKHri`0$J_R^NDwPWMA3AaefCm9g_TnFm zZNUJVHBI~6Q@b(bGz&_lQo&G7{RYp2&*&787qqv{dst=g3q?~?QzooyYk~r^7>Ijn zOt>m7@tk28Ilay2v>ZJ;+fn|9OTeH6?vAu;f666pz8U?W(-BWvcM#!G05vM@2#ADg zY99e`x8K;l?|@{|n(t|<$#|coxGJKT*We#dq+MGpAg=w;L6~Hx}SNC<4FLcr{@d+4=9>{5E2-dgMW-@wE192pcRbw zRqToI`&jJ7XYAqQ_2R!A(KKzpAgL3evivZag6W66_*WJn zk?6T(bTh>j2JX^zeTSEhlry@nzs*F;RBQ>j*)+|IRedW9z(U>!m7Od`XE{$b{St1Q z_ZUD)1yhnFex>SHNq|nvx{!bl1=GX90G6r-A8*KvSb~upBxe}&P19^t@u?(0AWO>? zZSOzPN@k_d(b4h&5wBD*4M+&3N0#fL_V)Hp2C>SH(C(>|C#_WpI-WW3(V1%iyrW>c zz%YzSZbq&G45sCL0QGKm#3<>)TCrHX__8zu4=b2XoG_uz^|9?L01@A;5NzM@@W6Tn zyNcjHA5VKq<=ub@u4CsYKrGgK78oi^&5uW;(P0IS^rieSjgiUTXm5SnTzt-uZtpKCz+pak^Bm-L%I#HQqg~D&=vtvCy zGwiO~3XlOVQs~0IaJWTfPF#^hBCUG>xITVGHapAqn%xCrq3|kATitCc9KtKECK4Cg zU9+79G4X}unh3%R6Q<=UWzda6&vv|~x8wsU?Ejb@?f_p2OI1z;^96R7>}NrY(+bj= zlMn*FW_QhL8hT0r`f{>jyC`QbfT#+PM{LK^=@I0*lv+g@9|BY<3ibk!!q!_ARSw%- zuopnuH>%zI#>z^1arS=(09~vB1%Y+4hgw_6}f@wyGUxb&b1CWy1{DF<=4PN-VYl z1c99j@f%FjJWs){A~8*~k$~xLc0nTBv9uLnv_67scC)0wTsNCaMMCMfzw4AG?)E0! z=jxs@U!fBf34^2VR^+&C>6{e-au)!6Z7kM%mb+bLVOiF6f_u&sF*^$2m=_chyg4Hw z;VhI0gyc<7OV{&FAwJa3g^a{4aIg549S%8f+&kU@nx^e%;AOYYF!LRTVN7tds}$C) z+a3a-@I4r9ajU%Lx*94N&non(Ay`wtNWrcmIQ{ewe*%?aA0%DJP9Zs0ZUW$14}rk< zrc$Z0E(Dto)~(wfQv6)$XnJtawfupr0J^RpX2J^!0nh*J(2+Y8?8Y@;KjWjj0i5e@ zNgJclsOyrx(uRyAtiniXN9T~h1D0jEzL<Dn6A2xL(tC5zwh% zdP-X2F%_TV5MmKn3x^H%<4pfDnlq2y%c1(7!}N4}d+R?`eS6xJC&*s_=vOeU zAqmqdD|oyRi}ju*Av&Q}L+CS>!^^$+_auO>>jyyeYZY4pri%>s#N+XCn_(A^$4?WX z?BAeV;E(Q$L?Zw862o&^N_z0~C)CTh@GJ_3dJN-5)zUU)Lp&ZojRK)wFlH(>U~GG9 z$Ls1flt%F^Kr|Z71_@CR)T*E`t_anpw_29vw@hTwF-`MK3WWNgUiG&>Ao!*gD^|Ep z)yLv1F-z05z07nQ6lO&L0LILW#MVTj`!YZAiiv5OmrICO0enlb2{ZAne(Ov7&Wwxb z^I`z6@zA6Jf?r9P))K!l3k=1R)}0dK6#!nA*H=f{+Ej{ooRFN>Zqh@8_XKO|W)PrS zw5|rgV+pgnS!TqNa{0P4nM^jY2&^Sin&*EBqcc4`;Az$~mYi}!qtR$KGc<4mfX&`o za=Qp-cPC8ifl@AGuUof0lrXKOOo5$>FIitgNcB&dv?$kN-gzlqY#5uTglY!+0DQ+s z^LH3{I5RZxwA0u2`7T`?s;$2j%*&y)u=W++NDtM{EsN5{#~2kM3E=xaUF>7V69U=w zC#tZR4*`(2?go{7*O$5{y13Y(iFS zIku>kL;3o~k?aMY51dlEoiEScI}Z$PLeMHoeU2s-|Cxew1OnyZf7z;bkB!MEM-2J93nv zz={q{d!v|vOLmy&$n;Pw1F!(V#FBIZI1124&eAH?J=}a#ZpYdF5QV=hzBtZSaCU>P z>m~gbuH*t7kI~a}MkvG00M7wvF2HB!$@RMcZDDX990ZPpO00;+13?zFR82|tP07*qoM6N<$f;gk7EC2ui literal 0 HcmV?d00001 From 341cb3999a0c7713585e635547bf935e5f72a340 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 30 Oct 2022 17:10:27 +0100 Subject: [PATCH 10/19] Add MethodHandle Support for Call Graph --- .../recaf/graph/call/CallGraphRegistry.java | 95 +++++++++++++------ .../recaf/ui/pane/MethodCallGraphPane.java | 1 - 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index e4615ba24..f19a7bce4 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -8,20 +8,15 @@ import me.coley.recaf.util.MemoizedFunction; import me.coley.recaf.workspace.Workspace; import me.coley.recaf.workspace.resource.Resources; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; +import org.objectweb.asm.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; public final class CallGraphRegistry { private Map> methodMap = new HashMap<>(); @@ -52,6 +47,40 @@ public void load() { methodMap = null; } + @Nullable + private static MethodInfo resolveMethodInfo( + LinkResolver resolver, + Function classInfoFromPathResolver, + int opcode, + ClassInfo callClassInfo, + String name, + String descriptor + ) { + Result> result; + switch (opcode) { + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKEVIRTUAL: + case Opcodes.H_INVOKESPECIAL: + case Opcodes.H_INVOKEVIRTUAL: + result = resolver.resolveVirtualMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + break; + case Opcodes.INVOKESTATIC: + case Opcodes.H_INVOKESTATIC: + result = resolver.resolveStaticMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + break; + case Opcodes.INVOKEINTERFACE: + case Opcodes.H_INVOKEINTERFACE: + result = resolver.resolveInterfaceMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + break; + default: + throw new IllegalArgumentException("Opcode in visitMethodInsn must be INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE."); + } + if (result.isSuccess()) { + return result.value().member().innerValue(); + } + return null; + } + private Map getMethodMap(ClassInfo info) { return methodMap.computeIfAbsent(info, k -> k.getMethods() @@ -77,7 +106,7 @@ private void visitClass( BiFunction thisClassMethodInfoResolver, Function> otherMethodInfoResolver ) { - LinkResolver resolver = LinkResolver.jvm(); + final LinkResolver resolver = LinkResolver.jvm(); info.getClassReader().accept(new ClassVisitor(Opcodes.ASM9) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { @@ -89,6 +118,31 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str if (!vertex.visited) { vertex.visited = true; return new MethodVisitor(Opcodes.ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) { + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + if (!"java/lang/invoke/LambdaMetafactory".equals(bootstrapMethodHandle.getOwner()) + || !"metafactory".equals(bootstrapMethodHandle.getName()) + || !"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;".equals(bootstrapMethodHandle.getDesc())) { + System.out.println(bootstrapMethodHandle); + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + return; + } + Optional handleObj = (Optional) (Optional) Stream.of(bootstrapMethodArguments).filter(o -> o instanceof Handle).findFirst(); + if (handleObj.isEmpty()) { + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + return; + } + Handle handle = handleObj.get(); + switch (handle.getTag()) { + case Opcodes.H_INVOKESPECIAL: + case Opcodes.H_INVOKEVIRTUAL: + case Opcodes.H_INVOKESTATIC: + case Opcodes.H_INVOKEINTERFACE: + visitMethodInsn(handle.getTag(), handle.getOwner(), handle.getName(), handle.getDesc(), handle.isInterface()); + } + } + @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { ClassInfo callClassInfo = classInfoFromPathResolver.apply(owner); @@ -96,28 +150,13 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri return; MethodInfo call = otherMethodInfoResolver.apply(callClassInfo).apply(name, descriptor); if (call == null) { - Result> result; - switch (opcode) { - case Opcodes.INVOKESPECIAL: - case Opcodes.INVOKEVIRTUAL: - result = resolver.resolveVirtualMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); - break; - case Opcodes.INVOKESTATIC: - result = resolver.resolveStaticMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); - break; - case Opcodes.INVOKEINTERFACE: - result = resolver.resolveInterfaceMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); - break; - default: - throw new IllegalArgumentException("Opcode in visitMethodInsn must be INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE."); - } - if (result.isSuccess()) { - call = result.value().member().innerValue(); - } + call = resolveMethodInfo(resolver, classInfoFromPathResolver, opcode, callClassInfo, name, descriptor); // should it log on else here? or would it be spam? } - if (call == null) + if (call == null) { + // System.out.println("Cannot resolve " + callClassInfo.getName() + "#" + name + descriptor); return; + } MutableCallGraphVertex nestedVertex = vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); vertex.getCalls().add(nestedVertex); nestedVertex.getCallers().add(vertex); diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index d45df18a7..df83cafb0 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -75,7 +75,6 @@ private void onUpdateMethod( @Override public void onUpdate(CommonClassInfo newValue) { classInfo = newValue; - } private class CallGraphTreeView extends TreeView implements Updatable { From 40ae1227a344aa25e1b383a7c884723fd0ad988b Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Tue, 1 Nov 2022 22:28:49 +0100 Subject: [PATCH 11/19] Bump jlinker to 1.0.4 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 96f5c1ad7..ddadb9fdc 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ subprojects { javassist = 'org.javassist:javassist:3.28.0-GA' javax_annos = 'javax.annotation:javax.annotation-api:1.3.2' jelf = 'net.fornwall:jelf:0.7.0' - jlinker = 'com.github.xxDark:jlinker:1.0.3' + jlinker = 'com.github.xxDark:jlinker:1.0.4' jphantom = 'com.github.Col-E:jphantom:1.4.3' junit_api = "org.junit.jupiter:junit-jupiter-api:$junitVersion" junit_engine = "org.junit.jupiter:junit-jupiter-engine:$junitVersion" From 5533798518d8751e6cca6b4703d6a4ba709c7db9 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Tue, 1 Nov 2022 22:30:38 +0100 Subject: [PATCH 12/19] Add Workspace and Class Addition Support for CallGraphRegistry --- .../main/java/me/coley/recaf/Controller.java | 11 +- .../java/me/coley/recaf/code/MemberInfo.java | 34 ++-- .../me/coley/recaf/code/MemberSignature.java | 53 +++++ .../recaf/graph/call/CallGraphRegistry.java | 188 ++++++++++++++---- .../recaf/graph/call/CallGraphVertex.java | 11 + .../graph/call/MutableCallGraphVertex.java | 22 +- .../recaf/ui/pane/MethodCallGraphPane.java | 1 + 7 files changed, 265 insertions(+), 55 deletions(-) create mode 100644 recaf-core/src/main/java/me/coley/recaf/code/MemberSignature.java diff --git a/recaf-core/src/main/java/me/coley/recaf/Controller.java b/recaf-core/src/main/java/me/coley/recaf/Controller.java index 4989e81f5..60680b253 100644 --- a/recaf-core/src/main/java/me/coley/recaf/Controller.java +++ b/recaf-core/src/main/java/me/coley/recaf/Controller.java @@ -120,16 +120,25 @@ private void addPresentationLayerListeners(Workspace workspace) { @Override public void onNewClass(Resource resource, ClassInfo newValue) { getPresentation().workspaceLayer().onNewClass(resource, newValue); + if (services.getCallGraphRegistry() != null) { + services.getCallGraphRegistry().onNewClass(resource, newValue); + } } @Override public void onUpdateClass(Resource resource, ClassInfo oldValue, ClassInfo newValue) { getPresentation().workspaceLayer().onUpdateClass(resource, oldValue, newValue); + if (services.getCallGraphRegistry() != null) { + services.getCallGraphRegistry().onUpdateClass(resource, oldValue, newValue); + } } @Override public void onRemoveClass(Resource resource, ClassInfo oldValue) { getPresentation().workspaceLayer().onRemoveClass(resource, oldValue); + if (services.getCallGraphRegistry() != null) { + services.getCallGraphRegistry().onRemoveClass(resource, oldValue); + } } }; ResourceDexClassListener dexListener = new ResourceDexClassListener() { @@ -145,7 +154,7 @@ public void onRemoveDexClass(Resource resource, String dexName, DexClassInfo old @Override public void onUpdateDexClass(Resource resource, String dexName, - DexClassInfo oldValue, DexClassInfo newValue) { + DexClassInfo oldValue, DexClassInfo newValue) { getPresentation().workspaceLayer().onUpdateDexClass(resource, dexName, oldValue, newValue); } }; diff --git a/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java b/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java index b5f8492f7..1a0a7a849 100644 --- a/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java +++ b/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java @@ -9,11 +9,9 @@ * @author Matt Coley */ public abstract class MemberInfo implements AccessibleInfo, ItemInfo { - private final String owner; - private final String name; - private final String descriptor; private final String signature; private final int access; + private final MemberSignature memberSignature; /** * @param owner @@ -25,12 +23,10 @@ public abstract class MemberInfo implements AccessibleInfo, ItemInfo { * @param access * Member access modifiers. */ - public MemberInfo(String owner, String name, String descriptor, String signature, int access) { - this.name = name; - this.owner = owner; - this.descriptor = descriptor; + public MemberInfo(String owner, String name, String descriptor, @Nullable String signature, int access) { this.signature = signature; this.access = access; + memberSignature = new MemberSignature(owner, name, descriptor); } /** @@ -49,19 +45,19 @@ public boolean isField() { * @return Name of type defining the member. */ public String getOwner() { - return owner; + return memberSignature.owner; } @Override public String getName() { - return name; + return memberSignature.name; } /** * @return Member descriptor. */ public String getDescriptor() { - return descriptor; + return memberSignature.descriptor; } /** @@ -72,6 +68,10 @@ public String getSignature() { return signature; } + public MemberSignature getMemberSignature() { + return memberSignature; + } + /** * @return Member access modifiers. */ @@ -85,17 +85,17 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; MemberInfo that = (MemberInfo) o; return access == that.access && - Objects.equals(owner, that.owner) && + Objects.equals(memberSignature.owner, that.memberSignature.owner) && Objects.equals(getName(), that.getName()) && - Objects.equals(descriptor, that.descriptor) && + Objects.equals(memberSignature.descriptor, that.memberSignature.descriptor) && Objects.equals(signature, that.signature); } @Override public int hashCode() { - int result = Objects.hashCode(owner); - result = 31 * result + Objects.hashCode(name); - result = 31 * result + Objects.hashCode(descriptor); + int result = Objects.hashCode(memberSignature.owner); + result = 31 * result + Objects.hashCode(memberSignature.name); + result = 31 * result + Objects.hashCode(memberSignature.descriptor); result = 31 * result + Objects.hashCode(signature); result = 31 * result + access; return result; @@ -104,9 +104,9 @@ public int hashCode() { @Override public String toString() { return "MemberInfo{" + - "owner='" + owner + '\'' + + "owner='" + memberSignature.owner + '\'' + ", name='" + getName() + '\'' + - ", descriptor='" + descriptor + '\'' + + ", descriptor='" + memberSignature.descriptor + '\'' + ", access=" + access + '}'; } diff --git a/recaf-core/src/main/java/me/coley/recaf/code/MemberSignature.java b/recaf-core/src/main/java/me/coley/recaf/code/MemberSignature.java new file mode 100644 index 000000000..59210c112 --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/code/MemberSignature.java @@ -0,0 +1,53 @@ +package me.coley.recaf.code; + +import javax.annotation.Nonnull; + +public final class MemberSignature { + @Nonnull + final String owner; + @Nonnull + final String name; + @Nonnull + final String descriptor; + + public MemberSignature(@Nonnull String owner, @Nonnull String name, @Nonnull String descriptor) { + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + } + + @Nonnull + public String getOwner() { + return owner; + } + + @Nonnull + public String getName() { + return name; + } + + @Nonnull + public String getDescriptor() { + return descriptor; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MemberSignature that = (MemberSignature) o; + + if (!owner.equals(that.owner)) return false; + if (!name.equals(that.name)) return false; + return descriptor.equals(that.descriptor); + } + + @Override + public int hashCode() { + int result = owner.hashCode(); + result = 31 * result + name.hashCode(); + result = 31 * result + descriptor.hashCode(); + return result; + } +} diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index f19a7bce4..41b1b5c69 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -4,11 +4,18 @@ import dev.xdark.jlinker.Result; import me.coley.recaf.code.ClassInfo; import me.coley.recaf.code.FieldInfo; +import me.coley.recaf.code.MemberSignature; import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.util.CancelSignal; import me.coley.recaf.util.MemoizedFunction; +import me.coley.recaf.util.logging.Logging; import me.coley.recaf.workspace.Workspace; +import me.coley.recaf.workspace.WorkspaceListener; +import me.coley.recaf.workspace.resource.Resource; +import me.coley.recaf.workspace.resource.ResourceClassListener; import me.coley.recaf.workspace.resource.Resources; import org.objectweb.asm.*; +import org.slf4j.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -16,11 +23,12 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; -public final class CallGraphRegistry { +public final class CallGraphRegistry implements WorkspaceListener, ResourceClassListener { + private static final Logger LOGGER = Logging.get(CallGraphRegistry.class); private Map> methodMap = new HashMap<>(); private final Map vertexMap = new HashMap<>(); + private final Map> unresolvedCalls = new HashMap<>(); private final Workspace workspace; public CallGraphRegistry(Workspace workspace) { @@ -29,6 +37,7 @@ public CallGraphRegistry(Workspace workspace) { public static CallGraphRegistry createAndLoad(Workspace workspace) { CallGraphRegistry registry = new CallGraphRegistry(workspace); + workspace.addListener(registry); registry.load(); return registry; } @@ -44,7 +53,8 @@ public void load() { = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); // seems like a hack tho, needs feedback! resources.getClasses().forEach(info -> visitClass(info, classInfoFromPathResolver, methodMapGetter)); - methodMap = null; + methodMap = new HashMap<>(); + LOGGER.info("Loaded {} vertices, {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); } @Nullable @@ -96,17 +106,9 @@ private void visitClass( ClassInfo info, Function classInfoFromPathResolver, Function> otherMethodInfoResolver - ) { - visitClass(info, classInfoFromPathResolver, otherMethodInfoResolver.apply(info), otherMethodInfoResolver); - } - - private void visitClass( - ClassInfo info, - Function classInfoFromPathResolver, - BiFunction thisClassMethodInfoResolver, - Function> otherMethodInfoResolver ) { final LinkResolver resolver = LinkResolver.jvm(); + BiFunction thisClassMethodInfoResolver = otherMethodInfoResolver.apply(info); info.getClassReader().accept(new ClassVisitor(Opcodes.ASM9) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { @@ -124,16 +126,15 @@ public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootst if (!"java/lang/invoke/LambdaMetafactory".equals(bootstrapMethodHandle.getOwner()) || !"metafactory".equals(bootstrapMethodHandle.getName()) || !"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;".equals(bootstrapMethodHandle.getDesc())) { - System.out.println(bootstrapMethodHandle); super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); return; } - Optional handleObj = (Optional) (Optional) Stream.of(bootstrapMethodArguments).filter(o -> o instanceof Handle).findFirst(); - if (handleObj.isEmpty()) { + Object handleObj = bootstrapMethodArguments.length == 3 ? bootstrapMethodArguments[1] : null; + if (!(handleObj instanceof Handle)) { super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); return; } - Handle handle = handleObj.get(); + Handle handle = (Handle) handleObj; switch (handle.getTag()) { case Opcodes.H_INVOKESPECIAL: case Opcodes.H_INVOKEVIRTUAL: @@ -145,21 +146,7 @@ public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootst @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - ClassInfo callClassInfo = classInfoFromPathResolver.apply(owner); - if (callClassInfo == null) - return; - MethodInfo call = otherMethodInfoResolver.apply(callClassInfo).apply(name, descriptor); - if (call == null) { - call = resolveMethodInfo(resolver, classInfoFromPathResolver, opcode, callClassInfo, name, descriptor); - // should it log on else here? or would it be spam? - } - if (call == null) { - // System.out.println("Cannot resolve " + callClassInfo.getName() + "#" + name + descriptor); - return; - } - MutableCallGraphVertex nestedVertex = vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); - vertex.getCalls().add(nestedVertex); - nestedVertex.getCallers().add(vertex); + visitMethodInstruction(opcode, owner, name, descriptor, vertex, classInfoFromPathResolver, otherMethodInfoResolver, resolver, vertexMap); } }; } @@ -168,7 +155,133 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri }, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); } - private static final class Descriptor { + private void visitMethodInstruction( + int opcode, String owner, String name, String descriptor, MutableCallGraphVertex vertex, + Function classInfoFromPathResolver, + Function> otherMethodInfoResolver, + LinkResolver resolver, + Map vertexMap + ) { + ClassInfo callClassInfo = classInfoFromPathResolver.apply(owner); + if (callClassInfo == null) { + unresolvedCalls.computeIfAbsent(owner, k -> new HashSet<>()).add(new UnresolvedCall(opcode, owner, name, descriptor, vertex)); + return; + } + MethodInfo call = otherMethodInfoResolver.apply(callClassInfo).apply(name, descriptor); + if (call == null) { + try { + call = resolveMethodInfo(resolver, classInfoFromPathResolver, opcode, callClassInfo, name, descriptor); + } catch (CancelSignal ignored) {} + // should it log on else here? or would it be spam? + } + if (call == null) { + unresolvedCalls.computeIfAbsent(owner, k -> new HashSet<>()).add(new UnresolvedCall(opcode, owner, name, descriptor, vertex)); + return; + } + MutableCallGraphVertex nestedVertex = + vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); + vertex.getCalls().add(nestedVertex); + nestedVertex.getCallers().add(vertex); + } + + private void updateUnresolved( + Function classInfoFromPathResolver, + Function> otherMethodInfoResolver, + LinkResolver resolver + ) { + Set calls = unresolvedCalls.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + unresolvedCalls.clear(); + calls.forEach(unresolvedCall -> visitMethodInstruction( + unresolvedCall.opcode, + unresolvedCall.owner, + unresolvedCall.name, + unresolvedCall.descriptor, + unresolvedCall.vertex, + classInfoFromPathResolver, + otherMethodInfoResolver, + resolver, + vertexMap) + ); + } + + @Override + public void onAddLibrary(Workspace workspace, Resource library) { + Function classInfoFromPathResolver + = MemoizedFunction.memoize(path -> + library.getClasses().stream().filter(c -> c.getName().equals(path)).findAny().orElseGet(() -> workspace.getResources().getClass(path)) + ); + Function> methodMapGetter + = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); + updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); + library.getClasses().forEach(c -> visitClass(c, classInfoFromPathResolver, methodMapGetter)); + LOGGER.info("There are now {} vertices, and {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + } + + @Override + public void onNewClass(Resource resource, ClassInfo newValue) { + final Function classInfoFromPathResolver + = MemoizedFunction.memoize(path -> workspace.getResources().getClass(path)); + final Function> methodMapGetter + = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); + updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); + visitClass(newValue, classInfoFromPathResolver, methodMapGetter); + } + + @Override + public void onRemoveLibrary(Workspace workspace, Resource library) { + } + + @Override + public void onRemoveClass(Resource resource, ClassInfo oldValue) { + + } + + @Override + public void onUpdateClass(Resource resource, ClassInfo oldValue, ClassInfo newValue) { + + } + + private static class UnresolvedCall { + final int opcode; + final String owner; + final String name; + final String descriptor; + final MutableCallGraphVertex vertex; + + private UnresolvedCall(int opcode, String owner, String name, String descriptor, MutableCallGraphVertex vertex) { + this.opcode = opcode; + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + this.vertex = vertex; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UnresolvedCall that = (UnresolvedCall) o; + + if (opcode != that.opcode) return false; + if (!owner.equals(that.owner)) return false; + if (!name.equals(that.name)) return false; + if (!descriptor.equals(that.descriptor)) return false; + return vertex.equals(that.vertex); + } + + @Override + public int hashCode() { + int result = opcode; + result = 31 * result + owner.hashCode(); + result = 31 * result + name.hashCode(); + result = 31 * result + descriptor.hashCode(); + result = 31 * result + vertex.hashCode(); + return result; + } + } + + protected static final class Descriptor { private final String name, desc; Descriptor(String name, String desc) { @@ -176,9 +289,13 @@ private static final class Descriptor { this.desc = desc; } + Descriptor(MemberSignature signature) { + this.name = signature.getName(); + this.desc = signature.getDescriptor(); + } + Descriptor(MethodInfo info) { - this.name = info.getName(); - this.desc = info.getDescriptor(); + this(info.getMemberSignature()); } @Override @@ -217,7 +334,8 @@ public dev.xdark.jlinker.ClassInfo superClass() { String superName = node.getSuperName(); if (superName == null) return null; final ClassInfo classInfo = fn.apply(superName); - return classInfo == null ? null : classInfo(classInfo, fn); + if (classInfo == null) throw CancelSignal.get(); + return classInfo(classInfo, fn); } @Override diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java index a150928e7..85ff8ddf0 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java @@ -1,14 +1,25 @@ package me.coley.recaf.graph.call; +import me.coley.recaf.code.MemberSignature; import me.coley.recaf.code.MethodInfo; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collection; public interface CallGraphVertex { + /** + * @return MethodInfo of this vertex, it's null when it's not resolved: a method which isn't in available in the workspace. + */ + @Nullable MethodInfo getMethodInfo(); + @Nonnull + MemberSignature getSignature(); + Collection getCallers(); Collection getCalls(); + } diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java index 4028e47fa..60343883e 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java @@ -1,24 +1,42 @@ package me.coley.recaf.graph.call; +import me.coley.recaf.code.MemberSignature; import me.coley.recaf.code.MethodInfo; +import javax.annotation.Nonnull; import java.util.*; public final class MutableCallGraphVertex implements CallGraphVertex { private final Set callers = Collections.newSetFromMap(new LinkedHashMap<>()); private final Set calls = Collections.newSetFromMap(new LinkedHashMap<>()); - private final MethodInfo methodInfo; + MethodInfo methodInfo; + private MemberSignature signature; boolean visited; - public MutableCallGraphVertex(MethodInfo methodInfo) { + public MutableCallGraphVertex(@Nonnull MethodInfo methodInfo) { this.methodInfo = methodInfo; } + public MutableCallGraphVertex(@Nonnull MemberSignature signature) { + this.signature = signature; + } + + public void setMethodInfo(MethodInfo methodInfo) { + this.methodInfo = methodInfo; + this.signature = methodInfo.getMemberSignature(); + } + @Override public MethodInfo getMethodInfo() { return methodInfo; } + @Nonnull + @Override + public MemberSignature getSignature() { + return signature; + } + @Override public Collection getCallers() { return callers; diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index df83cafb0..dc21ace8a 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -113,6 +113,7 @@ private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry ca if (vertex != null) { final List newTodo = childrenGetter.apply(vertex) .stream().map(CallGraphVertex::getMethodInfo) + .filter(Objects::nonNull) .map(c -> new CallGraphItem(c, visitedMethods.contains(c))) .filter(i -> { if (i.getValue() == null) return false; From 3e23edc57b30322bf60995f814e1eea4f8cfb3b4 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 6 Nov 2022 02:18:03 +0100 Subject: [PATCH 13/19] Add library and class removal It just reloads the whole damn thing, as it leaves some zombie vertices and calls. --- .../java/me/coley/recaf/code/MemberInfo.java | 3 + .../recaf/graph/call/CallGraphRegistry.java | 186 +++++++++++++----- .../recaf/graph/call/CallGraphVertex.java | 6 +- .../graph/call/MutableCallGraphVertex.java | 31 +-- .../recaf/ui/pane/MethodCallGraphPane.java | 11 +- 5 files changed, 160 insertions(+), 77 deletions(-) diff --git a/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java b/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java index 1a0a7a849..5b7fa06a5 100644 --- a/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java +++ b/recaf-core/src/main/java/me/coley/recaf/code/MemberInfo.java @@ -1,5 +1,6 @@ package me.coley.recaf.code; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Objects; @@ -11,6 +12,7 @@ public abstract class MemberInfo implements AccessibleInfo, ItemInfo { private final String signature; private final int access; + @Nonnull private final MemberSignature memberSignature; /** @@ -68,6 +70,7 @@ public String getSignature() { return signature; } + @Nonnull public MemberSignature getMemberSignature() { return memberSignature; } diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index 41b1b5c69..ab9a50651 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -26,7 +26,7 @@ public final class CallGraphRegistry implements WorkspaceListener, ResourceClassListener { private static final Logger LOGGER = Logging.get(CallGraphRegistry.class); - private Map> methodMap = new HashMap<>(); + private final Map> methodMap = new HashMap<>(); private final Map vertexMap = new HashMap<>(); private final Map> unresolvedCalls = new HashMap<>(); private final Workspace workspace; @@ -42,6 +42,12 @@ public static CallGraphRegistry createAndLoad(Workspace workspace) { return registry; } + public void clear() { + methodMap.clear(); + vertexMap.clear(); + unresolvedCalls.clear(); + } + public @Nullable CallGraphVertex getVertex(MethodInfo info) { return vertexMap.get(info); } @@ -53,8 +59,8 @@ public void load() { = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); // seems like a hack tho, needs feedback! resources.getClasses().forEach(info -> visitClass(info, classInfoFromPathResolver, methodMapGetter)); - methodMap = new HashMap<>(); - LOGGER.info("Loaded {} vertices, {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + methodMap.clear(); + LOGGER.debug("Loaded {} vertices, {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); } @Nullable @@ -109,50 +115,7 @@ private void visitClass( ) { final LinkResolver resolver = LinkResolver.jvm(); BiFunction thisClassMethodInfoResolver = otherMethodInfoResolver.apply(info); - info.getClassReader().accept(new ClassVisitor(Opcodes.ASM9) { - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - MethodInfo info = thisClassMethodInfoResolver.apply(name, descriptor); - if (info == null) - return null; - Map vertexMap = CallGraphRegistry.this.vertexMap; - MutableCallGraphVertex vertex = vertexMap.computeIfAbsent(info, MutableCallGraphVertex::new); - if (!vertex.visited) { - vertex.visited = true; - return new MethodVisitor(Opcodes.ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) { - - @Override - public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { - if (!"java/lang/invoke/LambdaMetafactory".equals(bootstrapMethodHandle.getOwner()) - || !"metafactory".equals(bootstrapMethodHandle.getName()) - || !"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;".equals(bootstrapMethodHandle.getDesc())) { - super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); - return; - } - Object handleObj = bootstrapMethodArguments.length == 3 ? bootstrapMethodArguments[1] : null; - if (!(handleObj instanceof Handle)) { - super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); - return; - } - Handle handle = (Handle) handleObj; - switch (handle.getTag()) { - case Opcodes.H_INVOKESPECIAL: - case Opcodes.H_INVOKEVIRTUAL: - case Opcodes.H_INVOKESTATIC: - case Opcodes.H_INVOKEINTERFACE: - visitMethodInsn(handle.getTag(), handle.getOwner(), handle.getName(), handle.getDesc(), handle.isInterface()); - } - } - - @Override - public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - visitMethodInstruction(opcode, owner, name, descriptor, vertex, classInfoFromPathResolver, otherMethodInfoResolver, resolver, vertexMap); - } - }; - } - return null; - } - }, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); + info.getClassReader().accept(new MethodCallsResolverClassVisitor(thisClassMethodInfoResolver, classInfoFromPathResolver, otherMethodInfoResolver, resolver), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); } private void visitMethodInstruction( @@ -178,8 +141,7 @@ private void visitMethodInstruction( unresolvedCalls.computeIfAbsent(owner, k -> new HashSet<>()).add(new UnresolvedCall(opcode, owner, name, descriptor, vertex)); return; } - MutableCallGraphVertex nestedVertex = - vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); + MutableCallGraphVertex nestedVertex = vertexMap.computeIfAbsent(call, MutableCallGraphVertex::new); vertex.getCalls().add(nestedVertex); nestedVertex.getCallers().add(vertex); } @@ -206,39 +168,97 @@ private void updateUnresolved( @Override public void onAddLibrary(Workspace workspace, Resource library) { + LOGGER.debug("Adding {} classes...", library.getClasses().size()); Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> library.getClasses().stream().filter(c -> c.getName().equals(path)).findAny().orElseGet(() -> workspace.getResources().getClass(path)) ); Function> methodMapGetter - = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); + = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); library.getClasses().forEach(c -> visitClass(c, classInfoFromPathResolver, methodMapGetter)); - LOGGER.info("There are now {} vertices, and {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + LOGGER.debug("There are now {} vertices, and {} unresolved calls.", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); } @Override public void onNewClass(Resource resource, ClassInfo newValue) { + LOGGER.debug("Adding {} methods...", newValue.getMethods().size()); final Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> workspace.getResources().getClass(path)); final Function> methodMapGetter - = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); + = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); visitClass(newValue, classInfoFromPathResolver, methodMapGetter); + LOGGER.debug("There are now {} vertices, and {} unresolved calls.", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); } @Override public void onRemoveLibrary(Workspace workspace, Resource library) { + // LOGGER.debug("Removing {} classes...", library.getClasses().size()); + // updateRemovedMethods(workspace, library.getClasses().values()); + // LOGGER.debug("There are now {} vertices, and {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + clear(); + load(); } @Override public void onRemoveClass(Resource resource, ClassInfo oldValue) { + // LOGGER.debug("Removing {} methods...", oldValue.getMethods().size()); + // updateRemovedMethods(workspace, Set.of(oldValue)); + // LOGGER.debug("There are now {} vertices, and {} unresolved calls.", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + clear(); + load(); + } + private void updateRemovedMethods(Workspace workspace, Collection removedClasses) { + // doesn't work as expected, as it leaves - for whatever reason - zombie vertices and calls + // so, it's just "cleaner" to reload the whole thing... + Set removedClassNames = removedClasses.stream().map(ClassInfo::getName).collect(Collectors.toSet()); + methodMap.keySet().removeIf(c -> removedClassNames.contains(c.getName())); + unresolvedCalls.keySet().removeIf(removedClassNames::contains); + unresolvedCalls.values().removeIf(cs -> { + cs.removeIf(c -> removedClassNames.contains(c.vertex.getMethodInfo().getOwner())); + return cs.isEmpty(); + }); + Set affectedVertices = vertexMap.values().stream() + .filter(v -> removedClassNames.contains(v.getMethodInfo().getOwner())) + .flatMap(vertex -> { + // removed -/> called + for (CallGraphVertex m : vertex.getCalls()) { + m.getCallers().remove(vertex); + } + // callers -> removed + return vertex.getCallers().stream(); + }) + .filter(m -> removedClassNames.contains(m.getMethodInfo().getOwner())) + .collect(Collectors.toSet()); + vertexMap.values().removeIf(v -> removedClassNames.contains(v.getMethodInfo().getOwner())); + // these are all MutableCallGraphVertex, we know it, right? riiight? + //noinspection unchecked,rawtypes + for (MutableCallGraphVertex affectedVertex : (Set) (Set) affectedVertices) { + affectedVertex.visited = false; + // callers -/> called + for (CallGraphVertex call : affectedVertex.getCalls()) { + call.getCallers().remove(affectedVertex); + } + affectedVertex.getCalls().clear(); + } + final Set affectedClasses = affectedVertices.stream() + .map(v -> v.getMethodInfo().getOwner()).collect(Collectors.toSet()); + Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> workspace.getResources().getClass(path)); + final Set existingAffectedClasses = affectedClasses.stream() + .map(classInfoFromPathResolver) + .filter(Objects::nonNull).collect(Collectors.toSet()); + LOGGER.debug("After affecting {} classes, there are now {} vertices, and {} unresolved calls. Rewiring {} existing classes.", affectedClasses.size(), vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum(), existingAffectedClasses.size()); + Function> methodMapGetter + = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); + existingAffectedClasses.forEach(classInfo -> visitClass(classInfo, classInfoFromPathResolver, methodMapGetter)); } @Override public void onUpdateClass(Resource resource, ClassInfo oldValue, ClassInfo newValue) { - + onRemoveClass(resource, oldValue); + onNewClass(resource, newValue); } private static class UnresolvedCall { @@ -329,6 +349,7 @@ public int accessFlags() { return node.getAccess(); } + @Nonnull @Override public dev.xdark.jlinker.ClassInfo superClass() { String superName = node.getSuperName(); @@ -338,6 +359,7 @@ public dev.xdark.jlinker.ClassInfo superClass() { return classInfo(classInfo, fn); } + @Nonnull @Override public List> interfaces() { return node.getInterfaces().stream().map(x -> { @@ -405,4 +427,62 @@ public boolean isPolymorphic() { } }; } + + private class MethodCallsResolverClassVisitor extends ClassVisitor { + private final BiFunction thisClassMethodInfoResolver; + private final Function classInfoFromPathResolver; + private final Function> otherMethodInfoResolver; + private final LinkResolver resolver; + + public MethodCallsResolverClassVisitor(BiFunction thisClassMethodInfoResolver, Function classInfoFromPathResolver, Function> otherMethodInfoResolver, LinkResolver resolver) { + super(Opcodes.ASM9); + this.thisClassMethodInfoResolver = thisClassMethodInfoResolver; + this.classInfoFromPathResolver = classInfoFromPathResolver; + this.otherMethodInfoResolver = otherMethodInfoResolver; + this.resolver = resolver; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodInfo info = thisClassMethodInfoResolver.apply(name, descriptor); + if (info == null) + return null; + Map vertexMap = CallGraphRegistry.this.vertexMap; + MutableCallGraphVertex vertex = vertexMap.computeIfAbsent(info, MutableCallGraphVertex::new); + if (!vertex.visited) { + vertex.visited = true; + return new MethodVisitor(Opcodes.ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) { + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + if (!"java/lang/invoke/LambdaMetafactory".equals(bootstrapMethodHandle.getOwner()) + || !"metafactory".equals(bootstrapMethodHandle.getName()) + || !"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;".equals(bootstrapMethodHandle.getDesc())) { + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + return; + } + Object handleObj = bootstrapMethodArguments.length == 3 ? bootstrapMethodArguments[1] : null; + if (!(handleObj instanceof Handle)) { + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + return; + } + Handle handle = (Handle) handleObj; + switch (handle.getTag()) { + case Opcodes.H_INVOKESPECIAL: + case Opcodes.H_INVOKEVIRTUAL: + case Opcodes.H_INVOKESTATIC: + case Opcodes.H_INVOKEINTERFACE: + visitMethodInsn(handle.getTag(), handle.getOwner(), handle.getName(), handle.getDesc(), handle.isInterface()); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + visitMethodInstruction(opcode, owner, name, descriptor, vertex, classInfoFromPathResolver, otherMethodInfoResolver, resolver, vertexMap); + } + }; + } + return null; + } + } } diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java index 85ff8ddf0..d514c3567 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphVertex.java @@ -4,15 +4,11 @@ import me.coley.recaf.code.MethodInfo; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.Collection; public interface CallGraphVertex { - /** - * @return MethodInfo of this vertex, it's null when it's not resolved: a method which isn't in available in the workspace. - */ - @Nullable + @Nonnull MethodInfo getMethodInfo(); @Nonnull diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java index 60343883e..4fe73c21f 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java @@ -7,25 +7,15 @@ import java.util.*; public final class MutableCallGraphVertex implements CallGraphVertex { - private final Set callers = Collections.newSetFromMap(new LinkedHashMap<>()); + private final Set callers = new HashSet<>(); private final Set calls = Collections.newSetFromMap(new LinkedHashMap<>()); - MethodInfo methodInfo; - private MemberSignature signature; + private final MethodInfo methodInfo; boolean visited; public MutableCallGraphVertex(@Nonnull MethodInfo methodInfo) { this.methodInfo = methodInfo; } - public MutableCallGraphVertex(@Nonnull MemberSignature signature) { - this.signature = signature; - } - - public void setMethodInfo(MethodInfo methodInfo) { - this.methodInfo = methodInfo; - this.signature = methodInfo.getMemberSignature(); - } - @Override public MethodInfo getMethodInfo() { return methodInfo; @@ -34,7 +24,7 @@ public MethodInfo getMethodInfo() { @Nonnull @Override public MemberSignature getSignature() { - return signature; + return methodInfo.getMemberSignature(); } @Override @@ -46,4 +36,19 @@ public Collection getCallers() { public Collection getCalls() { return calls; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MutableCallGraphVertex that = (MutableCallGraphVertex) o; + + return methodInfo.equals(that.methodInfo); + } + + @Override + public int hashCode() { + return methodInfo.hashCode(); + } } diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index dc21ace8a..04f4e31a3 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -26,8 +26,6 @@ import me.coley.recaf.ui.util.Lang; import me.coley.recaf.util.AccessFlag; import me.coley.recaf.util.EscapeUtil; -import me.coley.recaf.util.threading.FxThreadUtil; -import me.coley.recaf.util.threading.ThreadUtil; import me.coley.recaf.workspace.Workspace; import javax.annotation.Nonnull; @@ -89,11 +87,11 @@ public void onUpdate(CommonClassInfo newValue) { CallGraphRegistry callGraph = RecafUI.getController().getServices().getCallGraphRegistry(); final MethodInfo methodInfo = currentMethod.get(); if (methodInfo == null) setRoot(null); - else ThreadUtil.run(() -> { + else { CallGraphItem root = buildCallGraph(methodInfo, callGraph, mode.childrenGetter); root.setExpanded(true); - FxThreadUtil.run(() -> setRoot(root)); - }); + setRoot(root); + } } private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry callGraph, Function> childrenGetter) { @@ -165,6 +163,7 @@ protected void updateItem(MethodInfo item, boolean empty) { setText(null); setGraphic(null); setOnMouseClicked(null); + setContextMenu(null); if (onClickFilter != null) removeEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); } else { @@ -191,7 +190,7 @@ protected void updateItem(MethodInfo item, boolean empty) { setContextMenu(contextMenu); // Override the double click behavior to open the class. Doesn't work using the "setOn..." methods. onClickFilter = (MouseEvent e) -> { - if (e.getClickCount() >= 2 && e.getButton().equals(MouseButton.PRIMARY)) { + if (e.getButton().equals(MouseButton.PRIMARY) && e.getClickCount() >= 2) { e.consume(); CommonUX.openMember(classInfo, item); } From aa779446bb3699c1890e8df317491cc6bbe7aa42 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 6 Nov 2022 03:25:47 +0100 Subject: [PATCH 14/19] Memoize LinkResolver --- .../recaf/graph/call/CachedLinkResolver.java | 68 +++++++++++++++++ .../recaf/graph/call/CallGraphRegistry.java | 75 ++++++++++++------- 2 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java new file mode 100644 index 000000000..b978174b0 --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java @@ -0,0 +1,68 @@ +package me.coley.recaf.graph.call; + +import dev.xdark.jlinker.LinkResolver; +import dev.xdark.jlinker.Resolution; +import dev.xdark.jlinker.Result; +import me.coley.recaf.code.ClassInfo; +import me.coley.recaf.code.FieldInfo; +import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.util.MemoizedFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +class CachedLinkResolver implements LinkResolver { + + LinkResolver backedResolver = LinkResolver.jvm(); + + private final Function, BiFunction>>> + virtualMethodResolver = MemoizedFunction.memoize( + c -> MemoizedFunction.memoize((name, descriptor) -> backedResolver.resolveVirtualMethod(c, name, descriptor)) + ); + private final Function, BiFunction>>> + staticMethodResolver = MemoizedFunction.memoize( + c -> MemoizedFunction.memoize((name, descriptor) -> backedResolver.resolveStaticMethod(c, name, descriptor)) + ); + private final Function, BiFunction>>> + interfaceMethodResolver = MemoizedFunction.memoize( + c -> MemoizedFunction.memoize((name, descriptor) -> backedResolver.resolveInterfaceMethod(c, name, descriptor)) + ); + private final Function, BiFunction>>> + virtualFieldResolver = MemoizedFunction.memoize( + c -> MemoizedFunction.memoize((name, descriptor) -> backedResolver.resolveVirtualField(c, name, descriptor)) + ); + private final Function, BiFunction>>> + staticFieldResolver = MemoizedFunction.memoize( + c -> MemoizedFunction.memoize((name, descriptor) -> backedResolver.resolveStaticField(c, name, descriptor)) + ); + + @Override + public Result> resolveStaticMethod(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor, boolean itf) { + return staticMethodResolver.apply(owner).apply(name, descriptor); + } + + @Override + public Result> resolveStaticMethod(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor) { + return resolveStaticMethod(owner, name, descriptor, false); + } + + @Override + public Result> resolveVirtualMethod(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor) { + return virtualMethodResolver.apply(owner).apply(name, descriptor); + } + + @Override + public Result> resolveInterfaceMethod(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor) { + return interfaceMethodResolver.apply(owner).apply(name, descriptor); + } + + @Override + public Result> resolveStaticField(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor) { + return staticFieldResolver.apply(owner).apply(name, descriptor); + } + + @Override + public Result> resolveVirtualField(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor) { + return virtualFieldResolver.apply(owner).apply(name, descriptor); + } +} diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index ab9a50651..1da09eb96 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -56,9 +56,10 @@ public void load() { Resources resources = workspace.getResources(); Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> workspace.getResources().getClass(path)); Function> methodMapGetter - = MemoizedFunction.memoize(clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor))); + = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); // seems like a hack tho, needs feedback! - resources.getClasses().forEach(info -> visitClass(info, classInfoFromPathResolver, methodMapGetter)); + final CachedLinkResolver resolver = new CachedLinkResolver(); + resources.getClasses().forEach(info -> visitClass(info, classInfoFromPathResolver, methodMapGetter, resolver)); methodMap.clear(); LOGGER.debug("Loaded {} vertices, {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); } @@ -73,20 +74,21 @@ private static MethodInfo resolveMethodInfo( String descriptor ) { Result> result; + final dev.xdark.jlinker.ClassInfo classInfo = classInfo(callClassInfo, classInfoFromPathResolver); switch (opcode) { case Opcodes.INVOKESPECIAL: case Opcodes.INVOKEVIRTUAL: case Opcodes.H_INVOKESPECIAL: case Opcodes.H_INVOKEVIRTUAL: - result = resolver.resolveVirtualMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + result = resolver.resolveVirtualMethod(classInfo, name, descriptor); break; case Opcodes.INVOKESTATIC: case Opcodes.H_INVOKESTATIC: - result = resolver.resolveStaticMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + result = resolver.resolveStaticMethod(classInfo, name, descriptor); break; case Opcodes.INVOKEINTERFACE: case Opcodes.H_INVOKEINTERFACE: - result = resolver.resolveInterfaceMethod(classInfo(callClassInfo, classInfoFromPathResolver), name, descriptor); + result = resolver.resolveInterfaceMethod(classInfo, name, descriptor); break; default: throw new IllegalArgumentException("Opcode in visitMethodInsn must be INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE."); @@ -111,9 +113,8 @@ private Map getMethodMap(ClassInfo info) { private void visitClass( ClassInfo info, Function classInfoFromPathResolver, - Function> otherMethodInfoResolver + Function> otherMethodInfoResolver, LinkResolver resolver ) { - final LinkResolver resolver = LinkResolver.jvm(); BiFunction thisClassMethodInfoResolver = otherMethodInfoResolver.apply(info); info.getClassReader().accept(new MethodCallsResolverClassVisitor(thisClassMethodInfoResolver, classInfoFromPathResolver, otherMethodInfoResolver, resolver), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); } @@ -176,7 +177,8 @@ public void onAddLibrary(Workspace workspace, Resource library) { Function> methodMapGetter = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); - library.getClasses().forEach(c -> visitClass(c, classInfoFromPathResolver, methodMapGetter)); + final CachedLinkResolver resolver = new CachedLinkResolver(); + library.getClasses().forEach(c -> visitClass(c, classInfoFromPathResolver, methodMapGetter, resolver)); LOGGER.debug("There are now {} vertices, and {} unresolved calls.", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); } @@ -188,7 +190,7 @@ public void onNewClass(Resource resource, ClassInfo newValue) { final Function> methodMapGetter = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); - visitClass(newValue, classInfoFromPathResolver, methodMapGetter); + visitClass(newValue, classInfoFromPathResolver, methodMapGetter, new CachedLinkResolver()); LOGGER.debug("There are now {} vertices, and {} unresolved calls.", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); } @@ -252,7 +254,8 @@ private void updateRemovedMethods(Workspace workspace, Collection rem LOGGER.debug("After affecting {} classes, there are now {} vertices, and {} unresolved calls. Rewiring {} existing classes.", affectedClasses.size(), vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum(), existingAffectedClasses.size()); Function> methodMapGetter = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); - existingAffectedClasses.forEach(classInfo -> visitClass(classInfo, classInfoFromPathResolver, methodMapGetter)); + final CachedLinkResolver resolver = new CachedLinkResolver(); + existingAffectedClasses.forEach(classInfo -> visitClass(classInfo, classInfoFromPathResolver, methodMapGetter, resolver)); } @Override @@ -337,8 +340,29 @@ public int hashCode() { } } - private static dev.xdark.jlinker.ClassInfo classInfo(@Nonnull ClassInfo node, Function fn) { + private static dev.xdark.jlinker.ClassInfo classInfo( + @Nonnull ClassInfo node, Function pathToClass) { return new dev.xdark.jlinker.ClassInfo<>() { + private dev.xdark.jlinker.ClassInfo superClass = null; + private List> interfaces = null; + private final BiFunction> methodResolver = MemoizedFunction.memoize((name, descriptor) -> { + for (MethodInfo method : node.getMethods()) { + if (name.equals(method.getName()) && descriptor.equals(method.getDescriptor())) { + return methodInfo(method); + } + } + return null; + }); + + private final BiFunction> fieldResolver = MemoizedFunction.memoize((name, descriptor) -> { + for (FieldInfo field : node.getFields()) { + if (name.equals(field.getName()) && descriptor.equals(field.getDescriptor())) { + return fieldInfo(field); + } + } + return null; + }); + @Override public ClassInfo innerValue() { return node; @@ -352,40 +376,33 @@ public int accessFlags() { @Nonnull @Override public dev.xdark.jlinker.ClassInfo superClass() { + if (superClass != null) return superClass; String superName = node.getSuperName(); - if (superName == null) return null; - final ClassInfo classInfo = fn.apply(superName); + // should it just return null instead? + if (superName == null) throw CancelSignal.get(); + final ClassInfo classInfo = pathToClass.apply(superName); if (classInfo == null) throw CancelSignal.get(); - return classInfo(classInfo, fn); + return superClass = classInfo(classInfo, pathToClass); } @Nonnull @Override public List> interfaces() { - return node.getInterfaces().stream().map(x -> { - final ClassInfo classInfo = fn.apply(x); - return classInfo == null ? null : classInfo(classInfo, fn); + if (interfaces != null) return interfaces; + return interfaces = node.getInterfaces().stream().map(x -> { + final ClassInfo classInfo = pathToClass.apply(x); + return classInfo == null ? null : classInfo(classInfo, pathToClass); }).filter(Objects::nonNull).collect(Collectors.toList()); } @Override public dev.xdark.jlinker.MemberInfo getMethod(String name, String descriptor) { - for (MethodInfo method : node.getMethods()) { - if (name.equals(method.getName()) && descriptor.equals(method.getDescriptor())) { - return methodInfo(method); - } - } - return null; + return methodResolver.apply(name, descriptor); } @Override public dev.xdark.jlinker.MemberInfo getField(String name, String descriptor) { - for (FieldInfo field : node.getFields()) { - if (name.equals(field.getName()) && descriptor.equals(field.getDescriptor())) { - return fieldInfo(field); - } - } - return null; + return fieldResolver.apply(name, descriptor); } }; } From 656845bc940abc76f2e266cfc882abfb55820d9b Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 6 Nov 2022 17:57:33 +0100 Subject: [PATCH 15/19] Add also unresolved calls available in Graph Pane --- .../recaf/graph/call/CallGraphRegistry.java | 64 +++-------- .../graph/call/MutableCallGraphVertex.java | 1 + .../recaf/graph/call/UnresolvedCall.java | 106 ++++++++++++++++++ .../recaf/ui/pane/MethodCallGraphPane.java | 21 +++- 4 files changed, 139 insertions(+), 53 deletions(-) create mode 100644 recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedCall.java diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index 1da09eb96..31fd8dcd9 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -48,10 +48,15 @@ public void clear() { unresolvedCalls.clear(); } - public @Nullable CallGraphVertex getVertex(MethodInfo info) { + @Nullable + public CallGraphVertex getVertex(MethodInfo info) { return vertexMap.get(info); } + public Map> getUnresolvedCalls() { + return unresolvedCalls; + } + public void load() { Resources resources = workspace.getResources(); Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> workspace.getResources().getClass(path)); @@ -155,11 +160,11 @@ private void updateUnresolved( Set calls = unresolvedCalls.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); unresolvedCalls.clear(); calls.forEach(unresolvedCall -> visitMethodInstruction( - unresolvedCall.opcode, - unresolvedCall.owner, - unresolvedCall.name, - unresolvedCall.descriptor, - unresolvedCall.vertex, + unresolvedCall.getOpcode(), + unresolvedCall.getOwner(), + unresolvedCall.getName(), + unresolvedCall.getDescriptor(), + unresolvedCall.getVertex(), classInfoFromPathResolver, otherMethodInfoResolver, resolver, @@ -213,13 +218,14 @@ public void onRemoveClass(Resource resource, ClassInfo oldValue) { } private void updateRemovedMethods(Workspace workspace, Collection removedClasses) { - // doesn't work as expected, as it leaves - for whatever reason - zombie vertices and calls - // so, it's just "cleaner" to reload the whole thing... + // Doesn't work as expected, as it leaves - for whatever reason - zombie vertices and calls + // So, it's just "cleaner" to reload the whole thing... + // I leave this, so someday we can return to it, and have an idea of what to do Set removedClassNames = removedClasses.stream().map(ClassInfo::getName).collect(Collectors.toSet()); methodMap.keySet().removeIf(c -> removedClassNames.contains(c.getName())); unresolvedCalls.keySet().removeIf(removedClassNames::contains); unresolvedCalls.values().removeIf(cs -> { - cs.removeIf(c -> removedClassNames.contains(c.vertex.getMethodInfo().getOwner())); + cs.removeIf(c -> removedClassNames.contains(c.getVertex().getMethodInfo().getOwner())); return cs.isEmpty(); }); Set affectedVertices = vertexMap.values().stream() @@ -264,46 +270,6 @@ public void onUpdateClass(Resource resource, ClassInfo oldValue, ClassInfo newVa onNewClass(resource, newValue); } - private static class UnresolvedCall { - final int opcode; - final String owner; - final String name; - final String descriptor; - final MutableCallGraphVertex vertex; - - private UnresolvedCall(int opcode, String owner, String name, String descriptor, MutableCallGraphVertex vertex) { - this.opcode = opcode; - this.owner = owner; - this.name = name; - this.descriptor = descriptor; - this.vertex = vertex; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - UnresolvedCall that = (UnresolvedCall) o; - - if (opcode != that.opcode) return false; - if (!owner.equals(that.owner)) return false; - if (!name.equals(that.name)) return false; - if (!descriptor.equals(that.descriptor)) return false; - return vertex.equals(that.vertex); - } - - @Override - public int hashCode() { - int result = opcode; - result = 31 * result + owner.hashCode(); - result = 31 * result + name.hashCode(); - result = 31 * result + descriptor.hashCode(); - result = 31 * result + vertex.hashCode(); - return result; - } - } - protected static final class Descriptor { private final String name, desc; diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java index 4fe73c21f..8a56ee618 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/MutableCallGraphVertex.java @@ -16,6 +16,7 @@ public MutableCallGraphVertex(@Nonnull MethodInfo methodInfo) { this.methodInfo = methodInfo; } + @Nonnull @Override public MethodInfo getMethodInfo() { return methodInfo; diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedCall.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedCall.java new file mode 100644 index 000000000..80d6cbada --- /dev/null +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/UnresolvedCall.java @@ -0,0 +1,106 @@ +package me.coley.recaf.graph.call; + +import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.util.AccessFlag; +import org.objectweb.asm.Opcodes; + +import java.util.List; + +public class UnresolvedCall { + private final int opcode; + private final String owner; + private final String name; + private final String descriptor; + private final MutableCallGraphVertex vertex; + private MethodInfo dummyMethodInfo; + + UnresolvedCall(int opcode, String owner, String name, String descriptor, MutableCallGraphVertex vertex) { + this.opcode = opcode; + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + this.vertex = vertex; + } + + public int getOpcode() { + return opcode; + } + + public String getOwner() { + return owner; + } + + public String getName() { + return name; + } + + public String getDescriptor() { + return descriptor; + } + + public MutableCallGraphVertex getVertex() { + return vertex; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UnresolvedCall that = (UnresolvedCall) o; + + if (opcode != that.opcode) return false; + if (!owner.equals(that.owner)) return false; + if (!name.equals(that.name)) return false; + if (!descriptor.equals(that.descriptor)) return false; + return vertex.equals(that.vertex); + } + + @Override + public int hashCode() { + int result = opcode; + result = 31 * result + owner.hashCode(); + result = 31 * result + name.hashCode(); + result = 31 * result + descriptor.hashCode(); + result = 31 * result + vertex.hashCode(); + return result; + } + + public MethodInfo asMethodInfo() { + if (dummyMethodInfo != null) return dummyMethodInfo; + return dummyMethodInfo = new UnresolvedMethodInfo( + owner, name, descriptor, + "", + opcode == Opcodes.INVOKESTATIC ? AccessFlag.ACC_STATIC.getMask() : 0, + List.of(), this); + } + + public static class UnresolvedMethodInfo extends MethodInfo { + private final UnresolvedCall unresolvedCall; + + /** + * @param owner + * Name of type defining the member. + * @param name + * Method name. + * @param descriptor + * Method descriptor. + * @param signature + * Method generic signature. + * @param access + * Method access modifiers. + * @param exceptions + * Exception types thrown. + * @param unresolvedCall + * Backref to UnresolvedCall + */ + private UnresolvedMethodInfo(String owner, String name, String descriptor, String signature, int access, List exceptions, UnresolvedCall unresolvedCall) { + super(owner, name, descriptor, signature, access, exceptions); + this.unresolvedCall = unresolvedCall; + } + + public UnresolvedCall getUnresolvedCall() { + return unresolvedCall; + } + } +} diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java index 04f4e31a3..c847686af 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/pane/MethodCallGraphPane.java @@ -19,6 +19,7 @@ import me.coley.recaf.code.MethodInfo; import me.coley.recaf.graph.call.CallGraphRegistry; import me.coley.recaf.graph.call.CallGraphVertex; +import me.coley.recaf.graph.call.UnresolvedCall; import me.coley.recaf.ui.CommonUX; import me.coley.recaf.ui.behavior.Updatable; import me.coley.recaf.ui.context.ContextBuilder; @@ -33,6 +34,7 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; public class MethodCallGraphPane extends BorderPane implements Updatable { public static final int MAX_TREE_DEPTH = 20; @@ -109,10 +111,18 @@ private CallGraphItem buildCallGraph(MethodInfo rootMethod, CallGraphRegistry ca depth++; final CallGraphVertex vertex = callGraph.getVertex(item.getValue()); if (vertex != null) { - final List newTodo = childrenGetter.apply(vertex) - .stream().map(CallGraphVertex::getMethodInfo) + final List newTodo = Stream.concat( + childrenGetter.apply(vertex) + .stream().map(CallGraphVertex::getMethodInfo), + mode == CallGraphMode.CALLERS ? + Stream.empty() : + callGraph.getUnresolvedCalls().values().stream() + .flatMap(Collection::stream) + .filter(uc -> uc.getVertex().equals(vertex)) + .map(UnresolvedCall::asMethodInfo) + ) .filter(Objects::nonNull) - .map(c -> new CallGraphItem(c, visitedMethods.contains(c))) + .map(c -> new CallGraphItem(c, !(c instanceof UnresolvedCall.UnresolvedMethodInfo) && visitedMethods.contains(c))) .filter(i -> { if (i.getValue() == null) return false; item.getChildren().add(i); @@ -166,12 +176,13 @@ protected void updateItem(MethodInfo item, boolean empty) { setContextMenu(null); if (onClickFilter != null) removeEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); + setOpacity(1); } else { onClickFilter = null; Text classText = new Text(EscapeUtil.escape(item.getOwner())); classText.setFill(Color.CADETBLUE); Text methodText = new Text(item.getName()); - if (AccessFlag.isStatic(item.getAccess())) methodText.setFill(Color.GREEN); + if (AccessFlag.isStatic(item.getAccess())) methodText.setFill(Color.LIGHTGREEN); else methodText.setFill(Color.YELLOW); HBox box = new HBox(Icons.getMethodIcon(item), new TextFlow(classText, new Label("#"), methodText, new Label(item.getDescriptor()))); box.setSpacing(5); @@ -196,6 +207,8 @@ protected void updateItem(MethodInfo item, boolean empty) { } }; addEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); + } else { + setOpacity(0.5); } } } From 77d997b74be3db1e5fc2bc1629f54305d9b029b2 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 6 Nov 2022 19:10:11 +0100 Subject: [PATCH 16/19] Add debug messages for better tracking issues. --- .../recaf/graph/call/CallGraphRegistry.java | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index 31fd8dcd9..2d15fa84b 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -2,10 +2,7 @@ import dev.xdark.jlinker.LinkResolver; import dev.xdark.jlinker.Result; -import me.coley.recaf.code.ClassInfo; -import me.coley.recaf.code.FieldInfo; -import me.coley.recaf.code.MemberSignature; -import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.code.*; import me.coley.recaf.util.CancelSignal; import me.coley.recaf.util.MemoizedFunction; import me.coley.recaf.util.logging.Logging; @@ -66,7 +63,14 @@ public void load() { final CachedLinkResolver resolver = new CachedLinkResolver(); resources.getClasses().forEach(info -> visitClass(info, classInfoFromPathResolver, methodMapGetter, resolver)); methodMap.clear(); - LOGGER.debug("Loaded {} vertices, {} unresolved calls", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + LOGGER.debug("Loaded {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), + vertexMap.keySet().stream().map(MethodInfo::getOwner).distinct().count(), + unresolvedCalls.values().stream().mapToInt(Set::size).sum(), + unresolvedCalls.values().stream().flatMap(Collection::stream).map(UnresolvedCall::getVertex).distinct().count(), + unresolvedCalls.values().stream().flatMap(Collection::stream) + .map(UnresolvedCall::getVertex).map(MutableCallGraphVertex::getMethodInfo) + .map(MemberInfo::getOwner) + .distinct().count()); } @Nullable @@ -118,7 +122,8 @@ private Map getMethodMap(ClassInfo info) { private void visitClass( ClassInfo info, Function classInfoFromPathResolver, - Function> otherMethodInfoResolver, LinkResolver resolver + Function> otherMethodInfoResolver, + LinkResolver resolver ) { BiFunction thisClassMethodInfoResolver = otherMethodInfoResolver.apply(info); info.getClassReader().accept(new MethodCallsResolverClassVisitor(thisClassMethodInfoResolver, classInfoFromPathResolver, otherMethodInfoResolver, resolver), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); @@ -157,6 +162,8 @@ private void updateUnresolved( Function> otherMethodInfoResolver, LinkResolver resolver ) { + final int oldSum = unresolvedCalls.values().stream().mapToInt(Set::size).sum(); + LOGGER.debug("Resolving {} unresolved calls...", oldSum); Set calls = unresolvedCalls.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); unresolvedCalls.clear(); calls.forEach(unresolvedCall -> visitMethodInstruction( @@ -170,6 +177,10 @@ private void updateUnresolved( resolver, vertexMap) ); + final int newSum = unresolvedCalls.values().stream().mapToInt(Set::size).sum(); + if (oldSum == newSum) LOGGER.debug("The number of unresolved calls didn't change."); + else if (oldSum < newSum) LOGGER.debug("{} new unresolved calls added, there are {} now.", newSum - oldSum, newSum); + else LOGGER.debug("{} unresolved calls resolved, there are {} now.", oldSum - newSum, newSum); } @Override @@ -184,7 +195,14 @@ public void onAddLibrary(Workspace workspace, Resource library) { updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); final CachedLinkResolver resolver = new CachedLinkResolver(); library.getClasses().forEach(c -> visitClass(c, classInfoFromPathResolver, methodMapGetter, resolver)); - LOGGER.debug("There are now {} vertices, and {} unresolved calls.", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + LOGGER.debug("There are now {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), + vertexMap.keySet().stream().map(MethodInfo::getOwner).distinct().count(), + unresolvedCalls.values().stream().mapToInt(Set::size).sum(), + unresolvedCalls.values().stream().flatMap(Collection::stream).map(UnresolvedCall::getVertex).distinct().count(), + unresolvedCalls.values().stream().flatMap(Collection::stream) + .map(UnresolvedCall::getVertex).map(MutableCallGraphVertex::getMethodInfo) + .map(MemberInfo::getOwner) + .distinct().count()); } @Override @@ -196,7 +214,14 @@ public void onNewClass(Resource resource, ClassInfo newValue) { = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); visitClass(newValue, classInfoFromPathResolver, methodMapGetter, new CachedLinkResolver()); - LOGGER.debug("There are now {} vertices, and {} unresolved calls.", vertexMap.size(), unresolvedCalls.values().stream().mapToInt(Set::size).sum()); + LOGGER.debug("There are now {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), + vertexMap.keySet().stream().map(MethodInfo::getOwner).distinct().count(), + unresolvedCalls.values().stream().mapToInt(Set::size).sum(), + unresolvedCalls.values().stream().flatMap(Collection::stream).map(UnresolvedCall::getVertex).distinct().count(), + unresolvedCalls.values().stream().flatMap(Collection::stream) + .map(UnresolvedCall::getVertex).map(MutableCallGraphVertex::getMethodInfo) + .map(MemberInfo::getOwner) + .distinct().count()); } @Override From 4f43df98e2c99126a21f6e567036b4f70ebc49d5 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 6 Nov 2022 19:50:05 +0100 Subject: [PATCH 17/19] Make call graph and control flow graph accessible for methods in secondary libraries --- .../recaf/ui/context/MethodContextBuilder.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java b/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java index ca6f6d733..9fcbc2148 100644 --- a/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java +++ b/recaf-ui/src/main/java/me/coley/recaf/ui/context/MethodContextBuilder.java @@ -70,16 +70,18 @@ public ContextMenu build() { menu.getItems().add(createHeader(methodInfo.getName(), Icons.getMethodIcon(methodInfo))); if (!declaration) menu.getItems().add(action("menu.goto.method", Icons.OPEN, this::openDefinition)); - if (isPrimary() && isOwnerJvmClass()) { + if (isOwnerJvmClass()) { // TODO: When android cases are supported, remove 'isOwnerJvmClass()' check - Menu refactor = menu("menu.refactor"); - if (declaration) { - menu.getItems().add(action("menu.edit.assemble.method", Icons.ACTION_EDIT, this::assemble)); - menu.getItems().add(action("menu.edit.copy", Icons.ACTION_COPY, this::copy)); - menu.getItems().add(action("menu.edit.delete", Icons.ACTION_DELETE, this::delete)); + if (isPrimary()) { + Menu refactor = menu("menu.refactor"); + if (declaration) { + menu.getItems().add(action("menu.edit.assemble.method", Icons.ACTION_EDIT, this::assemble)); + menu.getItems().add(action("menu.edit.copy", Icons.ACTION_COPY, this::copy)); + menu.getItems().add(action("menu.edit.delete", Icons.ACTION_DELETE, this::delete)); + } + refactor.getItems().add(action("menu.refactor.rename", Icons.ACTION_EDIT, this::rename)); + menu.getItems().add(refactor); } - refactor.getItems().add(action("menu.refactor.rename", Icons.ACTION_EDIT, this::rename)); - menu.getItems().add(refactor); Menu view = menu("menu.view", Icons.EYE); view.getItems().add(action("menu.view.methodcfg", Icons.CHILDREN, this::graph)); view.getItems().add(action("menu.view.methodcallgraph", Icons.CHILDREN, this::callGraph)); From 2262e77e6be00b920fb7e6de1b191de67b903b16 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 20 Aug 2023 21:15:41 +0200 Subject: [PATCH 18/19] Update jlink to 1.0.6 --- build.gradle | 2 +- recaf-core/src/main/java/me/coley/recaf/Services.java | 5 ++++- .../me/coley/recaf/graph/call/CachedLinkResolver.java | 11 +++++++++++ .../me/coley/recaf/graph/call/CallGraphRegistry.java | 11 +++++++++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index e22c2040d..094207bd2 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ subprojects { java_parser = "com.github.javaparser:javaparser-symbol-solver-core:$jpVersion" javassist = 'org.javassist:javassist:3.29.2-GA' javax_annos = 'javax.annotation:javax.annotation-api:1.3.2' - jlinker = 'com.github.xxDark:jlinker:1.0.4' + jlinker = 'com.github.xxDark:jlinker:1.0.6' jelf = 'net.fornwall:jelf:0.9.0' jphantom = 'com.github.Col-E:jphantom:1.4.3' junit_api = "org.junit.jupiter:junit-jupiter-api:$junitVersion" diff --git a/recaf-core/src/main/java/me/coley/recaf/Services.java b/recaf-core/src/main/java/me/coley/recaf/Services.java index caafc68fc..2ce40d0d7 100644 --- a/recaf-core/src/main/java/me/coley/recaf/Services.java +++ b/recaf-core/src/main/java/me/coley/recaf/Services.java @@ -9,6 +9,7 @@ import me.coley.recaf.parse.WorkspaceSymbolSolver; import me.coley.recaf.ssvm.SsvmIntegration; import me.coley.recaf.util.WorkspaceTreeService; +import me.coley.recaf.util.threading.ThreadUtil; import me.coley.recaf.workspace.Workspace; import javax.annotation.Nullable; @@ -133,7 +134,9 @@ void updateWorkspace(Workspace workspace) { javaParserHelper = JavaParserHelper.create(symbolSolver); ssvmIntegration = new SsvmIntegration(workspace); treeService = new WorkspaceTreeService(workspace); - callGraphRegistry = CallGraphRegistry.createAndLoad(workspace); + CallGraphRegistry callGraphRegistry1 = CallGraphRegistry.create(workspace); + callGraphRegistry = callGraphRegistry1; + ThreadUtil.run(callGraphRegistry1::load); } } } diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java index b978174b0..66e1f2a1b 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CachedLinkResolver.java @@ -6,7 +6,9 @@ import me.coley.recaf.code.ClassInfo; import me.coley.recaf.code.FieldInfo; import me.coley.recaf.code.MethodInfo; +import me.coley.recaf.decompile.PostDecompileInterceptor; import me.coley.recaf.util.MemoizedFunction; +import org.checkerframework.checker.units.qual.C; import java.util.function.BiFunction; import java.util.function.Function; @@ -35,6 +37,10 @@ class CachedLinkResolver implements LinkResolver MemoizedFunction.memoize((name, descriptor) -> backedResolver.resolveStaticField(c, name, descriptor)) ); + private final Function, BiFunction>>> + specialMethodResolver = MemoizedFunction.memoize( + c -> MemoizedFunction.memoize((name, descriptor) -> backedResolver.resolveSpecialMethod(c, name, descriptor)) + ); @Override public Result> resolveStaticMethod(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor, boolean itf) { @@ -46,6 +52,11 @@ public Result> resolveStaticMethod(dev.xdark.j return resolveStaticMethod(owner, name, descriptor, false); } + @Override + public Result> resolveSpecialMethod(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor, boolean itf) { + return specialMethodResolver.apply(owner).apply(name, descriptor); + } + @Override public Result> resolveVirtualMethod(dev.xdark.jlinker.ClassInfo owner, String name, String descriptor) { return virtualMethodResolver.apply(owner).apply(name, descriptor); diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index 2d15fa84b..d05fa9c28 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -33,9 +33,14 @@ public CallGraphRegistry(Workspace workspace) { } public static CallGraphRegistry createAndLoad(Workspace workspace) { + CallGraphRegistry registry = create(workspace); + registry.load(); + return registry; + } + + public static CallGraphRegistry create(Workspace workspace) { CallGraphRegistry registry = new CallGraphRegistry(workspace); workspace.addListener(registry); - registry.load(); return registry; } @@ -86,8 +91,10 @@ private static MethodInfo resolveMethodInfo( final dev.xdark.jlinker.ClassInfo classInfo = classInfo(callClassInfo, classInfoFromPathResolver); switch (opcode) { case Opcodes.INVOKESPECIAL: - case Opcodes.INVOKEVIRTUAL: case Opcodes.H_INVOKESPECIAL: + result = resolver.resolveSpecialMethod(classInfo, name, descriptor); + break; + case Opcodes.INVOKEVIRTUAL: case Opcodes.H_INVOKEVIRTUAL: result = resolver.resolveVirtualMethod(classInfo, name, descriptor); break; From b4a8ae09c3d1aaa1e357a25449133a0473121b71 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Sun, 20 Aug 2023 21:36:47 +0200 Subject: [PATCH 19/19] Changed some prints from debug to info --- .../me/coley/recaf/graph/call/CallGraphRegistry.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java index d05fa9c28..37de9c64c 100644 --- a/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java +++ b/recaf-core/src/main/java/me/coley/recaf/graph/call/CallGraphRegistry.java @@ -68,7 +68,7 @@ public void load() { final CachedLinkResolver resolver = new CachedLinkResolver(); resources.getClasses().forEach(info -> visitClass(info, classInfoFromPathResolver, methodMapGetter, resolver)); methodMap.clear(); - LOGGER.debug("Loaded {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), + LOGGER.info("Loaded {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), vertexMap.keySet().stream().map(MethodInfo::getOwner).distinct().count(), unresolvedCalls.values().stream().mapToInt(Set::size).sum(), unresolvedCalls.values().stream().flatMap(Collection::stream).map(UnresolvedCall::getVertex).distinct().count(), @@ -192,7 +192,7 @@ private void updateUnresolved( @Override public void onAddLibrary(Workspace workspace, Resource library) { - LOGGER.debug("Adding {} classes...", library.getClasses().size()); + LOGGER.info("Adding {} classes...", library.getClasses().size()); Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> library.getClasses().stream().filter(c -> c.getName().equals(path)).findAny().orElseGet(() -> workspace.getResources().getClass(path)) @@ -202,7 +202,7 @@ public void onAddLibrary(Workspace workspace, Resource library) { updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); final CachedLinkResolver resolver = new CachedLinkResolver(); library.getClasses().forEach(c -> visitClass(c, classInfoFromPathResolver, methodMapGetter, resolver)); - LOGGER.debug("There are now {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), + LOGGER.info("There are now {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), vertexMap.keySet().stream().map(MethodInfo::getOwner).distinct().count(), unresolvedCalls.values().stream().mapToInt(Set::size).sum(), unresolvedCalls.values().stream().flatMap(Collection::stream).map(UnresolvedCall::getVertex).distinct().count(), @@ -214,14 +214,14 @@ public void onAddLibrary(Workspace workspace, Resource library) { @Override public void onNewClass(Resource resource, ClassInfo newValue) { - LOGGER.debug("Adding {} methods...", newValue.getMethods().size()); + LOGGER.info("Adding {} methods...", newValue.getMethods().size()); final Function classInfoFromPathResolver = MemoizedFunction.memoize(path -> workspace.getResources().getClass(path)); final Function> methodMapGetter = clazz -> (name, descriptor) -> getMethodMap(clazz).get(new Descriptor(name, descriptor)); updateUnresolved(classInfoFromPathResolver, methodMapGetter, LinkResolver.jvm()); visitClass(newValue, classInfoFromPathResolver, methodMapGetter, new CachedLinkResolver()); - LOGGER.debug("There are now {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), + LOGGER.info("There are now {} vertices for {} classes, {} unresolved calls for {} methods in {} classes", vertexMap.size(), vertexMap.keySet().stream().map(MethodInfo::getOwner).distinct().count(), unresolvedCalls.values().stream().mapToInt(Set::size).sum(), unresolvedCalls.values().stream().flatMap(Collection::stream).map(UnresolvedCall::getVertex).distinct().count(),