57
57
import software .coley .recaf .workspace .model .bundle .Bundle ;
58
58
import software .coley .recaf .workspace .model .bundle .JvmClassBundle ;
59
59
60
+ import java .util .Collections ;
60
61
import java .util .HashSet ;
61
62
import java .util .List ;
62
63
import java .util .Map ;
64
+ import java .util .Objects ;
63
65
import java .util .Set ;
64
66
import java .util .concurrent .CompletableFuture ;
65
67
import java .util .concurrent .ExecutorService ;
@@ -230,11 +232,36 @@ private void save() {
230
232
}
231
233
} while (recurseAddInners );
232
234
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 ();
238
265
}
239
266
240
267
// Compilation map has contents, update the workspace.
@@ -247,6 +274,7 @@ private void save() {
247
274
newInfo = info .toJvmClassBuilder ()
248
275
.adaptFrom (bytecode )
249
276
.build ();
277
+ path = Objects .requireNonNull (path .getParent (), "Class missing parent in path" ).child (newInfo );
250
278
} else {
251
279
// Handle inner classes.
252
280
JvmClassInfo originalClass = bundle .get (name );
@@ -272,6 +300,8 @@ private void save() {
272
300
// Update the class in the bundle.
273
301
bundle .put (newInfo );
274
302
});
303
+ for (String removedClass : notInCompilation )
304
+ bundle .remove (removedClass );
275
305
updateLock .set (false );
276
306
} else {
277
307
// Handle compile-result failure, or uncaught thrown exception.
@@ -355,6 +385,29 @@ private void showFirstTimeSaveWithErrors() {
355
385
});
356
386
}
357
387
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
+
358
411
/**
359
412
* Transition to handle countdown to allow acknowledging <i>"I can not save with errors"</i>.
360
413
*/
0 commit comments