Skip to content

Commit 0602ddc

Browse files
committed
Add support for compiling multiple sources at once using JavacCompiler service.
1 parent 6c80b7a commit 0602ddc

4 files changed

Lines changed: 138 additions & 16 deletions

File tree

recaf-core/src/main/java/software/coley/recaf/services/compile/JavacArguments.java

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import jakarta.annotation.Nullable;
55
import software.coley.recaf.workspace.model.Workspace;
66

7+
import java.util.Collections;
8+
import java.util.LinkedHashMap;
9+
import java.util.Map;
710
import java.util.Objects;
811

912
/**
@@ -15,7 +18,7 @@
1518
public class JavacArguments {
1619
// Primary inputs
1720
private final String className;
18-
private final String classSource;
21+
private final Map<String, String> classSources;
1922
// Options
2023
private final String classPath;
2124
private final int versionTarget;
@@ -45,8 +48,38 @@ public class JavacArguments {
4548
public JavacArguments(@Nonnull String className, @Nonnull String classSource,
4649
@Nullable String classPath, int versionTarget, int downsampleTarget,
4750
boolean debugVariables, boolean debugLineNumbers, boolean debugSourceName) {
51+
this(className, Collections.singletonMap(className, classSource),
52+
classPath, versionTarget, downsampleTarget,
53+
debugVariables, debugLineNumbers, debugSourceName);
54+
}
55+
56+
/**
57+
* @param className
58+
* Internal name of the primary class being compiled.
59+
* @param classSources
60+
* Sources of all classes to compile, keyed by internal name.
61+
* @param classPath
62+
* Classpath to use with compiler.
63+
* @param versionTarget
64+
* Java version to target.
65+
* @param downsampleTarget
66+
* Java version to target via down sampling. Negative to disable downs sampling.
67+
* @param debugVariables
68+
* Debug flag to include variable info.
69+
* @param debugLineNumbers
70+
* Debug flag to include line number info.
71+
* @param debugSourceName
72+
* Debug flag to include source file name.
73+
*/
74+
public JavacArguments(@Nonnull String className, @Nonnull Map<String, String> classSources,
75+
@Nullable String classPath, int versionTarget, int downsampleTarget,
76+
boolean debugVariables, boolean debugLineNumbers, boolean debugSourceName) {
4877
this.className = className;
49-
this.classSource = classSource;
78+
if (classSources.isEmpty())
79+
throw new IllegalArgumentException("Class sources must not be empty");
80+
if (!classSources.containsKey(className))
81+
throw new IllegalArgumentException("Class sources must contain the primary class: " + className);
82+
this.classSources = classSources;
5083
this.classPath = classPath;
5184
this.versionTarget = versionTarget;
5285
this.downsampleTarget = downsampleTarget;
@@ -88,11 +121,19 @@ public String getClassName() {
88121
}
89122

90123
/**
91-
* @return Source of the class.
124+
* @return Source of the primary class.
92125
*/
93126
@Nonnull
94127
public String getClassSource() {
95-
return classSource;
128+
return Objects.requireNonNull(classSources.get(className));
129+
}
130+
131+
/**
132+
* @return Sources of all classes to compile, keyed by internal name.
133+
*/
134+
@Nonnull
135+
public Map<String, String> getClassSources() {
136+
return classSources;
96137
}
97138

98139
/**
@@ -150,14 +191,14 @@ public boolean equals(Object o) {
150191
if (debugLineNumbers != other.debugLineNumbers) return false;
151192
if (debugSourceName != other.debugSourceName) return false;
152193
if (!className.equals(other.className)) return false;
153-
if (!classSource.equals(other.classSource)) return false;
194+
if (!classSources.equals(other.classSources)) return false;
154195
return Objects.equals(classPath, other.classPath);
155196
}
156197

157198
@Override
158199
public int hashCode() {
159200
int result = className.hashCode();
160-
result = 31 * result + classSource.hashCode();
201+
result = 31 * result + classSources.hashCode();
161202
result = 31 * result + (classPath != null ? classPath.hashCode() : 0);
162203
result = 31 * result + versionTarget;
163204
result = 31 * result + (debugVariables ? 1 : 0);

recaf-core/src/main/java/software/coley/recaf/services/compile/JavacArgumentsBuilder.java

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@
44
import jakarta.annotation.Nullable;
55
import software.coley.recaf.util.JavaVersion;
66

7+
import java.util.HashMap;
8+
import java.util.Map;
9+
710
/**
811
* Builder for {@link JavacArguments}.
912
*
1013
* @author Matt Coley
1114
*/
1215
public final class JavacArgumentsBuilder {
16+
// Primary class to compile.
1317
private String className;
1418
private String classSource;
19+
// All classes to compile, keyed by internal name.
20+
// Does not need to contain the primary class, that will be added automatically if provided.
21+
private Map<String, String> classSources;
22+
// Additional compiler arguments.
1523
private String classPath = System.getProperty("java.class.path");
1624
private int versionTarget = JavaVersion.get();
1725
private int downsampleTarget = -1;
@@ -21,7 +29,7 @@ public final class JavacArgumentsBuilder {
2129

2230
/**
2331
* @param className
24-
* Internal name of the class being compiled.
32+
* Internal name of the primary class being compiled.
2533
*
2634
* @return Builder.
2735
*/
@@ -33,7 +41,7 @@ public JavacArgumentsBuilder withClassName(@Nonnull String className) {
3341

3442
/**
3543
* @param classSource
36-
* Source of the class.
44+
* Source of the primary class to compile.
3745
*
3846
* @return Builder.
3947
*/
@@ -43,6 +51,33 @@ public JavacArgumentsBuilder withClassSource(@Nonnull String classSource) {
4351
return this;
4452
}
4553

54+
/**
55+
* @param classSources
56+
* Sources of additional classes to compile, keyed by internal name.
57+
*
58+
* @return Builder.
59+
*/
60+
@Nonnull
61+
public JavacArgumentsBuilder withClassSources(@Nonnull Map<String, String> classSources) {
62+
// Skip if no additional sources are provided.
63+
if (classSources.isEmpty())
64+
return this;
65+
66+
// Add additional sources to the builder.
67+
if (this.classSources == null)
68+
this.classSources = new HashMap<>();
69+
this.classSources.putAll(classSources);
70+
71+
// If the primary class is not set pick the first entry in the map as the primary class.
72+
if (className == null) {
73+
Map.Entry<String, String> entry = classSources.entrySet().iterator().next();
74+
return withClassName(entry.getKey())
75+
.withClassSource(entry.getValue());
76+
}
77+
78+
return this;
79+
}
80+
4681
/**
4782
* @param classPath
4883
* Classpath to use with compiler.
@@ -122,11 +157,20 @@ public JavacArgumentsBuilder withDebugSourceName(boolean debugSourceName) {
122157
@Nonnull
123158
public JavacArguments build() {
124159
if (className == null)
125-
throw new IllegalArgumentException("Class name must not be null");
126-
if (classSource == null)
127-
throw new IllegalArgumentException("Class source must not be null");
160+
throw new IllegalArgumentException("Primary class name must not be null");
161+
162+
// Combine primary input with any additional sources.
163+
Map<String, String> allClassSources = classSources == null ? new HashMap<>() : new HashMap<>(classSources);
164+
if (classSource != null)
165+
allClassSources.put(className, classSource);
166+
167+
// Validate that the primary class is present in the sources.
168+
if (allClassSources.isEmpty())
169+
throw new IllegalArgumentException("Class sources must not be empty");
170+
if (!allClassSources.containsKey(className))
171+
throw new IllegalArgumentException("Class sources must contain the primary class");
128172

129-
return new JavacArguments(className, classSource,
173+
return new JavacArguments(className, allClassSources,
130174
classPath, versionTarget, downsampleTarget,
131175
debugVariables, debugLineNumbers, debugSourceName);
132176
}

recaf-core/src/main/java/software/coley/recaf/services/compile/JavacCompiler.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.Collections;
2929
import java.util.List;
3030
import java.util.Locale;
31+
import java.util.Map;
32+
import java.util.Set;
3133
import java.util.stream.Collectors;
3234

3335
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -96,11 +98,14 @@ public CompilerResult compile(@Nonnull JavacArguments arguments,
9698
if (compiler == null)
9799
return new CompilerResult(new IllegalStateException("Cannot load 'javac' compiler."));
98100

99-
String className = arguments.getClassName();
101+
// Primary class to compile, plus any additional classes that are being compiled alongside it.
102+
String className = arguments.getClassName(); // This is primarily used for logging, there's no other special treatment.
103+
Map<String, String> classSources = arguments.getClassSources();
104+
Set<String> sourceClassNames = classSources.keySet();
100105

101106
// Class input map
102107
VirtualUnitMap unitMap = new VirtualUnitMap();
103-
unitMap.addSource(className, arguments.getClassSource());
108+
classSources.forEach(unitMap::addSource);
104109

105110
// Create a file manager to track files in-memory rather than on-disk
106111
List<WorkspaceResource> virtualClassPath = workspace == null ?
@@ -111,8 +116,10 @@ public CompilerResult compile(@Nonnull JavacArguments arguments,
111116
// Generate phantom classes if the workspace does not already have phantoms in it.
112117
if (workspace != null && config.getGeneratePhantoms().getValue()
113118
&& workspace.getSupportingResources().stream().noneMatch(resource -> resource instanceof GeneratedPhantomWorkspaceResource)) {
114-
// Only scan the target class and any of its inner classes for content to fill in.
115-
List<JvmClassInfo> classesToScan = workspace.findJvmClasses(c -> c.getName().equals(className) || c.isInnerClassOf(className)).stream()
119+
// Only scan the target classes and any of their inner classes for content to fill in.
120+
List<JvmClassInfo> classesToScan = workspace.findJvmClasses(c ->
121+
sourceClassNames.contains(c.getName()) ||
122+
sourceClassNames.stream().anyMatch(c::isInnerClassOf)).stream()
116123
.map(p -> p.getValue().asJvmClass())
117124
.collect(Collectors.toList());
118125
if (!classesToScan.isEmpty()) {

recaf-core/src/test/java/software/coley/recaf/services/compile/JavacCompilerTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,34 @@ public static void main(String[] args) {
184184
assertEquals(0, result.getDiagnostics().size(), "There were unexpected diagnostic messages");
185185
assertTrue(result.getCompilations().containsKey("HelloWorld"), "Class missing from compile map output");
186186
}
187+
188+
@Test
189+
void testJavacWithMultipleSources() {
190+
// Specify multiple sources with a minimal dependency between them.
191+
JavacArguments arguments = new JavacArgumentsBuilder()
192+
.withClassSources(Map.of(
193+
"HelloWorld",
194+
"""
195+
public class HelloWorld {
196+
public static void main(String[] args) {
197+
System.out.println(Helper.message());
198+
}
199+
}""",
200+
// Supporting class
201+
"Helper", """
202+
public class Helper {
203+
public static String message() {
204+
return "Hello world";
205+
}
206+
}"""
207+
))
208+
.build();
209+
210+
// We should be able to compile both classes at once.
211+
CompilerResult result = javac.compile(arguments, null, null);
212+
assertTrue(result.wasSuccess(), "Result does not indicate success");
213+
assertEquals(0, result.getDiagnostics().size(), "There were unexpected diagnostic messages");
214+
assertTrue(result.getCompilations().containsKey("HelloWorld"), "Primary class missing from compile map output");
215+
assertTrue(result.getCompilations().containsKey("Helper"), "Dependent class missing from compile map output");
216+
}
187217
}

0 commit comments

Comments
 (0)