diff --git a/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java b/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java index a07dd206a..79b7746bb 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java @@ -204,20 +204,21 @@ public List getOuterClassBreadcrumbs() { return breadcrumbs = Collections.emptyList(); int maxOuterDepth = 10; - breadcrumbs = new ArrayList<>(); + List list = new ArrayList<>(); int counter = 0; while (currentOuter != null) { if (++counter > maxOuterDepth) { - breadcrumbs.clear(); // assuming some obfuscator is at work, so breadcrumbs might be invalid. + list.clear(); // assuming some obfuscator is at work, so breadcrumbs might be invalid. break; } - breadcrumbs.addFirst(currentOuter); + list.addFirst(currentOuter); String targetOuter = currentOuter; currentOuter = innerClasses.stream() - .filter(i -> i.getInnerClassName().equals(targetOuter)) + .filter(i -> i.getInnerClassName().equals(targetOuter) && i.getOuterClassName() != null) .map(InnerClassInfo::getOuterClassName) .findFirst().orElse(null); } + breadcrumbs = Collections.unmodifiableList(list); } return breadcrumbs; } diff --git a/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java b/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java index e437b29de..b90131a29 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java @@ -120,14 +120,16 @@ default Stream parentTypesStream() { * This List MUST be sorted in order of the outermost first. * The last element is the outer of the class itself. *
- * For an example, if our class is 'C' then this list will be [A, B]: - *
+	 * For an example, if our class is 'C' then this list will be {@code [foo/A, foo/A$B]}:
+	 * 
{@code
+	 * package foo;
+	 *
 	 * class A {
 	 *     class B {
 	 *         class C {}  // This class
 	 *     }
 	 * }
-	 * 
+ * }
* * @return Breadcrumbs of the outer class. */ @@ -147,6 +149,19 @@ default boolean isInnerClass() { return getOuterClassName() != null || getOuterMethodName() != null; } + /** + * @param className + * Name of a supposed outer class. + * + * @return {@code true} if this class is an inner class of the given outer class. + */ + default boolean isInnerClassOf(@Nonnull String className) { + // If we don't start with that class name, we can't possibly be an inner class. + if (!getName().startsWith(className + "$")) + return false; + return getOuterClassBreadcrumbs().contains(className); + } + /** * @return {@code true} when this class is an anonymous inner class of another class. */ diff --git a/recaf-core/src/main/java/software/coley/recaf/services/compile/JavacCompiler.java b/recaf-core/src/main/java/software/coley/recaf/services/compile/JavacCompiler.java index 27073cd52..4593d11e5 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/compile/JavacCompiler.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/compile/JavacCompiler.java @@ -9,6 +9,9 @@ import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.services.Service; +import software.coley.recaf.services.phantom.GeneratedPhantomWorkspaceResource; +import software.coley.recaf.services.phantom.PhantomGenerationException; +import software.coley.recaf.services.phantom.PhantomGenerator; import software.coley.recaf.util.LookupUtil; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -25,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; import static java.nio.charset.StandardCharsets.UTF_8; @@ -43,10 +47,13 @@ public class JavacCompiler implements Service { private static final DebuggingLogger logger = Logging.get(JavacCompiler.class); private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); private static int minTargetVersion = 7; + private final PhantomGenerator phantomGenerator; private final JavacCompilerConfig config; @Inject - public JavacCompiler(JavacCompilerConfig config) { + public JavacCompiler(@Nonnull PhantomGenerator phantomGenerator, + @Nonnull JavacCompilerConfig config) { + this.phantomGenerator = phantomGenerator; this.config = config; } @@ -100,6 +107,25 @@ public CompilerResult compile(@Nonnull JavacArguments arguments, Collections.emptyList() : workspace.getAllResources(true); if (supplementaryResources != null) virtualClassPath = Lists.combine(virtualClassPath, supplementaryResources); + + // Generate phantom classes if the workspace does not already have phantoms in it. + if (workspace != null && config.getGeneratePhantoms().getValue() && workspace.getSupportingResources().stream() + .noneMatch(resource -> resource instanceof GeneratedPhantomWorkspaceResource)) { + try { + // Only scan the target class and any of its inner classes for content to fill in. + List classesToScan = workspace.findJvmClasses(c -> c.getName().equals(className) || c.isInnerClassOf(className)).stream() + .map(p -> p.getValue().asJvmClass()) + .collect(Collectors.toList()); + WorkspaceResource phantomResource = phantomGenerator.createPhantomsForClasses(workspace, classesToScan); + int generatedCount = phantomResource.getJvmClassBundle().size(); + if (generatedCount > 0) + logger.debug("Generated {} phantoms for pre-compile", generatedCount); + virtualClassPath = Lists.add(virtualClassPath, phantomResource); + } catch (PhantomGenerationException ex) { + logger.warn("Failed to generate phantoms for compilation against '{}'", className, ex); + } + } + List diagnostics = new ArrayList<>(); JavacListener listenerWrapper = createRecordingListener(listener, diagnostics); JavaFileManager fmFallback = compiler.getStandardFileManager(listenerWrapper, Locale.getDefault(), UTF_8); @@ -144,7 +170,7 @@ else if (downsampleTarget >= 0) logger.warn("Cannot downsample beyond Java {}", JavacCompiler.MIN_DOWNSAMPLE_VER); return new CompilerResult(compilations, diagnostics); } catch (RuntimeException ex) { - logger.debugging(l -> l.error("Compilation of '{}' crashed: {}", className, ex)); + logger.debugging(l -> l.error("Compilation of '{}' crashed", className, ex)); return new CompilerResult(ex); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java index c1887827a..eba20ee0f 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java @@ -17,7 +17,6 @@ import software.coley.observables.ObservableBoolean; import software.coley.observables.ObservableInteger; import software.coley.recaf.analytics.logging.Logging; -import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.InnerClassInfo; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.builder.JvmClassInfoBuilder; @@ -33,9 +32,6 @@ import software.coley.recaf.services.decompile.JvmDecompiler; import software.coley.recaf.services.info.association.FileTypeSyntaxAssociationService; import software.coley.recaf.services.navigation.Actions; -import software.coley.recaf.services.phantom.GeneratedPhantomWorkspaceResource; -import software.coley.recaf.services.phantom.PhantomGenerationException; -import software.coley.recaf.services.phantom.PhantomGenerator; import software.coley.recaf.services.source.AstResolveResult; import software.coley.recaf.services.source.AstService; import software.coley.recaf.ui.config.KeybindingConfig; @@ -58,10 +54,7 @@ import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.Bundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; -import software.coley.recaf.workspace.model.resource.WorkspaceResource; -import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -85,8 +78,6 @@ public class JvmDecompilerPane extends AbstractDecompilePane { private final ObservableInteger javacDownsampleTarget; private final ObservableBoolean javacDebug; private final ModalPaneComponent overlayModal = new ModalPaneComponent(); - private final PhantomGenerator phantomGenerator; - private final JavacCompilerConfig javacConfig; private final JavacCompiler javac; @Inject @@ -100,14 +91,11 @@ public JvmDecompilerPane(@Nonnull DecompilerPaneConfig config, @Nonnull DecompilerManager decompilerManager, @Nonnull JavacCompiler javac, @Nonnull JavacCompilerConfig javacConfig, - @Nonnull PhantomGenerator phantomGenerator, @Nonnull Actions actions) { super(config, searchBar, astService, contextActionSupport, languageAssociation, decompilerManager); - this.phantomGenerator = phantomGenerator; this.javacDebug = new ObservableBoolean(javacConfig.getDefaultEmitDebug().getValue()); this.javacTarget = new ObservableInteger(javacConfig.getDefaultTargetVersion().getValue()); this.javacDownsampleTarget = new ObservableInteger(javacConfig.getDefaultDownsampleTargetVersion().getValue()); - this.javacConfig = javacConfig; this.javac = javac; // Install tools container with configurator @@ -154,37 +142,6 @@ private void save() { // Invoke compiler with data. String infoName = info.getName(); CompletableFuture.supplyAsync(() -> { - // Generate phantoms for missing references in this class, if enabled and only if - // there are no generated phantom resources already in the workspace. - // This should be time-capped by 'completeOnTimeout' to prevent lock-ups. - List phantomResources; - if (javacConfig.getGeneratePhantoms().getValue() && workspace.getSupportingResources().stream() - .noneMatch(resource -> resource instanceof GeneratedPhantomWorkspaceResource)) { - ClassInfo currentInfo = path.getValue(); - Collection classesToScan; - if (currentInfo.getInnerClasses().isEmpty()) { - classesToScan = Collections.singleton(currentInfo.asJvmClass()); - } else { - classesToScan = workspace.findJvmClasses(c -> c.getName().startsWith(currentInfo.getName())).stream() - .map(p -> p.getValue().asJvmClass()) - .collect(Collectors.toList()); - } - try { - WorkspaceResource resource = phantomGenerator.createPhantomsForClasses(workspace, classesToScan); - phantomResources = Collections.singletonList(resource); - int generatedCount = resource.getJvmClassBundle().size(); - if (generatedCount > 0) - logger.debug("Generated {} phantoms for pre-compile", generatedCount); - } catch (PhantomGenerationException ex) { - logger.warn("Failed to generate phantoms for compilation against '{}'", currentInfo.getName(), ex); - phantomResources = null; - } - } else { - phantomResources = null; - } - return phantomResources; - }, compilePool).completeOnTimeout(null, 2, TimeUnit.SECONDS).thenApplyAsync(phantomResources -> { - // Populate javac args boolean debug = javacDebug.getValue(); JavacArgumentsBuilder builder = new JavacArgumentsBuilder() .withVersionTarget(useConfiguredVersion(info)) @@ -194,10 +151,8 @@ private void save() { .withDebugLineNumbers(debug) .withClassSource(editor.getText()) .withClassName(infoName); - - // Run javac with args + phantoms - return javac.compile(builder.build(), workspace, phantomResources, null); - }, compilePool).whenCompleteAsync((result, throwable) -> { + return javac.compile(builder.build(), workspace, null); + }, compilePool).completeOnTimeout(null, 2, TimeUnit.SECONDS).whenCompleteAsync((result, throwable) -> { // Handle results. // - Success --> Update content in the containing bundle // - Failure --> Show error + diagnostics to user