33import jakarta .annotation .Nonnull ;
44import jakarta .annotation .Nullable ;
55import software .coley .recaf .info .ClassInfo ;
6+ import software .coley .recaf .info .member .MethodMember ;
67import software .coley .recaf .path .ClassPathNode ;
78import software .coley .recaf .services .inheritance .InheritanceGraph ;
89import 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 ;
1019import software .coley .recaf .workspace .model .Workspace ;
1120
21+ import java .util .Collection ;
22+ import java .util .Comparator ;
1223import java .util .HashMap ;
1324import java .util .Iterator ;
25+ import java .util .List ;
1426import java .util .Map ;
27+ import java .util .Objects ;
1528import java .util .TreeMap ;
1629import java .util .function .Function ;
1730
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