From b1b635ff0fc8b51c7391b5d9c02dfaae8b0de7ed Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Mon, 10 Mar 2025 21:47:48 +0100 Subject: [PATCH 1/4] Make `getOwner()` return types maximally specific --- src/main/java/matcher/type/MemberInstance.java | 2 +- src/main/java/matcher/type/MethodVarInstance.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/matcher/type/MemberInstance.java b/src/main/java/matcher/type/MemberInstance.java index ca863472..50c694b5 100644 --- a/src/main/java/matcher/type/MemberInstance.java +++ b/src/main/java/matcher/type/MemberInstance.java @@ -91,7 +91,7 @@ public String getDisplayName(NameType type, boolean full) { public abstract boolean isReal(); @Override - public Matchable getOwner() { + public ClassInstance getOwner() { return cls; } diff --git a/src/main/java/matcher/type/MethodVarInstance.java b/src/main/java/matcher/type/MethodVarInstance.java index 5911789e..a5315f09 100644 --- a/src/main/java/matcher/type/MethodVarInstance.java +++ b/src/main/java/matcher/type/MethodVarInstance.java @@ -143,7 +143,7 @@ private boolean hasValidOrigName() { } @Override - public Matchable getOwner() { + public MethodInstance getOwner() { return method; } From 39cc154cd1ab0dc4ca94f1da6146e9cc96c35b09 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Mon, 10 Mar 2025 21:49:38 +0100 Subject: [PATCH 2/4] Fix `ProjectConfig` NPEs, use static builder method --- src/main/java/matcher/Matcher.java | 2 +- .../java/matcher/config/ProjectConfig.java | 20 +++++++++++-------- src/main/java/matcher/gui/Gui.java | 2 +- .../java/matcher/gui/menu/NewProjectPane.java | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/matcher/Matcher.java b/src/main/java/matcher/Matcher.java index 121b604b..1d6f1520 100644 --- a/src/main/java/matcher/Matcher.java +++ b/src/main/java/matcher/Matcher.java @@ -106,7 +106,7 @@ public void initFromMatches(List inputDirs, List classPathA = resolvePaths(inputDirs, cpFilesA); List classPathB = resolvePaths(inputDirs, cpFilesB); - ProjectConfig config = new ProjectConfig.Builder(pathsA, pathsB) + ProjectConfig config = ProjectConfig.builder(pathsA, pathsB) .classPathA(new ArrayList<>(classPathA)) .classPathB(new ArrayList<>(classPathB)) .sharedClassPath(new ArrayList<>(sharedClassPath)) diff --git a/src/main/java/matcher/config/ProjectConfig.java b/src/main/java/matcher/config/ProjectConfig.java index 11059c36..ed5cf223 100644 --- a/src/main/java/matcher/config/ProjectConfig.java +++ b/src/main/java/matcher/config/ProjectConfig.java @@ -9,8 +9,12 @@ import java.util.regex.PatternSyntaxException; public class ProjectConfig { + public static Builder builder(List pathsA, List pathsB) { + return new Builder(pathsA, pathsB); + } + public static class Builder { - public Builder(List pathsA, List pathsB) { + private Builder(List pathsA, List pathsB) { this.pathsA = pathsA; this.pathsB = pathsB; } @@ -97,17 +101,17 @@ public ProjectConfig build() { protected final List pathsA; protected final List pathsB; - protected List classPathA; - protected List classPathB; - protected List sharedClassPath; + protected List classPathA = Collections.emptyList(); + protected List classPathB = Collections.emptyList(); + protected List sharedClassPath = Collections.emptyList(); protected boolean inputsBeforeClassPath; protected Path mappingsPathA; protected Path mappingsPathB; protected boolean saveUnmappedMatches = true; - protected String nonObfuscatedClassPatternA; - protected String nonObfuscatedClassPatternB; - protected String nonObfuscatedMemberPatternA; - protected String nonObfuscatedMemberPatternB; + protected String nonObfuscatedClassPatternA = ""; + protected String nonObfuscatedClassPatternB = ""; + protected String nonObfuscatedMemberPatternA = ""; + protected String nonObfuscatedMemberPatternB = ""; } private ProjectConfig(List pathsA, List pathsB, List classPathA, List classPathB, diff --git a/src/main/java/matcher/gui/Gui.java b/src/main/java/matcher/gui/Gui.java index f7b5564e..b153afeb 100644 --- a/src/main/java/matcher/gui/Gui.java +++ b/src/main/java/matcher/gui/Gui.java @@ -212,7 +212,7 @@ private void handleStartupArgs(List args) { if (!validProjectConfigArgPresent) return; - ProjectConfig config = new ProjectConfig.Builder(inputsA, inputsB) + ProjectConfig config = ProjectConfig.builder(inputsA, inputsB) .classPathA(new ArrayList<>(classPathA)) .classPathB(new ArrayList<>(classPathB)) .sharedClassPath(new ArrayList<>(sharedClassPath)) diff --git a/src/main/java/matcher/gui/menu/NewProjectPane.java b/src/main/java/matcher/gui/menu/NewProjectPane.java index 518761d4..9af452ce 100644 --- a/src/main/java/matcher/gui/menu/NewProjectPane.java +++ b/src/main/java/matcher/gui/menu/NewProjectPane.java @@ -310,7 +310,7 @@ private Node createMiscPane() { } public ProjectConfig createConfig() { - return new ProjectConfig.Builder(new ArrayList<>(pathsA), new ArrayList<>(pathsB)) + return ProjectConfig.builder(new ArrayList<>(pathsA), new ArrayList<>(pathsB)) .classPathA(new ArrayList<>(classPathA)) .classPathB(new ArrayList<>(classPathB)) .sharedClassPath(new ArrayList<>(sharedClassPath)) From f81ba5eeb0ee8740c7d27c3f9159e539cec1e7c8 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Mon, 10 Mar 2025 21:51:55 +0100 Subject: [PATCH 3/4] Fix `isAssignableFrom` skipping every second hierarchy level of interfaces --- src/main/java/matcher/type/ClassInstance.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/matcher/type/ClassInstance.java b/src/main/java/matcher/type/ClassInstance.java index 30ce62db..82fb6ba1 100644 --- a/src/main/java/matcher/type/ClassInstance.java +++ b/src/main/java/matcher/type/ClassInstance.java @@ -1001,14 +1001,14 @@ public boolean isAssignableFrom(ClassInstance c) { // check if c or a superclass of c implements this with multiple indirections if (toCheck != null) { - while ((sc = toCheck.poll()) != null) { - for (ClassInstance iface : sc.getInterfaces()) { - assert iface != this; // already checked + ClassInstance iface; - if (implementers.contains(iface)) return true; + while ((iface = toCheck.poll()) != null) { + assert iface != this; // already checked - toCheck.addAll(iface.interfaces); - } + if (implementers.contains(iface)) return true; + + toCheck.addAll(iface.interfaces); } } } From b44142a7ecbbc0f21496f8e7164c97577f77005a Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Mon, 10 Mar 2025 21:57:10 +0100 Subject: [PATCH 4/4] Add Mapping-IO hierarchy provider --- .../mapping/Matcher2MioHierarchyProvider.java | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/main/java/matcher/mapping/Matcher2MioHierarchyProvider.java diff --git a/src/main/java/matcher/mapping/Matcher2MioHierarchyProvider.java b/src/main/java/matcher/mapping/Matcher2MioHierarchyProvider.java new file mode 100644 index 00000000..b22b056a --- /dev/null +++ b/src/main/java/matcher/mapping/Matcher2MioHierarchyProvider.java @@ -0,0 +1,204 @@ +package matcher.mapping; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import net.fabricmc.mappingio.tree.HierarchyInfoProvider; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.fabricmc.mappingio.tree.MappingTreeView.MethodMappingView; + +import matcher.Util; +import matcher.mapping.Matcher2MioHierarchyProvider.HierarchyData; +import matcher.type.ClassEnv; +import matcher.type.ClassInstance; +import matcher.type.FieldInstance; +import matcher.type.LocalClassEnv; +import matcher.type.MethodInstance; +import matcher.type.MethodVarInstance; + +public class Matcher2MioHierarchyProvider implements HierarchyInfoProvider { + public Matcher2MioHierarchyProvider(LocalClassEnv env, String namespace) { + this.env = env; + this.namespace = namespace; + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + public String resolveField(String owner, String name, String desc) { + ClassInstance cls = env.getClsByName(owner); + if (cls == null) return null; + + FieldInstance field = cls.resolveField(name, desc); + + return field != null ? field.getOwner().getName() : null; + } + + @Override + public String resolveMethod(String owner, String name, String desc) { + ClassInstance cls = env.getClsByName(owner); + if (cls == null) return null; + + MethodInstance method = cls.resolveMethod(name, desc, cls.isInterface()); + + return method != null ? method.getOwner().getName() : null; + } + + @Override + public HierarchyData getMethodHierarchy(String owner, String name, String desc) { + ClassInstance cls = env.getClsByName(owner); + if (cls == null) return null; + + MethodInstance method = cls.resolveMethod(name, desc, cls.isInterface()); + if (method == null) return null; + + assert !method.isStatic() || method.getAllHierarchyMembers().size() == 1; + + return getMethodHierarchy0(method); + } + + private HierarchyData getMethodHierarchy0(MethodInstance method) { + Set immediateHierarchy = method.getAllHierarchyMembers(); + Queue> toCheck = new ArrayDeque<>(immediateHierarchy.size()); + Set> checked = new HashSet<>(); + toCheck.add(immediateHierarchy); + + do { + Set currentHierarchy; + + while ((currentHierarchy = toCheck.poll()) != null) { + if (checked.contains(currentHierarchy)) { + continue; + } + + for (MethodInstance m : currentHierarchy) { + if (m.isSynthetic()) { + Set targets = m.getRefsOut(); + + for (MethodInstance target : targets) { + if (isPotentialBridge(m, target)) { + toCheck.add(target.getAllHierarchyMembers()); + } + } + } + + Set sources = findSyntheticSources(m); + + for (MethodInstance source : sources) { + if (isPotentialBridge(source, m)) { + toCheck.add(source.getAllHierarchyMembers()); + } + } + } + + checked.add(currentHierarchy); + } + } while (!toCheck.isEmpty()); + + Set fullHierarchy = Util.newIdentityHashSet(); + + for (Set hierarchy : checked) { + fullHierarchy.addAll(hierarchy); + } + + return new HierarchyData(fullHierarchy); + } + + private Set findSyntheticSources(MethodInstance method) { + Set sources = Collections.emptySet(); + + for (MethodInstance refIn : method.getRefsIn()) { + if (refIn.isSynthetic()) { + if (sources.isEmpty()) { + sources = new HashSet<>(); + } + + sources.add(refIn); + } + } + + return sources; + } + + private boolean isPotentialBridge(MethodInstance bridgeMethod, MethodInstance bridgedMethod) { + if (!bridgeMethod.isSynthetic()) return false; + + if (bridgeMethod.isPrivate() || bridgeMethod.isFinal() || bridgeMethod.isStatic()) { + return false; + } + + // Bridge method's target must be in the same class or in a parent class + if (!bridgedMethod.getOwner().isAssignableFrom(bridgeMethod.getOwner())) { + return false; + } + + MethodVarInstance[] bridgeParams = bridgeMethod.getArgs(); + MethodVarInstance[] bridgedParams = bridgedMethod.getArgs(); + + if (bridgeParams.length != bridgedParams.length) { + return false; + } + + for (int i = 0; i < bridgeParams.length; i++) { + if (!areTypesBridgeCompatible(bridgeParams[i].getType(), bridgedParams[i].getType())) { + return false; + } + } + + return areTypesBridgeCompatible(bridgeMethod.getRetType(), bridgedMethod.getRetType()); + } + + private boolean areTypesBridgeCompatible(ClassInstance bridgeType, ClassInstance bridgedType) { + if (bridgeType.equals(bridgedType)) { + return true; + } + + boolean bridgedExtendsBridge = bridgeType.isAssignableFrom(bridgedType); + + // If not equal, types in bridge method descriptor should always be less specific than in the bridged method + assert bridgedExtendsBridge || !bridgedType.isAssignableFrom(bridgeType); + + return bridgedExtendsBridge; + } + + @Override + public int getHierarchySize(HierarchyData hierarchy) { + return hierarchy != null ? hierarchy.methods.size() : 0; + } + + @Override + public Collection getHierarchyMethods(HierarchyData hierarchy, MappingTreeView tree) { + if (hierarchy == null) return Collections.emptyList(); + + List ret = new ArrayList<>(hierarchy.methods.size()); + int ns = tree.getNamespaceId(namespace); + assert ns != MappingTreeView.NULL_NAMESPACE_ID; + + for (MethodInstance method : hierarchy.methods) { + MethodMappingView m = tree.getMethod(method.getOwner().getName(), method.getName(), method.getDesc(), ns); + if (m != null) ret.add(m); + } + + return ret; + } + + public static final class HierarchyData { + HierarchyData(Collection methods) { + this.methods = methods; + } + + final Collection methods; + } + + private final ClassEnv env; + private final String namespace; +}