Skip to content

Commit adf2e86

Browse files
authored
Add API for reacting to collected replacements (#40)
1 parent 5867da1 commit adf2e86

File tree

4 files changed

+57
-41
lines changed

4 files changed

+57
-41
lines changed

api/src/main/java/net/neoforged/jst/api/Replacements.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@
44
import com.intellij.psi.PsiElement;
55

66
import java.util.ArrayList;
7+
import java.util.Collections;
78
import java.util.List;
89

910
public final class Replacements {
10-
private final List<Replacement> replacements = new ArrayList<>();
11+
private final List<Replacement> replacements;
12+
13+
public Replacements(List<Replacement> replacements) {
14+
this.replacements = replacements;
15+
}
16+
17+
public Replacements() {
18+
this(new ArrayList<>());
19+
}
1120

1221
public boolean isEmpty() {
1322
return replacements.isEmpty();
@@ -81,5 +90,4 @@ public String apply(CharSequence originalContent) {
8190
writer.append(originalContent, replacements.get(replacements.size() - 1).range().getEndOffset(), originalContent.length());
8291
return writer.toString();
8392
}
84-
8593
}

api/src/main/java/net/neoforged/jst/api/SourceTransformer.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.intellij.psi.PsiFile;
44

5+
import java.util.List;
6+
57
/**
68
* Transformers are created through {@link SourceTransformerPlugin plugins}, and handle source replacements.
79
* <p>
@@ -19,13 +21,25 @@ public interface SourceTransformer {
1921
default void beforeRun(TransformContext context) {
2022
}
2123

24+
/**
25+
* Invoke after replacements are collected for a given file, but before they are applied.
26+
* <p>
27+
* Can be used to react to or verify the replacements that were collected.
28+
* @param fileEntry the file entry being transformed
29+
* @param replacements the replacements that were collected; read-only
30+
* @return {@code true} if the transformation should continue, {@code false} if it should fail
31+
*/
32+
default boolean beforeReplacement(FileEntry fileEntry, List<Replacement> replacements) {
33+
return true;
34+
}
35+
2236
/**
2337
* Invoked after all source transformations are finished.
2438
* <p>
2539
* Can be used for post-transformation validation.
2640
*
2741
* @param context the transform context
28-
* @return {@code true} if the transformation was successful, {@code false} otherwise
42+
* @return {@code true} if the transformation was successful, {@code false} if it failed
2943
*/
3044
default boolean afterRun(TransformContext context) {
3145
return true;

cli/src/main/java/net/neoforged/jst/cli/Replacement.java

Lines changed: 0 additions & 31 deletions
This file was deleted.

cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import net.neoforged.jst.api.FileSink;
77
import net.neoforged.jst.api.FileSource;
88
import net.neoforged.jst.api.Logger;
9+
import net.neoforged.jst.api.Replacement;
910
import net.neoforged.jst.api.Replacements;
1011
import net.neoforged.jst.api.SourceTransformer;
1112
import net.neoforged.jst.api.TransformContext;
@@ -19,7 +20,9 @@
1920
import java.nio.file.attribute.FileTime;
2021
import java.time.Instant;
2122
import java.util.ArrayList;
23+
import java.util.Collections;
2224
import java.util.List;
25+
import java.util.concurrent.atomic.AtomicBoolean;
2326

2427
/**
2528
* Reference for out-of-IDE usage of the IntelliJ Java parser is from the Kotlin compiler
@@ -63,16 +66,22 @@ public boolean process(FileSource source, FileSink sink, List<SourceTransformer>
6366
});
6467
}
6568
} else {
69+
var success = new AtomicBoolean(true);
6670
try (var asyncOut = new OrderedParallelWorkQueue(sink, maxQueueDepth);
6771
var stream = source.streamEntries()) {
6872
stream.forEach(entry -> asyncOut.submitAsync(parallelSink -> {
6973
try {
70-
processEntry(entry, sourceRoot, transformers, parallelSink);
74+
if (!processEntry(entry, sourceRoot, transformers, parallelSink)) {
75+
success.set(false);
76+
}
7177
} catch (IOException e) {
7278
throw new UncheckedIOException(e);
7379
}
7480
}));
7581
}
82+
if (!success.get()) {
83+
return false;
84+
}
7685
}
7786

7887
boolean isOk = true;
@@ -83,25 +92,31 @@ public boolean process(FileSource source, FileSink sink, List<SourceTransformer>
8392
return isOk;
8493
}
8594

86-
private void processEntry(FileEntry entry, VirtualFile sourceRoot, List<SourceTransformer> transformers, FileSink sink) throws IOException {
95+
private boolean processEntry(FileEntry entry, VirtualFile sourceRoot, List<SourceTransformer> transformers, FileSink sink) throws IOException {
8796
if (entry.directory()) {
8897
sink.putDirectory(entry.relativePath());
89-
return;
98+
return true;
9099
}
100+
101+
boolean[] success = {true};
91102

92103
try (var in = entry.openInputStream()) {
93104
byte[] content = in.readAllBytes();
94105
var lastModified = entry.lastModified();
95106

96107
if (!isIgnored(entry.relativePath()) && !transformers.isEmpty() && entry.hasExtension("java")) {
97108
var orgContent = content;
98-
content = transformSource(sourceRoot, entry.relativePath(), transformers, content);
109+
content = transformSource(sourceRoot, entry, transformers, content, success);
110+
if (!success[0]) {
111+
return false;
112+
}
99113
if (orgContent != content) {
100114
lastModified = FileTime.from(Instant.now());
101115
}
102116
}
103117
sink.putFile(entry.relativePath(), lastModified, content);
104118
}
119+
return true;
105120
}
106121

107122
private boolean isIgnored(String relativePath) {
@@ -113,11 +128,12 @@ private boolean isIgnored(String relativePath) {
113128
return false;
114129
}
115130

116-
byte[] transformSource(VirtualFile contentRoot, String path, List<SourceTransformer> transformers, byte[] originalContentBytes) {
131+
private byte[] transformSource(VirtualFile contentRoot, FileEntry entry, List<SourceTransformer> transformers, byte[] originalContentBytes, boolean[] successOut) {
117132
// Instead of parsing the content we actually read from the file, we read the virtual file that is
118133
// visible to IntelliJ from adding the source jar. The reasoning is that IntelliJ will cache this internally
119134
// and reuse it when cross-referencing type-references. If we parsed from a String instead, it would parse
120135
// the same file twice.
136+
var path = entry.relativePath();
121137
var sourceFile = contentRoot.findFileByRelativePath(path);
122138
if (sourceFile == null) {
123139
System.err.println("Can't transform " + path + " since IntelliJ doesn't see it in the source jar.");
@@ -130,14 +146,23 @@ byte[] transformSource(VirtualFile contentRoot, String path, List<SourceTransfor
130146
}
131147

132148
// Gather replaced ranges in the source-file with their replacement
133-
var replacements = new Replacements();
149+
List<Replacement> replacementsList = new ArrayList<>();
150+
var replacements = new Replacements(replacementsList);
134151

135152
for (var transformer : transformers) {
136153
transformer.visitFile(psiFile, replacements);
137154
}
138155

156+
var readOnlyReplacements = Collections.unmodifiableList(replacementsList);
157+
boolean success = true;
158+
for (var transformer : transformers) {
159+
success = success && transformer.beforeReplacement(entry, readOnlyReplacements);
160+
}
161+
162+
successOut[0] = success;
163+
139164
// If no replacements were made, just stream the original content into the destination file
140-
if (replacements.isEmpty()) {
165+
if (!success || replacements.isEmpty()) {
141166
return originalContentBytes;
142167
}
143168

0 commit comments

Comments
 (0)