Skip to content

Commit 09e3d22

Browse files
committed
Fix mapping not effectively traversing up inheritance trees
1 parent 9e1dd9b commit 09e3d22

7 files changed

Lines changed: 418 additions & 30 deletions

File tree

recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingApplier.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ private Mappings enrich(@Nonnull Mappings mappings) {
191191
MappingsAdapter adapter = new MappingsAdapter(fieldDifferentiation, varDifferentiation);
192192
adapter.importIntermediate(intermediateMappings);
193193
mappings = adapter;
194+
195+
// Consider the method family of: "Circle.area() --> Shape.area()"
196+
// - If you provide a mapping for just "Shape.area()" a parent-lookup from "Circle.area()" can see the mapping by checking parents.
197+
// - If you provide a mapping for just "Circle.area()" then "Shape.area()" needs to look at children.
198+
// This infilling will flesh out the mappings so that if you provide either end of the family, the other end will be present in the mappings.
199+
adapter.infillMethodFamilies(intermediateMappings, inheritanceGraph);
194200
}
195201

196202
// Check if mappings can be enriched with type look-ups

recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingsAdapter.java

Lines changed: 217 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,28 @@
33
import jakarta.annotation.Nonnull;
44
import jakarta.annotation.Nullable;
55
import software.coley.recaf.info.ClassInfo;
6+
import software.coley.recaf.info.member.MethodMember;
67
import software.coley.recaf.path.ClassPathNode;
78
import software.coley.recaf.services.inheritance.InheritanceGraph;
89
import software.coley.recaf.services.inheritance.InheritanceVertex;
9-
import software.coley.recaf.services.mapping.data.*;
10+
import software.coley.recaf.services.mapping.data.ClassMapping;
11+
import software.coley.recaf.services.mapping.data.ClassMappingKey;
12+
import software.coley.recaf.services.mapping.data.FieldMapping;
13+
import software.coley.recaf.services.mapping.data.FieldMappingKey;
14+
import software.coley.recaf.services.mapping.data.MappingKey;
15+
import software.coley.recaf.services.mapping.data.MethodMapping;
16+
import software.coley.recaf.services.mapping.data.MethodMappingKey;
17+
import software.coley.recaf.services.mapping.data.VariableMapping;
18+
import software.coley.recaf.services.mapping.data.VariableMappingKey;
1019
import software.coley.recaf.workspace.model.Workspace;
1120

21+
import java.util.Collection;
22+
import java.util.Comparator;
1223
import java.util.HashMap;
1324
import java.util.Iterator;
25+
import java.util.List;
1426
import java.util.Map;
27+
import java.util.Objects;
1528
import java.util.TreeMap;
1629
import java.util.function.Function;
1730

@@ -20,6 +33,7 @@
2033
* <b>Enhancements</b>
2134
* <ol>
2235
* <li>Import mapping entries from a {@link IntermediateMappings} instance.</li>
36+
* <li>Enhance mapping entries with method family information from {@link InheritanceGraph}, see {@link #infillMethodFamilies(IntermediateMappings, InheritanceGraph)}</li>
2337
* <li>Enhance field/method lookups with inheritance info from {@link InheritanceGraph}, see {@link #enableHierarchyLookup(InheritanceGraph)}.</li>
2438
* <li>Enhance inner/outer class mapping edge cases via {@link #enableClassLookup(Workspace)}.</li>
2539
* <li>Adapt keys in cases where fields/vars do not have type info associated with them <i>(for formats that suck)</i>.</li>
@@ -66,11 +80,9 @@ public void importIntermediate(@Nonnull IntermediateMappings mappings) {
6680
String newName = fieldMapping.getNewName();
6781
if (!oldName.equals(newName)) {
6882
if (doesSupportFieldTypeDifferentiation()) {
69-
addField(fieldMapping.getOwnerName(), oldName,
70-
fieldMapping.getDesc(), newName);
83+
addField(fieldMapping.getOwnerName(), oldName, fieldMapping.getDesc(), newName);
7184
} else {
72-
addField(fieldMapping.getOwnerName(), oldName,
73-
newName);
85+
addField(fieldMapping.getOwnerName(), oldName, newName);
7486
}
7587
}
7688
}
@@ -79,10 +91,8 @@ public void importIntermediate(@Nonnull IntermediateMappings mappings) {
7991
String oldMethodDesc = methodMapping.getDesc();
8092
String newMethodName = methodMapping.getNewName();
8193
if (!oldMethodName.equals(newMethodName))
82-
addMethod(methodMapping.getOwnerName(), oldMethodName,
83-
oldMethodDesc, newMethodName);
84-
for (VariableMapping variableMapping :
85-
mappings.getMethodVariableMappings(className, oldMethodName, oldMethodDesc)) {
94+
addMethod(methodMapping.getOwnerName(), oldMethodName, oldMethodDesc, newMethodName);
95+
for (VariableMapping variableMapping : mappings.getMethodVariableMappings(className, oldMethodName, oldMethodDesc)) {
8696
addVariable(className, oldMethodName, oldMethodDesc,
8797
variableMapping.getOldName(), variableMapping.getDesc(), variableMapping.getIndex(),
8898
variableMapping.getNewName());
@@ -91,6 +101,94 @@ public void importIntermediate(@Nonnull IntermediateMappings mappings) {
91101
}
92102
}
93103

104+
/**
105+
* Enriches the provided mappings by normalizing method mappings across method families.
106+
*
107+
* @param inputMappings
108+
* Original mappings to enrich.
109+
* @param inheritanceGraph
110+
* Inheritance graph to use for looking up method families.
111+
*/
112+
public void infillMethodFamilies(@Nonnull IntermediateMappings inputMappings, @Nonnull InheritanceGraph inheritanceGraph) {
113+
// Collect all method mappings in the input.
114+
List<MethodMapping> explicitMappings = inputMappings.getMethods().values().stream()
115+
.flatMap(Collection::stream)
116+
.sorted(Comparator
117+
.comparing(MethodMapping::getOwnerName)
118+
.thenComparing(MethodMapping::getOldName)
119+
.thenComparing(MethodMapping::getDesc)
120+
.thenComparing(MethodMapping::getNewName))
121+
.toList();
122+
123+
// Compute which methods are mapped to what, and detect any conflicts in the provided mappings.
124+
Map<MethodMappingKey, MethodMapping> origins = new HashMap<>();
125+
for (MethodMapping explicitMapping : explicitMappings) {
126+
MethodMappingKey key = new MethodMappingKey(explicitMapping.getOwnerName(), explicitMapping.getOldName(), explicitMapping.getDesc());
127+
MethodMapping previous = origins.putIfAbsent(key, explicitMapping);
128+
if (previous != null && !previous.getNewName().equals(explicitMapping.getNewName()))
129+
throw newMethodConflict(key, key.getOwner(), previous, explicitMapping);
130+
}
131+
132+
// Iterate over the method mappings, and for each mapping,
133+
// check if there are any methods in the same override family that would be affected by the same mapping.
134+
// If so, add the same mapping for those methods as well.
135+
for (MethodMapping explicitMapping : explicitMappings) {
136+
// Skip constructors and static initializers.
137+
String methodName = explicitMapping.getOldName();
138+
if (!methodName.isEmpty() && methodName.charAt(0) == '<')
139+
continue;
140+
141+
// Skip enriching mappings on library classes. Can't map those anyways.
142+
String ownerName = explicitMapping.getOwnerName();
143+
InheritanceVertex ownerVertex = inheritanceGraph.getVertex(ownerName);
144+
if (ownerVertex == null || ownerVertex.isLibraryVertex())
145+
continue;
146+
147+
// Skip enriching mappings on private or static methods, as they cannot be inherited.
148+
ClassInfo ownerInfo = ownerVertex.getValue();
149+
MethodMember ownerMethod = ownerInfo.getDeclaredMethod(methodName, explicitMapping.getDesc());
150+
if (ownerMethod == null || ownerMethod.hasPrivateModifier() || ownerMethod.hasStaticModifier())
151+
continue;
152+
153+
// Iterate over classes in the inheritance family and apply the same mapping to any method with the same signature that is inherited by the owner method.
154+
List<InheritanceVertex> sortedFamily = inheritanceGraph.getVertexFamily(ownerName, false).stream()
155+
.sorted(Comparator.comparing(InheritanceVertex::getName))
156+
.toList();
157+
for (InheritanceVertex familyVertex : sortedFamily) {
158+
// Again, skipping library classes.
159+
if (familyVertex.isLibraryVertex())
160+
continue;
161+
162+
// Check if the method is inherited by this family member.
163+
// If it isn't, then we don't want to apply the mapping to it.
164+
ClassInfo familyInfo = familyVertex.getValue();
165+
MethodMember familyMethod = familyInfo.getDeclaredMethod(methodName, explicitMapping.getDesc());
166+
if (familyMethod == null || !MappingsAdapter.isInheritedMethod(ownerInfo, ownerMethod, familyInfo, familyMethod))
167+
continue;
168+
169+
// The method is inherited by this family member. Enrich the adapter.
170+
String familyOwnerName = familyInfo.getName();
171+
MethodMappingKey familyKey = new MethodMappingKey(familyOwnerName, methodName, explicitMapping.getDesc());
172+
String existing = getMappedMethodName(familyOwnerName, methodName, explicitMapping.getDesc());
173+
if (existing == null) {
174+
// Add the method mapping to this family member, and mark the original mapping as the origin for this mapping.
175+
addMethod(familyOwnerName, methodName, explicitMapping.getDesc(), explicitMapping.getNewName());
176+
origins.put(familyKey, explicitMapping);
177+
} else if (!existing.equals(explicitMapping.getNewName())) {
178+
// Shouldn't happen due to earlier check, but just in case... check for conflicting mappings.
179+
MethodMapping previous = origins.get(familyKey);
180+
if (previous == null)
181+
previous = new MethodMapping(familyOwnerName, methodName, explicitMapping.getDesc(), existing);
182+
throw newMethodConflict(familyKey, familyOwnerName, previous, explicitMapping);
183+
} else {
184+
// This family member already has the same mapping, so we can just mark the
185+
// original mapping as the origin for this family member's mapping.
186+
origins.putIfAbsent(familyKey, explicitMapping);
187+
}
188+
}
189+
}
190+
}
191+
94192
@Nullable
95193
@Override
96194
public String getMappedClassName(@Nonnull String internalName) {
@@ -106,9 +204,8 @@ public String getMappedClassName(@Nonnull String internalName) {
106204
if (outerName != null && outerName.length() < name.length()) {
107205
String inner = name.substring(outerName.length() + 1);
108206
String outerMapped = getMappedClassName(outerName);
109-
if (outerMapped != null) {
207+
if (outerMapped != null)
110208
mapped = outerMapped + "$" + inner;
111-
}
112209
}
113210
}
114211
} else if (isInner(internalName)) {
@@ -117,9 +214,8 @@ public String getMappedClassName(@Nonnull String internalName) {
117214
String inner = internalName.substring(split + 1);
118215
String outer = internalName.substring(0, split);
119216
String outerMapped = getMappedClassName(outer);
120-
if (outerMapped != null) {
217+
if (outerMapped != null)
121218
mapped = outerMapped + "$" + inner;
122-
}
123219
}
124220
}
125221
return mapped;
@@ -130,9 +226,8 @@ public String getMappedClassName(@Nonnull String internalName) {
130226
public String getMappedFieldName(@Nonnull String ownerName, @Nonnull String fieldName, @Nonnull String fieldDesc) {
131227
MappingKey key = getFieldKey(ownerName, fieldName, fieldDesc);
132228
String mapped = mappings.get(key);
133-
if (mapped == null && inheritanceGraph != null) {
229+
if (mapped == null && inheritanceGraph != null)
134230
mapped = findInParent(ownerName, parent -> getFieldKey(parent, fieldName, fieldDesc));
135-
}
136231
return mapped;
137232
}
138233

@@ -141,9 +236,8 @@ public String getMappedFieldName(@Nonnull String ownerName, @Nonnull String fiel
141236
public String getMappedMethodName(@Nonnull String ownerName, @Nonnull String methodName, @Nonnull String methodDesc) {
142237
MappingKey key = getMethodKey(ownerName, methodName, methodDesc);
143238
String mapped = mappings.get(key);
144-
if (mapped == null && inheritanceGraph != null) {
145-
mapped = findInParent(ownerName, parent -> getMethodKey(parent, methodName, methodDesc));
146-
}
239+
if (mapped == null && inheritanceGraph != null)
240+
mapped = findMethodInParent(ownerName, methodName, methodDesc);
147241
return mapped;
148242
}
149243

@@ -197,9 +291,8 @@ public boolean doesSupportVariableTypeDifferentiation() {
197291
*
198292
* @return The first mapping match in a parent class found by the lookup function.
199293
*/
294+
@Nullable
200295
private String findInParent(String owner, Function<String, ? extends MappingKey> lookup) {
201-
// TODO: Diamond class hierarchy doesn't work with this.
202-
// We need to visit the whole family tree.
203296
InheritanceVertex vertex = inheritanceGraph.getVertex(owner);
204297
if (vertex == null)
205298
return null;
@@ -208,13 +301,86 @@ private String findInParent(String owner, Function<String, ? extends MappingKey>
208301
vertex = iterator.next();
209302
MappingKey key = lookup.apply(vertex.getName());
210303
String result = mappings.get(key);
211-
if (result != null) {
304+
if (result != null)
212305
return result;
213-
}
214306
}
215307
return null;
216308
}
217309

310+
/**
311+
* @param ownerName
312+
* Internal name of the class <i>"defining"</i> the member.
313+
* <i>(Location in reference may not be where the member is actually defined, hence this lookup)</i>
314+
* @param methodName
315+
* Name of the method.
316+
* @param methodDesc
317+
* Descriptor of the method.
318+
*
319+
* @return The first mapping match in a parent class with the same name/desc <i>(Some access restrictions apply)</i>.
320+
*/
321+
@Nullable
322+
private String findMethodInParent(@Nonnull String ownerName, @Nonnull String methodName, @Nonnull String methodDesc) {
323+
// Skip if not in workspace/inheritance graph.
324+
InheritanceVertex ownerVertex = inheritanceGraph.getVertex(ownerName);
325+
if (ownerVertex == null)
326+
return null;
327+
328+
// Skip if the method is defined in the owner class and is private/static, as it would not be inherited.
329+
ClassInfo ownerInfo = ownerVertex.getValue();
330+
MethodMember ownerMethod = ownerInfo.getDeclaredMethod(methodName, methodDesc);
331+
if (ownerMethod != null && (ownerMethod.hasPrivateModifier() || ownerMethod.hasStaticModifier()))
332+
return null;
333+
334+
// Iterate through parents and find the first method match that is inherited by the owner class.
335+
Iterator<InheritanceVertex> iterator = ownerVertex.allParents().iterator();
336+
while (iterator.hasNext()) {
337+
InheritanceVertex parentVertex = iterator.next();
338+
ClassInfo parentInfo = parentVertex.getValue();
339+
340+
// If the parent class doesn't have a method with the same name/desc, skip it.
341+
MethodMember parentMethod = parentInfo.getDeclaredMethod(methodName, methodDesc);
342+
if (parentMethod == null || !isInheritedMethod(ownerInfo, ownerMethod, parentInfo, parentMethod))
343+
continue;
344+
345+
// Otherwise, we have a method that is inherited by the owner class, so we can check for a mapping on it.
346+
String mapped = mappings.get(getMethodKey(parentVertex.getName(), methodName, methodDesc));
347+
if (mapped != null)
348+
return mapped;
349+
}
350+
351+
// No mapping found in any parent class.
352+
return null;
353+
}
354+
355+
/**
356+
* @param ownerInfo
357+
* Class defining the target method.
358+
* @param ownerMethod
359+
* Target method in the owner class, or {@code null} if the method is not declared in the owner class.
360+
* @param parentInfo
361+
* Class defining the parent method.
362+
* @param parentMethod
363+
* Method in the parent class with the same name/desc as the target method.
364+
*
365+
* @return {@code true} when the parent method is inherited/implemented by the owner class.
366+
* {@code false} when the parent method cannot be inherited/implemented by the owner class due to access restrictions.
367+
*/
368+
protected static boolean isInheritedMethod(@Nonnull ClassInfo ownerInfo, @Nullable MethodMember ownerMethod,
369+
@Nonnull ClassInfo parentInfo, @Nonnull MethodMember parentMethod) {
370+
// Cannot inherit private/static methods.
371+
if (parentMethod.hasPrivateModifier() || parentMethod.hasStaticModifier())
372+
return false;
373+
374+
// Package-private inheritance only works if the owners are in the same package.
375+
String ownerPackage = ownerInfo.getPackageName();
376+
String parentPackage = parentInfo.getPackageName();
377+
if (ownerMethod == null)
378+
return !parentMethod.hasPackagePrivateModifier()
379+
|| Objects.equals(ownerPackage, parentPackage);
380+
return (!ownerMethod.hasPackagePrivateModifier() && !parentMethod.hasPackagePrivateModifier())
381+
|| Objects.equals(ownerPackage, parentPackage);
382+
}
383+
218384
/**
219385
* @param internalName
220386
* Some class name.
@@ -408,4 +574,33 @@ protected MappingKey getVariableKey(@Nonnull String className, @Nonnull String m
408574
@Nullable String name, @Nullable String desc, int index) {
409575
return new VariableMappingKey(className, methodName, methodDesc, name, desc);
410576
}
577+
578+
/**
579+
* Build the mapping conflict exception. Occurs when you have a mapping setup like:
580+
* <pre>
581+
* {@code
582+
* Shape.area() -> foo
583+
* Circle.area() -> bar
584+
* }</pre>
585+
* If {@code Circle} implements {@code Shape}, we cannot have two names for the same 'family'.
586+
*
587+
* @param key
588+
* Method info.
589+
* @param affectedOwner
590+
* Class that is affected by the conflict.
591+
* @param previous
592+
* Existing mapping that is causing the conflict.
593+
* @param current
594+
* New method mapping that is causing the conflict.
595+
*
596+
* @return Exception detailing the method mapping conflict.
597+
*/
598+
@Nonnull
599+
private static IllegalStateException newMethodConflict(@Nonnull MethodMappingKey key, @Nonnull String affectedOwner,
600+
@Nonnull MethodMapping previous, @Nonnull MethodMapping current) {
601+
return new IllegalStateException("Conflicting method mapping for family '" +
602+
key.getName() + key.getDesc() + "' affecting '" + affectedOwner + "': '" +
603+
previous.getOwnerName() + "' -> '" + previous.getNewName() + "' conflicts with '" +
604+
current.getOwnerName() + "' -> '" + current.getNewName() + "'");
605+
}
411606
}

0 commit comments

Comments
 (0)