Skip to content

Commit bcf59f7

Browse files
committed
Improve inner class renaming detection on recompile
1 parent 345f152 commit bcf59f7

File tree

1 file changed

+58
-5
lines changed

1 file changed

+58
-5
lines changed

recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@
5757
import software.coley.recaf.workspace.model.bundle.Bundle;
5858
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
5959

60+
import java.util.Collections;
6061
import java.util.HashSet;
6162
import java.util.List;
6263
import java.util.Map;
64+
import java.util.Objects;
6365
import java.util.Set;
6466
import java.util.concurrent.CompletableFuture;
6567
import java.util.concurrent.ExecutorService;
@@ -230,11 +232,36 @@ private void save() {
230232
}
231233
} while (recurseAddInners);
232234

233-
// Ensure all names in the compilation exist in the previous inner classes info
234-
if (!realInners.isEmpty() && compilations.keySet().stream().anyMatch(name -> !name.equals(infoName) && !realInners.containsKey(name))) {
235-
logger.warn("Please only rename inner classes via mapping operations.");
236-
Animations.animateWarn(this, 1000);
237-
return;
235+
// Abort if we cannot ensure this compilation includes renaming of inner classes.
236+
Set<String> notInCompilation;
237+
Set<String> notInExisting;
238+
if (!realInners.isEmpty()) {
239+
// Collect inner class names from the compilation.
240+
Set<String> compiledInnerNames = new HashSet<>(compilations.keySet());
241+
compiledInnerNames.remove(infoName);
242+
243+
// Collect names that do not exist in the before/after states.
244+
notInCompilation = new HashSet<>();
245+
notInExisting = new HashSet<>();
246+
for (String existingInner : realInners.keySet())
247+
if (!compiledInnerNames.remove(existingInner))
248+
notInCompilation.add(existingInner);
249+
for (String compiledInnerName : compiledInnerNames)
250+
if (!realInners.containsKey(compiledInnerName))
251+
notInExisting.add(compiledInnerName);
252+
253+
// If we see names in both collections (that are not anonymous inner classes)
254+
// we cannot safely know if this is a result of renaming or not.
255+
// But if we see only insertions or only removals that is totally fine.
256+
if (notInCompilation.stream().anyMatch(JvmDecompilerPane::isNotAnonymousInnerClass) &&
257+
notInExisting.stream().anyMatch(JvmDecompilerPane::isNotAnonymousInnerClass)) {
258+
logger.warn("Please only rename inner classes via mapping operations.");
259+
Animations.animateWarn(this, 1000);
260+
return;
261+
}
262+
} else {
263+
notInCompilation = Collections.emptySet();
264+
notInExisting = Collections.emptySet();
238265
}
239266

240267
// Compilation map has contents, update the workspace.
@@ -247,6 +274,7 @@ private void save() {
247274
newInfo = info.toJvmClassBuilder()
248275
.adaptFrom(bytecode)
249276
.build();
277+
path = Objects.requireNonNull(path.getParent(), "Class missing parent in path").child(newInfo);
250278
} else {
251279
// Handle inner classes.
252280
JvmClassInfo originalClass = bundle.get(name);
@@ -272,6 +300,8 @@ private void save() {
272300
// Update the class in the bundle.
273301
bundle.put(newInfo);
274302
});
303+
for (String removedClass : notInCompilation)
304+
bundle.remove(removedClass);
275305
updateLock.set(false);
276306
} else {
277307
// Handle compile-result failure, or uncaught thrown exception.
@@ -355,6 +385,29 @@ private void showFirstTimeSaveWithErrors() {
355385
});
356386
}
357387

388+
/**
389+
* @param name
390+
* Internal class name.
391+
*
392+
* @return {@code true} when it appears to not be a top-level anonymous inner class.
393+
*/
394+
private static boolean isNotAnonymousInnerClass(@Nonnull String name) {
395+
return !isAnonymousInnerClass(name);
396+
}
397+
398+
/**
399+
* @param name
400+
* Internal class name.
401+
*
402+
* @return {@code true} when it appears to be a top-level anonymous inner class.
403+
*/
404+
private static boolean isAnonymousInnerClass(@Nonnull String name) {
405+
// We should only see numeric names in anonymous inner classes coming from javac
406+
// so the first inner split's next character should be a good enough check.
407+
int firstInnerSplit = name.indexOf('$');
408+
return firstInnerSplit > 0 && firstInnerSplit + 1 < name.length() && Character.isDigit(name.charAt(firstInnerSplit + 1));
409+
}
410+
358411
/**
359412
* Transition to handle countdown to allow acknowledging <i>"I can not save with errors"</i>.
360413
*/

0 commit comments

Comments
 (0)