diff --git a/.gitignore b/.gitignore index d86b045..1986480 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .gradle -/build +build !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..e641a1e --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java-library' +} + +group = 'net.neoforged.jst' + +dependencies { + api "com.jetbrains.intellij.java:java-psi-impl:$intellij_version" + api "info.picocli:picocli:$picocli_version" + compileOnly "org.jetbrains:annotations:$jetbrains_annotations_version" +} diff --git a/src/main/java/PsiHelper.java b/api/src/main/java/net/neoforged/jst/api/PsiHelper.java similarity index 71% rename from src/main/java/PsiHelper.java rename to api/src/main/java/net/neoforged/jst/api/PsiHelper.java index 34ce59f..e6f5563 100644 --- a/src/main/java/PsiHelper.java +++ b/api/src/main/java/net/neoforged/jst/api/PsiHelper.java @@ -1,4 +1,5 @@ -import com.intellij.openapi.util.Key; +package net.neoforged.jst.api; + import com.intellij.psi.PsiClass; import com.intellij.psi.PsiLambdaExpression; import com.intellij.psi.PsiMethod; @@ -11,65 +12,12 @@ import com.intellij.psi.util.ClassUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ObjectIntHashMap; -import namesanddocs.NamesAndDocsDatabase; -import namesanddocs.NamesAndDocsForClass; -import namesanddocs.NamesAndDocsForMethod; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.Optional; public final class PsiHelper { - // Keys for attaching mapping data to a PsiClass. Used to prevent multiple lookups for the same class/method. - private static final Key> CLASS_DATA_KEY = Key.create("names_and_docs_for_class"); - private static final Key> METHOD_DATA_KEY = Key.create("names_and_docs_for_method"); - - @SuppressWarnings("OptionalAssignedToNull") - @Nullable - public static NamesAndDocsForClass getClassData(NamesAndDocsDatabase namesAndDocs, @Nullable PsiClass psiClass) { - if (psiClass == null) { - return null; - } - var classData = psiClass.getUserData(CLASS_DATA_KEY); - if (classData != null) { - return classData.orElse(null); - } else { - var sb = new StringBuilder(); - getBinaryClassName(psiClass, sb); - if (sb.isEmpty()) { - classData = Optional.empty(); - } else { - classData = Optional.ofNullable(namesAndDocs.getClass(sb.toString())); - } - psiClass.putUserData(CLASS_DATA_KEY, classData); - return classData.orElse(null); - } - } - - @SuppressWarnings("OptionalAssignedToNull") - @Nullable - public static NamesAndDocsForMethod getMethodData(NamesAndDocsDatabase namesAndDocs, @Nullable PsiMethod psiMethod) { - if (psiMethod == null) { - return null; - } - var methodData = psiMethod.getUserData(METHOD_DATA_KEY); - if (methodData != null) { - return methodData.orElse(null); - } else { - methodData = Optional.empty(); - var classData = getClassData(namesAndDocs, psiMethod.getContainingClass()); - if (classData != null) { - var methodName = getBinaryMethodName(psiMethod); - var methodSignature = getBinaryMethodSignature(psiMethod); - methodData = Optional.ofNullable(classData.getMethod(methodName, methodSignature)); - } - - psiMethod.putUserData(METHOD_DATA_KEY, methodData); - return methodData.orElse(null); - } - } - public static String getBinaryMethodName(PsiMethod psiMethod) { return psiMethod.isConstructor() ? "" : psiMethod.getName(); } diff --git a/api/src/main/java/net/neoforged/jst/api/Replacement.java b/api/src/main/java/net/neoforged/jst/api/Replacement.java new file mode 100644 index 0000000..a5ecf34 --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/Replacement.java @@ -0,0 +1,11 @@ +package net.neoforged.jst.api; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; + +import java.util.Comparator; + +public record Replacement(TextRange range, String newText) { + + public static final Comparator COMPARATOR = Comparator.comparingInt(replacement -> replacement.range.getStartOffset()); +} diff --git a/api/src/main/java/net/neoforged/jst/api/Replacements.java b/api/src/main/java/net/neoforged/jst/api/Replacements.java new file mode 100644 index 0000000..556afcf --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/Replacements.java @@ -0,0 +1,73 @@ +package net.neoforged.jst.api; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; + +import java.util.ArrayList; +import java.util.List; + +public final class Replacements { + private final List replacements = new ArrayList<>(); + + public boolean isEmpty() { + return replacements.isEmpty(); + } + + public void replace(PsiElement element, String newText) { + add(new Replacement(element.getTextRange(), newText)); + } + + public void insertBefore(PsiElement element, String newText) { + var startOffset = element.getTextRange().getStartOffset(); + add(new Replacement(new TextRange( + startOffset, + startOffset + ), newText)); + } + + public void insertAfter(PsiElement element, String newText) { + var endOffset = element.getTextRange().getEndOffset(); + add(new Replacement(new TextRange( + endOffset, + endOffset + ), newText)); + } + + public void add(Replacement replacement) { + replacements.add(replacement); + } + + public String apply(CharSequence originalContent) { + // We will assemble the resulting file by iterating all ranges (replaced or not) + // For this to work, the replacement ranges need to be in ascending order and non-overlapping + replacements.sort(Replacement.COMPARATOR); + + var writer = new StringBuilder(); + // Copy up until the first replacement + + writer.append(originalContent, 0, replacements.get(0).range().getStartOffset()); + for (int i = 0; i < replacements.size(); i++) { + var replacement = replacements.get(i); + var range = replacement.range(); + if (i > 0) { + // Copy between previous and current replacement verbatim + var previousReplacement = replacements.get(i - 1); + // validate that replacement ranges are non-overlapping + if (previousReplacement.range().getEndOffset() > range.getStartOffset()) { + throw new IllegalStateException("Trying to replace overlapping ranges: " + + replacement + " and " + previousReplacement); + } + + writer.append( + originalContent, + previousReplacement.range().getEndOffset(), + range.getStartOffset() + ); + } + writer.append(replacement.newText()); + } + writer.append(originalContent, replacements.get(replacements.size() - 1).range().getEndOffset(), originalContent.length()); + return writer.toString(); + } + +} diff --git a/api/src/main/java/net/neoforged/jst/api/SourceTransformer.java b/api/src/main/java/net/neoforged/jst/api/SourceTransformer.java new file mode 100644 index 0000000..d4a39d5 --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/SourceTransformer.java @@ -0,0 +1,13 @@ +package net.neoforged.jst.api; + +import com.intellij.psi.PsiFile; + +public interface SourceTransformer { + default void beforeRun() { + } + + default void afterRun() { + } + + void visitFile(PsiFile psiFile, Replacements replacements); +} diff --git a/api/src/main/java/net/neoforged/jst/api/SourceTransformerPlugin.java b/api/src/main/java/net/neoforged/jst/api/SourceTransformerPlugin.java new file mode 100644 index 0000000..be5f285 --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/SourceTransformerPlugin.java @@ -0,0 +1,19 @@ +package net.neoforged.jst.api; + +/** + * Accessed via {@link java.util.ServiceLoader}. + */ +public interface SourceTransformerPlugin { + + /** + * Unique name used in command-line options to enable this plugin. + */ + String getName(); + + /** + * Creates a new transformer to be applied to source code. + */ + SourceTransformer createTransformer(); + +} + diff --git a/build.gradle b/build.gradle index 87e8c12..646766e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,68 +1,12 @@ plugins { - id 'java' id 'maven-publish' - id 'com.github.johnrengelman.shadow' id 'net.neoforged.gradleutils' } group = "net.neoforged" project.version = gradleutils.version -jar { - manifest { - attributes 'Main-Class': 'ApplyParchmentToSourceJar' - } +subprojects { + group = rootProject.group + version = rootProject.version } - -repositories { - mavenCentral() - maven { - url "https://www.jetbrains.com/intellij-repository/releases/" - } - maven { - url "https://cache-redirector.jetbrains.com/intellij-dependencies/" - } - maven { - url "https://maven.parchmentmc.org/" - } -} - -dependencies { - implementation 'com.jetbrains.intellij.java:java-psi-impl:233.11799.300' - - implementation 'org.parchmentmc.feather:io-gson:1.1.0' - implementation 'net.fabricmc:mapping-io:0.5.1' - - testImplementation platform('org.junit:junit-bom:5.9.1') - testImplementation 'org.junit.jupiter:junit-jupiter' -} - -test { - useJUnitPlatform() -} - -assemble.configure { - dependsOn shadowJar -} - -// This skips the shadowjar from being published as part of the normal publication -components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { - skip() -} - -publishing { - publications { - // This publication only contains the unshaded jar with dependencies in the pom.xml - plain(MavenPublication) { - artifactId = 'apply-parchment' - - from components.java - } - // This publication only contains the shaded standalone jar - bundle(MavenPublication) { - artifactId = 'apply-parchment-bundle' - - project.shadow.component(bundle) - } - } -} \ No newline at end of file diff --git a/cli/build.gradle b/cli/build.gradle new file mode 100644 index 0000000..30133bd --- /dev/null +++ b/cli/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' +} + +group = 'net.neoforged.jst' + +jar { + manifest { + attributes 'Main-Class': 'net.neoforged.jst.cli.Main' + } +} + +dependencies { + implementation project(":api") + implementation "info.picocli:picocli:$picocli_version" + implementation project(":parchment") + + testImplementation platform("org.junit:junit-bom:$junit_version") + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} + +assemble.configure { + dependsOn shadowJar +} + +// This skips the shadowjar from being published as part of the normal publication +components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { + skip() +} + +publishing { + publications { + // This publication only contains the unshaded jar with dependencies in the pom.xml + plain(MavenPublication) { + artifactId = 'jst-cli' + + from components.java + } + // This publication only contains the shaded standalone jar + bundle(MavenPublication) { + artifactId = 'jst-cli-bundle' + + project.shadow.component(bundle) + } + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/Main.java b/cli/src/main/java/net/neoforged/jst/cli/Main.java new file mode 100644 index 0000000..c1e6656 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/Main.java @@ -0,0 +1,178 @@ +package net.neoforged.jst.cli; + +import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.api.SourceTransformerPlugin; +import net.neoforged.jst.cli.io.Sink; +import net.neoforged.jst.cli.io.Source; +import org.jetbrains.annotations.VisibleForTesting; +import picocli.CommandLine; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.Callable; + +@CommandLine.Command(name = "jst", mixinStandardHelpOptions = true, usageHelpWidth = 100) +public class Main implements Callable { + @CommandLine.Parameters(index = "0", paramLabel = "INPUT", description = "Path to a single Java-file, a source-archive or a folder containing the source to transform.") + private Path inputPath; + + @CommandLine.Parameters(index = "1", paramLabel = "OUTPUT", description = "Path to where the resulting source should be placed.") + private Path outputPath; + + @CommandLine.Option(names = "--in-format", description = "Specify the format of INPUT explicitly. AUTO (the default) performs auto-detection. Other options are SINGLE_FILE for Java files, ARCHIVE for source jars or zips, and FOLDER for folders containing Java code.") + private PathType inputFormat = PathType.AUTO; + + @CommandLine.Option(names = "--out-format", description = "Specify the format of OUTPUT explicitly. Allows the same options as --in-format.") + private PathType outputFormat = PathType.AUTO; + + @CommandLine.Option(names = "--libraries-list", description = "Specifies a file that contains a path to an archive or directory to add to the classpath on each line.") + private Path librariesList; + + private final HashSet enabledTransformers = new HashSet<>(); + + public static void main(String[] args) { + System.exit(innerMain(args)); + } + + @VisibleForTesting + public static int innerMain(String... args) { + // Load these up front so that they can add CommandLine Options + var plugins = ServiceLoader.load(SourceTransformerPlugin.class).stream().map(ServiceLoader.Provider::get).toList(); + + var main = new Main(); + var commandLine = new CommandLine(main); + var spec = commandLine.getCommandSpec(); + + main.setupPluginCliOptions(plugins, spec); + return commandLine.execute(args); + } + + @Override + public Integer call() throws Exception { + + try (var source = new Source(inputPath, inputFormat); + var processor = new SourceFileProcessor()) { + + if (librariesList != null) { + processor.addLibrariesList(librariesList); + } + + var orderedTransformers = new ArrayList<>(enabledTransformers); + + try (var sink = new Sink(source, outputPath, outputFormat)) { + processor.process(source, sink, orderedTransformers); + } + + } + + return 0; + } + + private void setupPluginCliOptions(List plugins, CommandLine.Model.CommandSpec spec) { + for (var plugin : plugins) { + var transformer = plugin.createTransformer(); + + var builder = CommandLine.Model.ArgGroupSpec.builder(); + builder + .exclusive(false) + .heading("Plugin - " + plugin.getName() + "%n"); + + builder.addArg(CommandLine.Model.OptionSpec.builder("--enable-" + plugin.getName()) + .type(boolean.class) + .required(true) + .setter(new CommandLine.Model.ISetter() { + @SuppressWarnings("unchecked") + @Override + public T set(T value) { + var previous = enabledTransformers.contains(transformer); + if ((boolean) value) { + enabledTransformers.add(transformer); + } else { + enabledTransformers.remove(transformer); + } + return (T) (Object) previous; + } + }) + .description("Enable " + plugin.getName()) + .build()); + + var transformerSpec = CommandLine.Model.CommandSpec.forAnnotatedObject(transformer); + for (var option : transformerSpec.options()) { + builder.addArg(option); + } + spec.addArgGroup(builder.build()); + } + } + +// +// void poo() { +// String[] args = new String[0]; +// +// Path inputPath = null, outputPath = null, namesAndDocsPath = null, librariesPath = null; +// boolean enableJavadoc = true; +// int queueDepth = 50; +// +// for (int i = 0; i < args.length; i++) { +// var arg = args[i]; +// switch (arg) { +// case "--in": +// if (i + 1 >= args.length) { +// System.err.println("Missing argument for --in"); +// System.exit(1); +// } +// inputPath = Paths.get(args[++i]); +// break; +// case "--out": +// if (i + 1 >= args.length) { +// System.err.println("Missing argument for --out"); +// System.exit(1); +// } +// outputPath = Paths.get(args[++i]); +// break; +// case "--libraries": +// if (i + 1 >= args.length) { +// System.err.println("Missing argument for --libraries"); +// System.exit(1); +// } +// librariesPath = Paths.get(args[++i]); +// break; +// case "--names": +// if (i + 1 >= args.length) { +// System.err.println("Missing argument for --names"); +// System.exit(1); +// } +// namesAndDocsPath = Paths.get(args[++i]); +// break; +// case "--skip-javadoc": +// enableJavadoc = false; +// break; +// case "--queue-depth": +// if (i + 1 >= args.length) { +// System.err.println("Missing argument for --queue-depth"); +// System.exit(1); +// } +// queueDepth = Integer.parseUnsignedInt(args[++i]); +// break; +// case "--help": +// printUsage(System.out); +// System.exit(0); +// break; +// default: +// System.err.println("Unknown argument: " + arg); +// printUsage(System.err); +// System.exit(1); +// break; +// } +// } +// +// if (inputPath == null || outputPath == null || namesAndDocsPath == null) { +// System.err.println("Missing arguments"); +// printUsage(System.err); +// System.exit(1); +// } +// +// } +} diff --git a/src/main/java/OrderedWorkQueue.java b/cli/src/main/java/net/neoforged/jst/cli/OrderedWorkQueue.java similarity index 98% rename from src/main/java/OrderedWorkQueue.java rename to cli/src/main/java/net/neoforged/jst/cli/OrderedWorkQueue.java index 1b8bba9..7f89acd 100644 --- a/src/main/java/OrderedWorkQueue.java +++ b/cli/src/main/java/net/neoforged/jst/cli/OrderedWorkQueue.java @@ -1,3 +1,5 @@ +package net.neoforged.jst.cli; + import java.io.IOException; import java.util.ArrayDeque; import java.util.Deque; diff --git a/cli/src/main/java/net/neoforged/jst/cli/PathType.java b/cli/src/main/java/net/neoforged/jst/cli/PathType.java new file mode 100644 index 0000000..992501d --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/PathType.java @@ -0,0 +1,8 @@ +package net.neoforged.jst.cli; + +public enum PathType { + AUTO, + SINGLE_FILE, + ARCHIVE, + FOLDER +} diff --git a/src/main/java/Replacement.java b/cli/src/main/java/net/neoforged/jst/cli/Replacement.java similarity index 96% rename from src/main/java/Replacement.java rename to cli/src/main/java/net/neoforged/jst/cli/Replacement.java index 5b1d872..9fad609 100644 --- a/src/main/java/Replacement.java +++ b/cli/src/main/java/net/neoforged/jst/cli/Replacement.java @@ -1,3 +1,5 @@ +package net.neoforged.jst.cli; + import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; diff --git a/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java new file mode 100644 index 0000000..98e2cde --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java @@ -0,0 +1,128 @@ +package net.neoforged.jst.cli; + +import com.intellij.openapi.vfs.VirtualFile; +import net.neoforged.jst.api.Replacements; +import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.cli.intellij.ClasspathSetup; +import net.neoforged.jst.cli.intellij.IntelliJEnvironment; +import net.neoforged.jst.cli.io.Sink; +import net.neoforged.jst.cli.io.Source; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; + +/** + * Reference for out-of-IDE usage of the IntelliJ Java parser is from the Kotlin compiler + * https://github.com/JetBrains/kotlin/blob/22aa9ee65f759ad21aeaeb8ad9ac0b123b2c32fe/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt#L108 + */ +class SourceFileProcessor implements AutoCloseable { + private final IntelliJEnvironment ijEnv = new IntelliJEnvironment(); + private int maxQueueDepth = 50; + private boolean enableJavadoc = true; + + public SourceFileProcessor() throws IOException { + ijEnv.addCurrentJdkToClassPath(); + } + + public void process(Source source, Sink sink, List transformers) throws IOException, InterruptedException { + + var sourceRoot = source.createSourceRoot(ijEnv.getAppEnv()); + ijEnv.addSourceRoot(sourceRoot); + + for (var transformer : transformers) { + transformer.beforeRun(); + } + var javaEnv = ijEnv.getProjectEnv(); + + if (sink.isOrdered()) { +// var asyncZout = new OrderedWorkQueue(new ZipOutputStream(fout), maxQueueDepth); + + source.streamEntries().forEach(entry -> { + + try (var in = entry.openInputStream()) { + if (entry.hasExtension("java")) { + var content = in.readAllBytes(); + content = transformSource(sourceRoot, entry.relativePath(), transformers, content); + sink.put(entry, content); + } else { + sink.put(entry, in); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } else { + + source.streamEntries().parallel().forEach(entry -> { + try (var in = entry.openInputStream()) { + if (entry.hasExtension("java")) { + var content = in.readAllBytes(); + content = transformSource(sourceRoot, entry.relativePath(), transformers, content); + sink.put(entry, content); + } else { + sink.put(entry, in); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + + } + + for (var transformer : transformers) { + transformer.afterRun(); + } + } + + byte[] transformSource(VirtualFile contentRoot, String path, List transformers, byte[] originalContentBytes) { + // Instead of parsing the content we actually read from the file, we read the virtual file that is + // visible to IntelliJ from adding the source jar. The reasoning is that IntelliJ will cache this internally + // and reuse it when cross-referencing type-references. If we parsed from a String instead, it would parse + // the same file twice. + var sourceFile = contentRoot.findFileByRelativePath(path); + if (sourceFile == null) { + System.err.println("Can't transform " + path + " since IntelliJ doesn't see it in the source jar."); + return originalContentBytes; + } + var psiFile = ijEnv.getPsiManager().findFile(sourceFile); + if (psiFile == null) { + System.err.println("Can't transform " + path + " since IntelliJ can't load it."); + return originalContentBytes; + } + + // Gather replaced ranges in the source-file with their replacement + var replacements = new Replacements(); + + for (var transformer : transformers) { + transformer.visitFile(psiFile, replacements); + } + + // If no replacements were made, just stream the original content into the destination file + if (replacements.isEmpty()) { + return originalContentBytes; + } + + var originalContent = psiFile.getViewProvider().getContents(); + return replacements.apply(originalContent).getBytes(StandardCharsets.UTF_8); + } + + public void setMaxQueueDepth(int maxQueueDepth) { + this.maxQueueDepth = maxQueueDepth; + } + + public void setEnableJavadoc(boolean enableJavadoc) { + this.enableJavadoc = enableJavadoc; + } + + @Override + public void close() throws IOException { + ijEnv.close(); + } + + public void addLibrariesList(Path librariesList) throws IOException { + ClasspathSetup.addLibraries(librariesList, ijEnv); + } +} diff --git a/src/main/java/ClasspathSetup.java b/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java similarity index 98% rename from src/main/java/ClasspathSetup.java rename to cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java index 3b83a4e..2718337 100644 --- a/src/main/java/ClasspathSetup.java +++ b/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java @@ -1,3 +1,5 @@ +package net.neoforged.jst.cli.intellij; + import com.intellij.core.JavaCoreProjectEnvironment; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; diff --git a/src/main/java/modules/CoreJrtFileSystem.java b/cli/src/main/java/net/neoforged/jst/cli/intellij/CoreJrtFileSystem.java similarity index 97% rename from src/main/java/modules/CoreJrtFileSystem.java rename to cli/src/main/java/net/neoforged/jst/cli/intellij/CoreJrtFileSystem.java index 6358d9b..449dece 100644 --- a/src/main/java/modules/CoreJrtFileSystem.java +++ b/cli/src/main/java/net/neoforged/jst/cli/intellij/CoreJrtFileSystem.java @@ -1,4 +1,4 @@ -package modules; +package net.neoforged.jst.cli.intellij; import com.intellij.openapi.vfs.DeprecatedVirtualFileSystem; import com.intellij.openapi.vfs.StandardFileSystems; @@ -17,7 +17,7 @@ import java.nio.file.FileSystems; import java.util.Map; -public class CoreJrtFileSystem extends DeprecatedVirtualFileSystem { +class CoreJrtFileSystem extends DeprecatedVirtualFileSystem { private final Map roots = ConcurrentFactoryMap.createMap(jdkHomePath -> { var jdkHome = new File(jdkHomePath); diff --git a/src/main/java/modules/CoreJrtVirtualFile.java b/cli/src/main/java/net/neoforged/jst/cli/intellij/CoreJrtVirtualFile.java similarity index 95% rename from src/main/java/modules/CoreJrtVirtualFile.java rename to cli/src/main/java/net/neoforged/jst/cli/intellij/CoreJrtVirtualFile.java index 2f6dea1..598a797 100644 --- a/src/main/java/modules/CoreJrtVirtualFile.java +++ b/cli/src/main/java/net/neoforged/jst/cli/intellij/CoreJrtVirtualFile.java @@ -1,4 +1,4 @@ -package modules; +package net.neoforged.jst.cli.intellij; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VfsUtilCore; @@ -22,7 +22,7 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -public class CoreJrtVirtualFile extends VirtualFile { +class CoreJrtVirtualFile extends VirtualFile { private final CoreJrtFileSystem virtualFileSystem; private final String jdkHomePath; @@ -75,7 +75,7 @@ public boolean isValid() { } @Override - public com.intellij.openapi.vfs.VirtualFile getParent() { + public VirtualFile getParent() { return parent; } @@ -85,7 +85,7 @@ public com.intellij.openapi.vfs.VirtualFile getParent() { private final ReadWriteLock rwl = new ReentrantReadWriteLock(); @Override - public com.intellij.openapi.vfs.VirtualFile[] getChildren() { + public VirtualFile[] getChildren() { rwl.readLock().lock(); try { if (myChildren != null) { diff --git a/src/main/java/IntelliJEnvironment.java b/cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironment.java similarity index 91% rename from src/main/java/IntelliJEnvironment.java rename to cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironment.java index 7e3dc1c..f357f1e 100644 --- a/src/main/java/IntelliJEnvironment.java +++ b/cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironment.java @@ -1,3 +1,5 @@ +package net.neoforged.jst.cli.intellij; + import com.intellij.core.CoreApplicationEnvironment; import com.intellij.core.JavaCoreApplicationEnvironment; import com.intellij.core.JavaCoreProjectEnvironment; @@ -11,6 +13,7 @@ import com.intellij.openapi.roots.LanguageLevelProjectExtension; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileSystem; import com.intellij.openapi.vfs.impl.ZipHandler; import com.intellij.pom.java.InternalPersistentJavaLanguageLevelReaderService; @@ -29,12 +32,13 @@ import com.intellij.psi.impl.source.tree.JavaTreeGenerator; import com.intellij.psi.impl.source.tree.TreeGenerator; import com.intellij.psi.util.JavaClassSupers; -import modules.CoreJrtFileSystem; +import org.jetbrains.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; public class IntelliJEnvironment implements AutoCloseable { @@ -86,10 +90,20 @@ public JavaCoreProjectEnvironment getProjectEnv() { return javaEnv; } - void addJarToClassPath(Path jarFile) { + public void addJarToClassPath(Path jarFile) { javaEnv.addJarToClassPath(jarFile.toFile()); } + public void addFolderToClasspath(Path folder) { + var localFile = getAppEnv().getLocalFileSystem().findFileByNioFile(folder); + Objects.requireNonNull(localFile); + javaEnv.addSourcesToClasspath(localFile); + } + + public void addSourceRoot(VirtualFile sourceRoot) { + javaEnv.addSourcesToClasspath(sourceRoot); + } + public void addCurrentJdkToClassPath() { // Add the Java Runtime we are currently running in var javaHome = Paths.get(System.getProperty("java.home")); @@ -104,7 +118,8 @@ public void close() throws IOException { Files.deleteIfExists(tempDir); } - PsiFile parseFileFromMemory(String filename, String fileContent) { + @VisibleForTesting + public PsiFile parseFileFromMemory(String filename, String fileContent) { var fileFactory = PsiFileFactory.getInstance(project); return fileFactory.createFileFromText(filename, JavaLanguage.INSTANCE, fileContent); } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/PathSourceEntry.java b/cli/src/main/java/net/neoforged/jst/cli/io/PathSourceEntry.java new file mode 100644 index 0000000..210476f --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/PathSourceEntry.java @@ -0,0 +1,49 @@ +package net.neoforged.jst.cli.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +final class PathSourceEntry implements SourceEntry { + private final Path relativeTo; + private final Path path; + private final String relativePath; + private final boolean directory; + private final long lastModified; + + public PathSourceEntry(Path relativeTo, Path path) { + this.directory = Files.isDirectory(path); + this.relativeTo = relativeTo; + this.path = path; + var relativized = relativeTo.relativize(path).toString(); + relativized = relativized.replace('\\', '/'); + this.relativePath = relativized; + try { + this.lastModified = Files.getLastModifiedTime(path).toMillis(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public boolean directory() { + return directory; + } + + @Override + public String relativePath() { + return relativePath; + } + + @Override + public long lastModified() { + return lastModified; + } + + @Override + public InputStream openInputStream() throws IOException { + return Files.newInputStream(path); + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/Sink.java b/cli/src/main/java/net/neoforged/jst/cli/io/Sink.java new file mode 100644 index 0000000..b346bcb --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/Sink.java @@ -0,0 +1,75 @@ +package net.neoforged.jst.cli.io; + +import net.neoforged.jst.cli.PathType; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class Sink implements AutoCloseable { + private final Path path; + private final PathType format; + @Nullable + private final ZipOutputStream zout; + + public Sink(Source source, Path outputPath, PathType outputFormat) throws IOException { + this.format = outputFormat == PathType.AUTO ? source.getFormat() : outputFormat; + this.path = outputPath; + this.zout = this.format == PathType.ARCHIVE ? new ZipOutputStream(Files.newOutputStream(outputPath)) : null; + } + + public void put(SourceEntry entry, byte[] content) throws IOException { + put(entry, new ByteArrayInputStream(content)); + } + + public void put(SourceEntry entry, InputStream content) throws IOException { + switch (format) { + case SINGLE_FILE -> { + Path targetPath; + if (Files.isDirectory(path)) { + targetPath = path.resolve(entry.relativePath()); + } else { + targetPath = path; + } + try (var out = Files.newOutputStream(targetPath)) { + content.transferTo(out); + } + Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); + } + case ARCHIVE -> { + if (zout != null) { + var ze = new ZipEntry(entry.relativePath()); + ze.setLastModifiedTime(FileTime.from(Instant.now())); + zout.putNextEntry(ze); + content.transferTo(zout); + zout.closeEntry(); + } + } + case FOLDER -> { + try (var out = Files.newOutputStream(path.resolve(entry.relativePath()))) { + content.transferTo(out); + } + Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); + } + default -> throw new IllegalStateException("Unexpected format: " + format); + } + } + + @Override + public void close() throws Exception { + if (zout != null) { + zout.close(); + } + } + + public boolean isOrdered() { + return format == PathType.ARCHIVE; + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/Source.java b/cli/src/main/java/net/neoforged/jst/cli/io/Source.java new file mode 100644 index 0000000..cb4b07e --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/Source.java @@ -0,0 +1,127 @@ +package net.neoforged.jst.cli.io; + +import com.intellij.core.CoreApplicationEnvironment; +import com.intellij.openapi.vfs.VirtualFile; +import net.neoforged.jst.cli.PathType; +import org.jetbrains.annotations.Nullable; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class Source implements AutoCloseable { + private final Path path; + private final PathType format; + @Nullable + private ZipFile zf = null; + @Nullable + private Stream directoryStream; + + public Source(Path path, PathType format) throws IOException { + this.path = path; + + if (!Files.exists(path)) { + throw new FileNotFoundException("File does not exist: " + path); + } + + this.format = switch (format) { + case AUTO -> { + // Directories are easy + if (Files.isDirectory(path)) { + this.zf = null; + yield PathType.FOLDER; + } else if (Files.isRegularFile(path)) { + // Try opening it as a ZIP-File first + try { + zf = new ZipFile(path.toFile()); + } catch (IOException ignored) { + } + yield zf != null ? PathType.ARCHIVE : PathType.SINGLE_FILE; + } else { + throw new IOException("Cannot detect type of " + path + " it is neither file nor folder."); + } + } + case SINGLE_FILE -> { + if (!Files.isRegularFile(path)) { + throw new IOException("Expected " + path + " to be a file."); + } + yield PathType.SINGLE_FILE; + } + case ARCHIVE -> { + if (!Files.isRegularFile(path)) { + throw new IOException("Expected " + path + " to be a file."); + } + zf = new ZipFile(path.toFile()); + yield PathType.ARCHIVE; + } + case FOLDER -> { + if (!Files.isDirectory(path)) { + throw new IOException("Expected " + path + " to be a directory."); + } + yield PathType.FOLDER; + } + }; + } + + public Path getPath() { + return path; + } + + public PathType getFormat() { + return format; + } + + public Stream streamEntries() throws IOException { + switch (format) { + case SINGLE_FILE -> { + return Stream.of(new PathSourceEntry(path.getParent(), path)); + } + case ARCHIVE -> { + return createArchiveStream(); + } + case FOLDER -> { + return Files.walk(path) + .map(child -> new PathSourceEntry(path, child)); + } + default -> throw new IllegalStateException("Unexpected format: " + format); + } + } + + private Stream createArchiveStream() { + assert zf != null; + + Spliterator spliterator = Spliterators.spliterator(zf.entries().asIterator(), zf.size(), Spliterator.IMMUTABLE | Spliterator.ORDERED); + + return StreamSupport.stream(spliterator, false).map(ze -> new ZipFileSourceEntry(zf, ze)); + } + + public VirtualFile createSourceRoot(CoreApplicationEnvironment env) { + return switch (format) { + case SINGLE_FILE -> env.getLocalFileSystem().findFileByNioFile(path.getParent()); + case FOLDER -> env.getLocalFileSystem().findFileByNioFile(path); + case ARCHIVE -> env.getJarFileSystem().findFileByPath(path.toString() + "!/"); + default -> throw new IllegalStateException("Unexpected format: " + format); + }; + } + + @Override + public void close() throws IOException { + if (zf != null) { + zf.close(); + } + if (directoryStream != null) { + directoryStream.close(); + } + } + + public boolean isOrdered() { + return format == PathType.ARCHIVE; + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntry.java b/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntry.java new file mode 100644 index 0000000..d8b8dfc --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntry.java @@ -0,0 +1,31 @@ +package net.neoforged.jst.cli.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +public interface SourceEntry { + /** + * @return True for directories. + */ + boolean directory(); + + /** + * Path to the file or directory. Uses forward slashes as path-separators, and does not have a leading slash. + */ + String relativePath(); + + /** + * @return Millis since epoch denoting when the file was last modified. 0 for directories. + */ + long lastModified(); + + /** + * @return An input stream to read this content. + */ + InputStream openInputStream() throws IOException; + + default boolean hasExtension(String extension) { + return relativePath().toLowerCase(Locale.ROOT).endsWith("." + extension.toLowerCase(Locale.ROOT)); + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java b/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java new file mode 100644 index 0000000..7f48ee1 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java @@ -0,0 +1,6 @@ +package net.neoforged.jst.cli.io; + +import java.io.InputStream; + +public record SourceEntryWithContent(SourceEntry sourceEntry, InputStream contentStream) { +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileSourceEntry.java b/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileSourceEntry.java new file mode 100644 index 0000000..050d2c4 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileSourceEntry.java @@ -0,0 +1,36 @@ +package net.neoforged.jst.cli.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +class ZipFileSourceEntry implements SourceEntry { + private final ZipFile zipFile; + private final ZipEntry zipEntry; + + public ZipFileSourceEntry(ZipFile zipFile, ZipEntry zipEntry) { + this.zipFile = zipFile; + this.zipEntry = zipEntry; + } + + @Override + public boolean directory() { + return zipEntry.isDirectory(); + } + + @Override + public String relativePath() { + return zipEntry.getName(); + } + + @Override + public long lastModified() { + return zipEntry.getLastModifiedTime().toMillis(); + } + + @Override + public InputStream openInputStream() throws IOException { + return zipFile.getInputStream(zipEntry); + } +} diff --git a/src/test/java/PsiHelperTest.java b/cli/src/test/java/net/neoforged/jst/cli/PsiHelperTest.java similarity index 97% rename from src/test/java/PsiHelperTest.java rename to cli/src/test/java/net/neoforged/jst/cli/PsiHelperTest.java index 768e8e7..6d1d2c2 100644 --- a/src/test/java/PsiHelperTest.java +++ b/cli/src/test/java/net/neoforged/jst/cli/PsiHelperTest.java @@ -1,7 +1,11 @@ +package net.neoforged.jst.cli; + import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiMethod; import com.intellij.psi.util.PsiTreeUtil; +import net.neoforged.jst.api.PsiHelper; +import net.neoforged.jst.cli.intellij.IntelliJEnvironment; import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..73644fe --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ + +intellij_version=233.11799.300 +jetbrains_annotations_version=24.1.0 +picocli_version=4.7.5 +junit_version=5.9.1 +assertj_version=3.24.2 diff --git a/parchment/build.gradle b/parchment/build.gradle new file mode 100644 index 0000000..9f82cb3 --- /dev/null +++ b/parchment/build.gradle @@ -0,0 +1,9 @@ +plugins { + id "java-library" +} + +dependencies { + implementation project(":api") + implementation 'org.parchmentmc.feather:io-gson:1.1.0' + implementation 'net.fabricmc:mapping-io:0.5.1' +} diff --git a/src/main/java/GatherReplacementsVisitor.java b/parchment/src/main/java/net/neoforged/jst/parchment/GatherReplacementsVisitor.java similarity index 83% rename from src/main/java/GatherReplacementsVisitor.java rename to parchment/src/main/java/net/neoforged/jst/parchment/GatherReplacementsVisitor.java index 7ff774c..d6c7755 100644 --- a/src/main/java/GatherReplacementsVisitor.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/GatherReplacementsVisitor.java @@ -1,3 +1,5 @@ +package net.neoforged.jst.parchment; + import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; @@ -8,8 +10,10 @@ import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiReferenceExpression; import com.intellij.psi.search.GlobalSearchScope; -import namesanddocs.NamesAndDocsDatabase; -import namesanddocs.NamesAndDocsForParameter; +import net.neoforged.jst.api.PsiHelper; +import net.neoforged.jst.api.Replacements; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsDatabase; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForParameter; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -23,7 +27,7 @@ class GatherReplacementsVisitor extends PsiRecursiveElementVisitor { private final NamesAndDocsDatabase namesAndDocs; private final boolean enableJavadoc; - private final List replacements; + private final Replacements replacements; /** * Renamed parameters of the combined outer scopes we are currently visiting. * Since scopes may be nested (classes defined in method bodies and their methods), @@ -33,7 +37,7 @@ class GatherReplacementsVisitor extends PsiRecursiveElementVisitor { public GatherReplacementsVisitor(NamesAndDocsDatabase namesAndDocs, boolean enableJavadoc, - List replacements) { + Replacements replacements) { this.namesAndDocs = namesAndDocs; this.enableJavadoc = enableJavadoc; this.replacements = replacements; @@ -56,19 +60,19 @@ public void visitElement(@NotNull PsiElement element) { } // Add javadoc if available - var classData = PsiHelper.getClassData(namesAndDocs, psiClass); + var classData = PsiParchmentHelper.getClassData(namesAndDocs, psiClass); if (classData != null) { applyJavadoc(psiClass, classData.getJavadoc(), replacements); } } else if (element instanceof PsiField psiField) { - var classData = PsiHelper.getClassData(namesAndDocs, psiField.getContainingClass()); + var classData = PsiParchmentHelper.getClassData(namesAndDocs, psiField.getContainingClass()); var fieldData = classData != null ? classData.getField(psiField.getName()) : null; if (fieldData != null) { // Add javadoc if available applyJavadoc(psiField, fieldData.getJavadoc(), replacements); } } else if (element instanceof PsiMethod psiMethod) { - var methodData = PsiHelper.getMethodData(namesAndDocs, psiMethod); + var methodData = PsiParchmentHelper.getMethodData(namesAndDocs, psiMethod); if (methodData != null) { Map parameterJavadoc = new HashMap<>(); @@ -95,7 +99,7 @@ public void visitElement(@NotNull PsiElement element) { activeParameters.put(psiParameter, paramData); // Find and replace the parameter identifier - replacements.add(Replacement.replace(psiParameter.getNameIdentifier(), paramData.getName())); + replacements.replace(psiParameter.getNameIdentifier(), paramData.getName()); // Record the replacement for remapping existing Javadoc @param tags renamedParameters.put(psiParameter.getName(), paramData.getName()); @@ -117,14 +121,16 @@ public void visitElement(@NotNull PsiElement element) { } // Add javadoc if available - JavadocHelper.enrichJavadoc( - psiMethod, - methodData.getJavadoc(), - parameterJavadoc, - renamedParameters, - parameterOrder, - replacements - ); + if (enableJavadoc) { + JavadocHelper.enrichJavadoc( + psiMethod, + methodData.getJavadoc(), + parameterJavadoc, + renamedParameters, + parameterOrder, + replacements + ); + } // When replacements were made and activeParamets were added, we visit the method children here ourselves // and clean up active parameters afterward @@ -142,7 +148,7 @@ public void visitElement(@NotNull PsiElement element) { } else if (element instanceof PsiReferenceExpression refExpr && refExpr.getReferenceNameElement() != null) { for (var entry : activeParameters.entrySet()) { if (refExpr.isReferenceTo(entry.getKey())) { - replacements.add(Replacement.replace(refExpr.getReferenceNameElement(), entry.getValue().getName())); + replacements.replace(refExpr.getReferenceNameElement(), entry.getValue().getName()); break; } } @@ -153,7 +159,7 @@ public void visitElement(@NotNull PsiElement element) { private void applyJavadoc(PsiJavaDocumentedElement psiElement, List javadoc, - List replacements) { + Replacements replacements) { if (enableJavadoc && !javadoc.isEmpty()) { JavadocHelper.enrichJavadoc(psiElement, javadoc, replacements); } diff --git a/src/main/java/JavadocHelper.java b/parchment/src/main/java/net/neoforged/jst/parchment/JavadocHelper.java similarity index 97% rename from src/main/java/JavadocHelper.java rename to parchment/src/main/java/net/neoforged/jst/parchment/JavadocHelper.java index 1af4505..8159f80 100644 --- a/src/main/java/JavadocHelper.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/JavadocHelper.java @@ -1,3 +1,5 @@ +package net.neoforged.jst.parchment; + import com.intellij.psi.JavaDocTokenType; import com.intellij.psi.PsiDocCommentBase; import com.intellij.psi.PsiElement; @@ -5,6 +7,8 @@ import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.javadoc.PsiDocComment; import com.intellij.psi.javadoc.PsiDocToken; +import net.neoforged.jst.api.Replacement; +import net.neoforged.jst.api.Replacements; import org.jetbrains.annotations.Nullable; import java.text.BreakIterator; @@ -14,7 +18,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -44,7 +47,7 @@ private JavadocHelper() { public static void enrichJavadoc(PsiJavaDocumentedElement psiElement, List javadoc, - List replacements) { + Replacements replacements) { enrichJavadoc(psiElement, javadoc, Map.of(), Map.of(), List.of(), replacements); } @@ -53,7 +56,7 @@ public static void enrichJavadoc(PsiJavaDocumentedElement psiElement, Map parameters, Map renamedParameters, List parameterOrder, - List replacements) { + Replacements replacements) { var existingDocComment = psiElement.getDocComment(); if (existingDocComment != null) { @@ -90,10 +93,10 @@ public static void enrichJavadoc(PsiJavaDocumentedElement psiElement, parameterDocs.putAll(parameters); var indent = JavadocHelper.getIndent(existingDocComment); - replacements.add(Replacement.replace( + replacements.replace( existingDocComment, JavadocHelper.formatJavadoc(indent, bodyLines, tags, parameterDocs, parameterOrder) - )); + ); } else { // If no parameter documentation or javadoc is given @@ -106,13 +109,13 @@ public static void enrichJavadoc(PsiJavaDocumentedElement psiElement, if (psiElement.getPrevSibling() instanceof PsiWhiteSpace psiWhiteSpace) { indent = JavadocHelper.getLastLineLength(psiWhiteSpace); } - replacements.add(Replacement.insertBefore( + replacements.insertBefore( psiElement, JavadocHelper.formatJavadoc(indent, javadoc, List.of(), parameters, parameterOrder) // We have to make an indent part of the replacement since it will now be // in front of our comment, making the original element unindented + "\n" + " ".repeat(indent) - )); + ); } } diff --git a/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentPlugin.java b/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentPlugin.java new file mode 100644 index 0000000..9b92fd3 --- /dev/null +++ b/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentPlugin.java @@ -0,0 +1,16 @@ +package net.neoforged.jst.parchment; + +import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.api.SourceTransformerPlugin; + +public class ParchmentPlugin implements SourceTransformerPlugin { + @Override + public String getName() { + return "parchment"; + } + + @Override + public SourceTransformer createTransformer() { + return new ParchmentTransformer(); + } +} diff --git a/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentTransformer.java b/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentTransformer.java new file mode 100644 index 0000000..ba831b8 --- /dev/null +++ b/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentTransformer.java @@ -0,0 +1,41 @@ +package net.neoforged.jst.parchment; + +import com.intellij.psi.PsiFile; +import net.neoforged.jst.api.Replacements; +import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.parchment.namesanddocs.NameAndDocSourceLoader; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsDatabase; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; + +public class ParchmentTransformer implements SourceTransformer { + @CommandLine.Option(names = "--parchment-mappings", required = true) + public Path mappingsPath; + + @CommandLine.Option(names = "--parchment-javadoc") + public boolean enableJavadoc = true; + + private NamesAndDocsDatabase namesAndDocs; + + @Override + public void beforeRun() { + System.out.println("Loading mapping file " + mappingsPath); + try { + namesAndDocs = NameAndDocSourceLoader.load(mappingsPath); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void visitFile(PsiFile psiFile, Replacements replacements) { + + var visitor = new GatherReplacementsVisitor(namesAndDocs, enableJavadoc, replacements); + visitor.visitElement(psiFile); + + } + +} diff --git a/parchment/src/main/java/net/neoforged/jst/parchment/PsiParchmentHelper.java b/parchment/src/main/java/net/neoforged/jst/parchment/PsiParchmentHelper.java new file mode 100644 index 0000000..316dc48 --- /dev/null +++ b/parchment/src/main/java/net/neoforged/jst/parchment/PsiParchmentHelper.java @@ -0,0 +1,64 @@ +package net.neoforged.jst.parchment; + +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import net.neoforged.jst.api.PsiHelper; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsDatabase; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForClass; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForMethod; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public class PsiParchmentHelper { + // Keys for attaching mapping data to a PsiClass. Used to prevent multiple lookups for the same class/method. + private static final Key> CLASS_DATA_KEY = Key.create("names_and_docs_for_class"); + private static final Key> METHOD_DATA_KEY = Key.create("names_and_docs_for_method"); + + @SuppressWarnings("OptionalAssignedToNull") + @Nullable + public static NamesAndDocsForClass getClassData(NamesAndDocsDatabase namesAndDocs, @Nullable PsiClass psiClass) { + if (psiClass == null) { + return null; + } + var classData = psiClass.getUserData(CLASS_DATA_KEY); + if (classData != null) { + return classData.orElse(null); + } else { + var sb = new StringBuilder(); + PsiHelper.getBinaryClassName(psiClass, sb); + if (sb.isEmpty()) { + classData = Optional.empty(); + } else { + classData = Optional.ofNullable(namesAndDocs.getClass(sb.toString())); + } + psiClass.putUserData(CLASS_DATA_KEY, classData); + return classData.orElse(null); + } + } + + @SuppressWarnings("OptionalAssignedToNull") + @Nullable + public static NamesAndDocsForMethod getMethodData(NamesAndDocsDatabase namesAndDocs, @Nullable PsiMethod psiMethod) { + if (psiMethod == null) { + return null; + } + var methodData = psiMethod.getUserData(METHOD_DATA_KEY); + if (methodData != null) { + return methodData.orElse(null); + } else { + methodData = Optional.empty(); + var classData = getClassData(namesAndDocs, psiMethod.getContainingClass()); + if (classData != null) { + var methodName = PsiHelper.getBinaryMethodName(psiMethod); + var methodSignature = PsiHelper.getBinaryMethodSignature(psiMethod); + methodData = Optional.ofNullable(classData.getMethod(methodName, methodSignature)); + } + + psiMethod.putUserData(METHOD_DATA_KEY, methodData); + return methodData.orElse(null); + } + } + +} diff --git a/src/main/java/namesanddocs/NameAndDocSourceLoader.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NameAndDocSourceLoader.java similarity index 89% rename from src/main/java/namesanddocs/NameAndDocSourceLoader.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NameAndDocSourceLoader.java index e910342..0b0df80 100644 --- a/src/main/java/namesanddocs/NameAndDocSourceLoader.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NameAndDocSourceLoader.java @@ -1,10 +1,10 @@ -package namesanddocs; +package net.neoforged.jst.parchment.namesanddocs; -import namesanddocs.mappingio.TreeData; -import namesanddocs.parchment.ParchmentDatabase; import net.fabricmc.mappingio.MappingReader; import net.fabricmc.mappingio.format.MappingFormat; import net.fabricmc.mappingio.tree.MemoryMappingTree; +import net.neoforged.jst.parchment.namesanddocs.mappingio.TreeData; +import net.neoforged.jst.parchment.namesanddocs.parchment.ParchmentDatabase; import org.jetbrains.annotations.Nullable; import java.io.IOException; diff --git a/src/main/java/namesanddocs/NameAndDocsFormat.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NameAndDocsFormat.java similarity index 62% rename from src/main/java/namesanddocs/NameAndDocsFormat.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NameAndDocsFormat.java index 8367d85..a1eb7da 100644 --- a/src/main/java/namesanddocs/NameAndDocsFormat.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NameAndDocsFormat.java @@ -1,4 +1,4 @@ -package namesanddocs; +package net.neoforged.jst.parchment.namesanddocs; public enum NameAndDocsFormat { PARCHMENT_ZIP, diff --git a/src/main/java/namesanddocs/NamesAndDocsDatabase.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsDatabase.java similarity index 65% rename from src/main/java/namesanddocs/NamesAndDocsDatabase.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsDatabase.java index f0f3292..0089bf1 100644 --- a/src/main/java/namesanddocs/NamesAndDocsDatabase.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsDatabase.java @@ -1,4 +1,4 @@ -package namesanddocs; +package net.neoforged.jst.parchment.namesanddocs; public interface NamesAndDocsDatabase { NamesAndDocsForClass getClass(String className); diff --git a/src/main/java/namesanddocs/NamesAndDocsForClass.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForClass.java similarity index 81% rename from src/main/java/namesanddocs/NamesAndDocsForClass.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForClass.java index 0c16aab..6d1f528 100644 --- a/src/main/java/namesanddocs/NamesAndDocsForClass.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForClass.java @@ -1,4 +1,4 @@ -package namesanddocs; +package net.neoforged.jst.parchment.namesanddocs; import java.util.List; diff --git a/src/main/java/namesanddocs/NamesAndDocsForField.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForField.java similarity index 66% rename from src/main/java/namesanddocs/NamesAndDocsForField.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForField.java index f5c27c5..84ebd2a 100644 --- a/src/main/java/namesanddocs/NamesAndDocsForField.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForField.java @@ -1,4 +1,4 @@ -package namesanddocs; +package net.neoforged.jst.parchment.namesanddocs; import java.util.List; diff --git a/src/main/java/namesanddocs/NamesAndDocsForMethod.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForMethod.java similarity index 75% rename from src/main/java/namesanddocs/NamesAndDocsForMethod.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForMethod.java index 4ce1915..bff1fa2 100644 --- a/src/main/java/namesanddocs/NamesAndDocsForMethod.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForMethod.java @@ -1,4 +1,4 @@ -package namesanddocs; +package net.neoforged.jst.parchment.namesanddocs; import java.util.List; diff --git a/src/main/java/namesanddocs/NamesAndDocsForParameter.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForParameter.java similarity index 76% rename from src/main/java/namesanddocs/NamesAndDocsForParameter.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForParameter.java index 6af59f7..9098079 100644 --- a/src/main/java/namesanddocs/NamesAndDocsForParameter.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/NamesAndDocsForParameter.java @@ -1,4 +1,4 @@ -package namesanddocs; +package net.neoforged.jst.parchment.namesanddocs; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/namesanddocs/mappingio/TreeClassData.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeClassData.java similarity index 72% rename from src/main/java/namesanddocs/mappingio/TreeClassData.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeClassData.java index 2f12487..8741343 100644 --- a/src/main/java/namesanddocs/mappingio/TreeClassData.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeClassData.java @@ -1,9 +1,9 @@ -package namesanddocs.mappingio; +package net.neoforged.jst.parchment.namesanddocs.mappingio; -import namesanddocs.NamesAndDocsForClass; -import namesanddocs.NamesAndDocsForField; -import namesanddocs.NamesAndDocsForMethod; import net.fabricmc.mappingio.tree.MappingTree; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForClass; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForField; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForMethod; import java.util.List; diff --git a/src/main/java/namesanddocs/mappingio/TreeData.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeData.java similarity index 68% rename from src/main/java/namesanddocs/mappingio/TreeData.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeData.java index bef593b..2e9f7ec 100644 --- a/src/main/java/namesanddocs/mappingio/TreeData.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeData.java @@ -1,8 +1,8 @@ -package namesanddocs.mappingio; +package net.neoforged.jst.parchment.namesanddocs.mappingio; -import namesanddocs.NamesAndDocsDatabase; -import namesanddocs.NamesAndDocsForClass; import net.fabricmc.mappingio.tree.MemoryMappingTree; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsDatabase; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForClass; public class TreeData implements NamesAndDocsDatabase { private final MemoryMappingTree tree; diff --git a/src/main/java/namesanddocs/mappingio/TreeMethodData.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeMethodData.java similarity index 82% rename from src/main/java/namesanddocs/mappingio/TreeMethodData.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeMethodData.java index 8c6b294..55da46a 100644 --- a/src/main/java/namesanddocs/mappingio/TreeMethodData.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/mappingio/TreeMethodData.java @@ -1,8 +1,8 @@ -package namesanddocs.mappingio; +package net.neoforged.jst.parchment.namesanddocs.mappingio; -import namesanddocs.NamesAndDocsForMethod; -import namesanddocs.NamesAndDocsForParameter; import net.fabricmc.mappingio.tree.MappingTree; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForMethod; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForParameter; import org.jetbrains.annotations.Nullable; import java.util.List; diff --git a/src/main/java/namesanddocs/parchment/ParchmentDatabase.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentDatabase.java similarity index 92% rename from src/main/java/namesanddocs/parchment/ParchmentDatabase.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentDatabase.java index 8b1b605..799f181 100644 --- a/src/main/java/namesanddocs/parchment/ParchmentDatabase.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentDatabase.java @@ -1,9 +1,9 @@ -package namesanddocs.parchment; +package net.neoforged.jst.parchment.namesanddocs.parchment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import namesanddocs.NamesAndDocsDatabase; -import namesanddocs.NamesAndDocsForClass; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsDatabase; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForClass; import org.parchmentmc.feather.io.gson.MDCGsonAdapterFactory; import org.parchmentmc.feather.io.gson.SimpleVersionAdapter; import org.parchmentmc.feather.mapping.VersionedMappingDataContainer; diff --git a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForClass.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForClass.java similarity index 77% rename from src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForClass.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForClass.java index 2cfa0e9..2253a03 100644 --- a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForClass.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForClass.java @@ -1,8 +1,8 @@ -package namesanddocs.parchment; +package net.neoforged.jst.parchment.namesanddocs.parchment; -import namesanddocs.NamesAndDocsForClass; -import namesanddocs.NamesAndDocsForField; -import namesanddocs.NamesAndDocsForMethod; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForClass; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForField; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForMethod; import org.parchmentmc.feather.mapping.MappingDataContainer; import java.util.List; diff --git a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForField.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForField.java similarity index 77% rename from src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForField.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForField.java index 4133705..64408e7 100644 --- a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForField.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForField.java @@ -1,6 +1,6 @@ -package namesanddocs.parchment; +package net.neoforged.jst.parchment.namesanddocs.parchment; -import namesanddocs.NamesAndDocsForField; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForField; import org.parchmentmc.feather.mapping.MappingDataContainer; import java.util.List; diff --git a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForMethod.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForMethod.java similarity index 77% rename from src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForMethod.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForMethod.java index 7c62613..2e0680b 100644 --- a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForMethod.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForMethod.java @@ -1,7 +1,7 @@ -package namesanddocs.parchment; +package net.neoforged.jst.parchment.namesanddocs.parchment; -import namesanddocs.NamesAndDocsForMethod; -import namesanddocs.NamesAndDocsForParameter; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForMethod; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForParameter; import org.parchmentmc.feather.mapping.MappingDataContainer; import java.util.List; diff --git a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForParameter.java b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForParameter.java similarity index 81% rename from src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForParameter.java rename to parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForParameter.java index c91136b..3ffeb49 100644 --- a/src/main/java/namesanddocs/parchment/ParchmentNamesAndDocsForParameter.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/namesanddocs/parchment/ParchmentNamesAndDocsForParameter.java @@ -1,6 +1,6 @@ -package namesanddocs.parchment; +package net.neoforged.jst.parchment.namesanddocs.parchment; -import namesanddocs.NamesAndDocsForParameter; +import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsForParameter; import org.jetbrains.annotations.Nullable; import org.parchmentmc.feather.mapping.MappingDataContainer; diff --git a/parchment/src/main/resources/META-INF/services/net.neoforged.jst.api.SourceTransformerPlugin b/parchment/src/main/resources/META-INF/services/net.neoforged.jst.api.SourceTransformerPlugin new file mode 100644 index 0000000..183b115 --- /dev/null +++ b/parchment/src/main/resources/META-INF/services/net.neoforged.jst.api.SourceTransformerPlugin @@ -0,0 +1 @@ +net.neoforged.jst.parchment.ParchmentPlugin \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d02688d..d31ba40 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,25 @@ pluginManagement { id 'net.neoforged.gradleutils' version '3.0.0-alpha.8' } } + +dependencyResolutionManagement { + repositories { + mavenCentral() + maven { + url "https://www.jetbrains.com/intellij-repository/releases/" + } + maven { + url "https://cache-redirector.jetbrains.com/intellij-dependencies/" + } + maven { + url "https://maven.parchmentmc.org/" + } + } +} + rootProject.name = 'JavaSourceTransformer' + +include 'api' +include 'cli' +include 'parchment' +include 'tests' diff --git a/src/main/java/ApplyParchmentToSourceJar.java b/src/main/java/ApplyParchmentToSourceJar.java deleted file mode 100644 index 23331c7..0000000 --- a/src/main/java/ApplyParchmentToSourceJar.java +++ /dev/null @@ -1,235 +0,0 @@ -import com.intellij.openapi.vfs.VirtualFile; -import namesanddocs.NameAndDocSourceLoader; -import namesanddocs.NamesAndDocsDatabase; -import org.jetbrains.annotations.NotNull; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - -/** - * Reference for out-of-IDE usage of the IntelliJ Java parser is from the Kotlin compiler - * https://github.com/JetBrains/kotlin/blob/22aa9ee65f759ad21aeaeb8ad9ac0b123b2c32fe/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt#L108 - */ -public class ApplyParchmentToSourceJar implements AutoCloseable { - private final NamesAndDocsDatabase namesAndDocs; - private final IntelliJEnvironment ijEnv = new IntelliJEnvironment(); - private int maxQueueDepth = 50; - private boolean enableJavadoc = true; - - public ApplyParchmentToSourceJar(NamesAndDocsDatabase namesAndDocs) throws IOException { - this.namesAndDocs = namesAndDocs; - ijEnv.addCurrentJdkToClassPath(); - } - - public static void main(String[] args) throws Exception { - Path inputPath = null, outputPath = null, namesAndDocsPath = null, librariesPath = null; - boolean enableJavadoc = true; - int queueDepth = 50; - - for (int i = 0; i < args.length; i++) { - var arg = args[i]; - switch (arg) { - case "--in": - if (i + 1 >= args.length) { - System.err.println("Missing argument for --in"); - System.exit(1); - } - inputPath = Paths.get(args[++i]); - break; - case "--out": - if (i + 1 >= args.length) { - System.err.println("Missing argument for --out"); - System.exit(1); - } - outputPath = Paths.get(args[++i]); - break; - case "--libraries": - if (i + 1 >= args.length) { - System.err.println("Missing argument for --libraries"); - System.exit(1); - } - librariesPath = Paths.get(args[++i]); - break; - case "--names": - if (i + 1 >= args.length) { - System.err.println("Missing argument for --names"); - System.exit(1); - } - namesAndDocsPath = Paths.get(args[++i]); - break; - case "--skip-javadoc": - enableJavadoc = false; - break; - case "--queue-depth": - if (i + 1 >= args.length) { - System.err.println("Missing argument for --queue-depth"); - System.exit(1); - } - queueDepth = Integer.parseUnsignedInt(args[++i]); - break; - case "--help": - printUsage(System.out); - System.exit(0); - break; - default: - System.err.println("Unknown argument: " + arg); - printUsage(System.err); - System.exit(1); - break; - } - } - - if (inputPath == null || outputPath == null || namesAndDocsPath == null) { - System.err.println("Missing arguments"); - printUsage(System.err); - System.exit(1); - } - - var namesAndDocs = NameAndDocSourceLoader.load(namesAndDocsPath); - - try (var applyParchment = new ApplyParchmentToSourceJar(namesAndDocs)) { - // Add external libraries to classpath - if (librariesPath != null) { - ClasspathSetup.addLibraries(librariesPath, applyParchment.ijEnv); - } - - applyParchment.setMaxQueueDepth(queueDepth); - applyParchment.setEnableJavadoc(enableJavadoc); - applyParchment.apply(inputPath, outputPath); - } - } - - private static void printUsage(PrintStream out) { - out.println("Arguments:"); - out.println(" --in Path to input source-jar"); - out.println(" --out Path where new source-jar will be written"); - out.println(" --names Path to Parchment ZIP-File or merged TSRG2-Mappings"); - out.println(" --skip-javadoc Don't apply Javadocs"); - out.println(" --queue-depth How many source files to wait for in parallel. 0 for synchronous processing."); - out.println(" 0 for synchronous processing. Default is 50."); - out.println(" --help Print help"); - } - - public void apply(Path inputPath, Path outputPath) throws IOException, InterruptedException { - - var javaEnv = ijEnv.getProjectEnv(); - - var sourceJarRoot = javaEnv.getEnvironment().getJarFileSystem().findFileByPath(inputPath + "!/"); - if (sourceJarRoot == null) { - throw new FileNotFoundException("Cannot find JAR-File " + inputPath); - } - - javaEnv.addSourcesToClasspath(sourceJarRoot); - - try (var zin = new ZipInputStream(Files.newInputStream(inputPath)); - var fout = Files.newOutputStream(outputPath); - var asyncZout = new OrderedWorkQueue(new ZipOutputStream(fout), maxQueueDepth)) { - - for (var entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) { - var originalContentBytes = zin.readAllBytes(); - - var entryPath = entry.getName(); - if (entryPath.endsWith(".java")) { - asyncZout.submitAsync(entry, () -> { - return transformSource(sourceJarRoot, entryPath, originalContentBytes); - }); - } else { - asyncZout.submit(entry, originalContentBytes); - } - } - } - } - - void addJarToClassPath(Path jarFile) { - ijEnv.addJarToClassPath(jarFile); - } - - byte[] transformSource(VirtualFile contentRoot, String path, byte[] originalContentBytes) { - // Instead of parsing the content we actually read from the file, we read the virtual file that is - // visible to IntelliJ from adding the source jar. The reasoning is that IntelliJ will cache this internally - // and reuse it when cross-referencing type-references. If we parsed from a String instead, it would parse - // the same file twice. - var sourceFile = contentRoot.findFileByRelativePath(path); - if (sourceFile == null) { - System.err.println("Can't transform " + path + " since IntelliJ doesn't see it in the source jar."); - return originalContentBytes; - } - var psiFile = ijEnv.getPsiManager().findFile(sourceFile); - if (psiFile == null) { - System.err.println("Can't transform " + path + " since IntelliJ can't load it."); - return originalContentBytes; - } - - // Gather replaced ranges in the source-file with their replacement - List replacements = new ArrayList<>(); - - var visitor = new GatherReplacementsVisitor(namesAndDocs, enableJavadoc, replacements); - visitor.visitElement(psiFile); - - // If no replacements were made, just stream the original content into the destination file - if (replacements.isEmpty()) { - return originalContentBytes; - } - - var originalContent = psiFile.getViewProvider().getContents(); - return applyReplacements(originalContent, replacements).getBytes(StandardCharsets.UTF_8); - } - - @NotNull - private static String applyReplacements(CharSequence originalContent, List replacements) { - // We will assemble the resulting file by iterating all ranges (replaced or not) - // For this to work, the replacement ranges need to be in ascending order and non-overlapping - replacements.sort(Replacement.COMPARATOR); - - var writer = new StringBuilder(); - // Copy up until the first replacement - - writer.append(originalContent, 0, replacements.get(0).range().getStartOffset()); - for (int i = 0; i < replacements.size(); i++) { - var replacement = replacements.get(i); - var range = replacement.range(); - if (i > 0) { - // Copy between previous and current replacement verbatim - var previousReplacement = replacements.get(i - 1); - // validate that replacement ranges are non-overlapping - if (previousReplacement.range().getEndOffset() > range.getStartOffset()) { - throw new IllegalStateException("Trying to replace overlapping ranges: " - + replacement + " and " + previousReplacement); - } - - writer.append( - originalContent, - previousReplacement.range().getEndOffset(), - range.getStartOffset() - ); - } - writer.append(replacement.newText()); - } - writer.append(originalContent, replacements.get(replacements.size() - 1).range().getEndOffset(), originalContent.length()); - return writer.toString(); - } - - - public void setMaxQueueDepth(int maxQueueDepth) { - this.maxQueueDepth = maxQueueDepth; - } - - public void setEnableJavadoc(boolean enableJavadoc) { - this.enableJavadoc = enableJavadoc; - } - - @Override - public void close() throws IOException { - ijEnv.close(); - } - -} diff --git a/src/main/resources/META-INF/LightJavaPlugin.xml b/src/main/resources/META-INF/LightJavaPlugin.xml deleted file mode 100644 index 07c0620..0000000 --- a/src/main/resources/META-INF/LightJavaPlugin.xml +++ /dev/null @@ -1,5 +0,0 @@ - - lightjava - - - diff --git a/src/test/java/ApplyParchmentToSourceJarTest.java b/src/test/java/ApplyParchmentToSourceJarTest.java deleted file mode 100644 index 8cbea2f..0000000 --- a/src/test/java/ApplyParchmentToSourceJarTest.java +++ /dev/null @@ -1,84 +0,0 @@ -import com.intellij.util.io.ZipUtil; -import namesanddocs.NameAndDocSourceLoader; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Test that references to external classes in method signatures are correctly resolved. - */ -public class ApplyParchmentToSourceJarTest { - @TempDir - private Path tempDir; - - @Test - void testInnerAndLocalClasses() throws Exception { - runTest("/nested"); - } - - @Test - void testExternalReferences() throws Exception { - runTest("/external_refs"); - } - - @Test - void testParamIndices() throws Exception { - runTest("/param_indices"); - } - - @Test - void testJavadoc() throws Exception { - runTest("/javadoc"); - } - - protected final void runTest(String testDir) throws Exception { - var parchmentFile = Paths.get(getClass().getResource(testDir + "/parchment.json").toURI()); - var sourceDir = parchmentFile.resolveSibling("source"); - var expectedDir = parchmentFile.resolveSibling("expected"); - - var inputFile = tempDir.resolve("input.jar"); - try (var zos = new ZipOutputStream(Files.newOutputStream(inputFile))) { - ZipUtil.addDirToZipRecursively(zos, null, sourceDir.toFile(), "", file -> { - return file.isDirectory() || file.getName().endsWith(".java"); - }, null); - } - - var ouptutFile = tempDir.resolve("output.jar"); - try (var remapper = new ApplyParchmentToSourceJar(NameAndDocSourceLoader.load(parchmentFile))) { - // For testing external references, add JUnit-API so it can be referenced - var junitJarPath = Paths.get(Test.class.getProtectionDomain().getCodeSource().getLocation().toURI()); - remapper.addJarToClassPath(junitJarPath); - - // Easier to debug without concurrency - remapper.setMaxQueueDepth(0); - - remapper.apply(inputFile, ouptutFile); - } - - try (var zipFile = new ZipFile(ouptutFile.toFile())) { - var it = zipFile.entries().asIterator(); - while (it.hasNext()) { - var entry = it.next(); - if (entry.isDirectory()) { - continue; - } - - var actualFile = normalizeLines(new String(zipFile.getInputStream(entry).readAllBytes(), StandardCharsets.UTF_8)); - var expectedFile = normalizeLines(Files.readString(expectedDir.resolve(entry.getName()), StandardCharsets.UTF_8)); - assertEquals(expectedFile, actualFile); - } - } - } - - private String normalizeLines(String s) { - return s.replaceAll("\r\n", "\n"); - } -} diff --git a/tests/build.gradle b/tests/build.gradle new file mode 100644 index 0000000..94be220 --- /dev/null +++ b/tests/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java-library' +} + +configurations { + cli { + canBeConsumed = false + canBeResolved = true + transitive = false + } +} + +dependencies { + cli project(path: ':cli', configuration: 'shadow') + testImplementation project(path: ':cli', configuration: 'shadow') + + testImplementation platform("org.junit:junit-bom:$junit_version") + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation "org.assertj:assertj-core:$assertj_version" +} + +abstract class ExecutableArgumentProvider implements CommandLineArgumentProvider { + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + abstract ConfigurableFileCollection getConfiguration() + + @Override + Iterable asArguments() { + ["-Djst.executableJar=${configuration.singleFile}"] + } +} + +test { + useJUnitPlatform() + jvmArgumentProviders.add( + objects.newInstance(ExecutableArgumentProvider).tap { + configuration = configurations.cli + } + ) + systemProperty("jst.testDataDir", "${project.projectDir}/data") + + if (Boolean.getBoolean("test.debug") || Boolean.getBoolean("jst.debug") || System.getProperty("idea.debugger.dispatch.port") != null) { + systemProperty("jst.debug", "true") + } +} diff --git a/src/test/resources/external_refs/expected/TestClass.java b/tests/data/external_refs/expected/TestClass.java similarity index 100% rename from src/test/resources/external_refs/expected/TestClass.java rename to tests/data/external_refs/expected/TestClass.java diff --git a/src/test/resources/external_refs/parchment.json b/tests/data/external_refs/parchment.json similarity index 100% rename from src/test/resources/external_refs/parchment.json rename to tests/data/external_refs/parchment.json diff --git a/src/test/resources/external_refs/source/TestClass.java b/tests/data/external_refs/source/TestClass.java similarity index 100% rename from src/test/resources/external_refs/source/TestClass.java rename to tests/data/external_refs/source/TestClass.java diff --git a/src/test/resources/javadoc/expected/ExistingJavadoc.java b/tests/data/javadoc/expected/ExistingJavadoc.java similarity index 100% rename from src/test/resources/javadoc/expected/ExistingJavadoc.java rename to tests/data/javadoc/expected/ExistingJavadoc.java diff --git a/src/test/resources/javadoc/expected/NoExistingJavadoc.java b/tests/data/javadoc/expected/NoExistingJavadoc.java similarity index 100% rename from src/test/resources/javadoc/expected/NoExistingJavadoc.java rename to tests/data/javadoc/expected/NoExistingJavadoc.java diff --git a/src/test/resources/javadoc/parchment.json b/tests/data/javadoc/parchment.json similarity index 100% rename from src/test/resources/javadoc/parchment.json rename to tests/data/javadoc/parchment.json diff --git a/src/test/resources/javadoc/source/ExistingJavadoc.java b/tests/data/javadoc/source/ExistingJavadoc.java similarity index 100% rename from src/test/resources/javadoc/source/ExistingJavadoc.java rename to tests/data/javadoc/source/ExistingJavadoc.java diff --git a/src/test/resources/javadoc/source/NoExistingJavadoc.java b/tests/data/javadoc/source/NoExistingJavadoc.java similarity index 100% rename from src/test/resources/javadoc/source/NoExistingJavadoc.java rename to tests/data/javadoc/source/NoExistingJavadoc.java diff --git a/src/test/resources/nested/expected/DefaultPkgClass.java b/tests/data/nested/expected/DefaultPkgClass.java similarity index 100% rename from src/test/resources/nested/expected/DefaultPkgClass.java rename to tests/data/nested/expected/DefaultPkgClass.java diff --git a/src/test/resources/nested/expected/pkg/Outer.java b/tests/data/nested/expected/pkg/Outer.java similarity index 100% rename from src/test/resources/nested/expected/pkg/Outer.java rename to tests/data/nested/expected/pkg/Outer.java diff --git a/src/test/resources/nested/expected/pkg/SamePkgClass.java b/tests/data/nested/expected/pkg/SamePkgClass.java similarity index 100% rename from src/test/resources/nested/expected/pkg/SamePkgClass.java rename to tests/data/nested/expected/pkg/SamePkgClass.java diff --git a/src/test/resources/nested/parchment.json b/tests/data/nested/parchment.json similarity index 100% rename from src/test/resources/nested/parchment.json rename to tests/data/nested/parchment.json diff --git a/src/test/resources/nested/source/DefaultPkgClass.java b/tests/data/nested/source/DefaultPkgClass.java similarity index 100% rename from src/test/resources/nested/source/DefaultPkgClass.java rename to tests/data/nested/source/DefaultPkgClass.java diff --git a/src/test/resources/nested/source/pkg/Outer.java b/tests/data/nested/source/pkg/Outer.java similarity index 100% rename from src/test/resources/nested/source/pkg/Outer.java rename to tests/data/nested/source/pkg/Outer.java diff --git a/src/test/resources/nested/source/pkg/SamePkgClass.java b/tests/data/nested/source/pkg/SamePkgClass.java similarity index 100% rename from src/test/resources/nested/source/pkg/SamePkgClass.java rename to tests/data/nested/source/pkg/SamePkgClass.java diff --git a/src/test/resources/param_indices/expected/TestClass.java b/tests/data/param_indices/expected/TestClass.java similarity index 100% rename from src/test/resources/param_indices/expected/TestClass.java rename to tests/data/param_indices/expected/TestClass.java diff --git a/src/test/resources/param_indices/expected/TestEnum.java b/tests/data/param_indices/expected/TestEnum.java similarity index 100% rename from src/test/resources/param_indices/expected/TestEnum.java rename to tests/data/param_indices/expected/TestEnum.java diff --git a/src/test/resources/param_indices/parchment.json b/tests/data/param_indices/parchment.json similarity index 100% rename from src/test/resources/param_indices/parchment.json rename to tests/data/param_indices/parchment.json diff --git a/src/test/resources/param_indices/source/TestClass.java b/tests/data/param_indices/source/TestClass.java similarity index 100% rename from src/test/resources/param_indices/source/TestClass.java rename to tests/data/param_indices/source/TestClass.java diff --git a/src/test/resources/param_indices/source/TestEnum.java b/tests/data/param_indices/source/TestEnum.java similarity index 100% rename from src/test/resources/param_indices/source/TestEnum.java rename to tests/data/param_indices/source/TestEnum.java diff --git a/tests/src/test/java/net/neoforged/jst/tests/MainTest.java b/tests/src/test/java/net/neoforged/jst/tests/MainTest.java new file mode 100644 index 0000000..2425f08 --- /dev/null +++ b/tests/src/test/java/net/neoforged/jst/tests/MainTest.java @@ -0,0 +1,168 @@ +package net.neoforged.jst.tests; + +import net.neoforged.jst.cli.Main; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test that references to external classes in method signatures are correctly resolved. + */ +public class MainTest { + @TempDir + private Path tempDir; + + @Test + void testInnerAndLocalClasses() throws Exception { + runTest("nested"); + } + + @Test + void testExternalReferences() throws Exception { + runTest("external_refs"); + } + + @Test + void testParamIndices() throws Exception { + runTest("param_indices"); + } + + @Test + void testJavadoc() throws Exception { + runTest("javadoc"); + } + + protected final void runTest(String testDir) throws Exception { + Path testDataRoot = Paths.get(getRequiredSystemProperty("jst.testDataDir")) + .resolve(testDir); + + var parchmentFile = testDataRoot.resolve("parchment.json"); + var sourceDir = testDataRoot.resolve("source"); + var expectedDir = testDataRoot.resolve("expected"); + + var inputFile = tempDir.resolve("input.jar"); + zipDirectory(sourceDir, inputFile, path -> { + return Files.isDirectory(path) || path.getFileName().toString().endsWith(".java"); + }); + + var outputFile = tempDir.resolve("output.jar"); + + // For testing external references, add JUnit-API, so it can be referenced + var junitJarPath = Paths.get(Test.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + var librariesFile = tempDir.resolve("libraries.txt"); + Files.write(librariesFile, List.of("-e=" + junitJarPath)); + + runExecutableJar( + "--libraries-list", + librariesFile.toString(), + "--enable-parchment", + "--parchment-mappings", + parchmentFile.toString(), + inputFile.toString(), + outputFile.toString() + ); + + try (var zipFile = new ZipFile(outputFile.toFile())) { + var it = zipFile.entries().asIterator(); + while (it.hasNext()) { + var entry = it.next(); + if (entry.isDirectory()) { + continue; + } + + var actualFile = normalizeLines(new String(zipFile.getInputStream(entry).readAllBytes(), StandardCharsets.UTF_8)); + var expectedFile = normalizeLines(Files.readString(expectedDir.resolve(entry.getName()), StandardCharsets.UTF_8)); + assertEquals(expectedFile, actualFile); + } + } + } + + private static void runExecutableJar(String... args) throws IOException, InterruptedException { + // Run in-process for easier debugging + if (Boolean.getBoolean("jst.debug")) { + Main.innerMain(args); + return; + } + + var javaExecutablePath = ProcessHandle.current() + .info() + .command() + .orElseThrow(); + + List commandLine = new ArrayList<>(); + commandLine.add(javaExecutablePath); + commandLine.add("-jar"); + commandLine.add(getRequiredSystemProperty("jst.executableJar")); + Collections.addAll(commandLine, args); + + var process = new ProcessBuilder(commandLine) + .redirectErrorStream(true) + .start(); + + process.getOutputStream().close(); // Close stdin to java + + byte[] output = process.getInputStream().readAllBytes(); + System.out.println(new String(output)); + + int exitCode = process.waitFor(); + assertEquals(0, exitCode); + } + + private static String getRequiredSystemProperty(String key) { + var value = System.getProperty(key); + if (value == null) { + throw new RuntimeException("Missing system property: " + key); + } + return value; + } + + private String normalizeLines(String s) { + return s.replaceAll("\r\n", "\n"); + } + + private static void zipDirectory(Path directory, Path destinationPath, Predicate filter) throws IOException { + try (var zOut = new ZipOutputStream(Files.newOutputStream(destinationPath)); + var files = Files.walk(directory)) { + files.filter(filter).forEach(path -> { + // Skip visiting the root directory itself + if (path.equals(directory)) { + return; + } + + var relativePath = directory.relativize(path).toString().replace('\\', '/'); + + try { + if (Files.isDirectory(path)) { + var entry = new ZipEntry(relativePath + "/"); + zOut.putNextEntry(entry); + zOut.closeEntry(); + } else { + var entry = new ZipEntry(relativePath); + entry.setLastModifiedTime(Files.getLastModifiedTime(path)); + + zOut.putNextEntry(entry); + Files.copy(path, zOut); + zOut.closeEntry(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } +}