diff --git a/README.md b/README.md index b156364da..08cc0c926 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ Enigma is distributed under the [LGPL-3.0](LICENSE). Enigma includes the following open-source libraries: -- A [modified version](https://github.com/FabricMC/procyon) of [Procyon](https://bitbucket.org/mstrobel/procyon) (Apache-2.0) +- [Vineflower](https://github.com/Vineflower/vineflower) (Apache-2.0) - A [modified version](https://github.com/FabricMC/cfr) of [CFR](https://github.com/leibnitz27/cfr) (MIT) +- A [modified version](https://github.com/FabricMC/procyon) of [Procyon](https://bitbucket.org/mstrobel/procyon) (Apache-2.0) - [Guava](https://github.com/google/guava) (Apache-2.0) - [SyntaxPane](https://github.com/Sciss/SyntaxPane) (Apache-2.0) - [FlatLaf](https://github.com/JFormDesigner/FlatLaf) (Apache-2.0) diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java index f9b5cbe12..41ee2547c 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java @@ -4,6 +4,7 @@ import cuchaz.enigma.source.Decompilers; public enum Decompiler { + VINEFLOWER("Vineflower", Decompilers.VINEFLOWER), CFR("CFR", Decompilers.CFR), PROCYON("Procyon", Decompilers.PROCYON), BYTECODE("Bytecode", Decompilers.BYTECODE); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/UiConfig.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/UiConfig.java index 32d6d762e..ecd17f28b 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/UiConfig.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/UiConfig.java @@ -105,7 +105,7 @@ public static void setLookAndFeel(LookAndFeel laf) { } public static Decompiler getDecompiler() { - return ui.data().section("Decompiler").setIfAbsentEnum(Decompiler::valueOf, "Current", Decompiler.CFR); + return ui.data().section("Decompiler").setIfAbsentEnum(Decompiler::valueOf, "Current", Decompiler.VINEFLOWER); } public static void setDecompiler(Decompiler d) { diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/legacy/Config.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/legacy/Config.java index 0e8f7da2f..2f7cf19b7 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/legacy/Config.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/legacy/Config.java @@ -78,7 +78,7 @@ public Color get() { public float scaleFactor = 1.0f; - public Decompiler decompiler = Decompiler.CFR; + public Decompiler decompiler = Decompiler.VINEFLOWER; public Config() { gson = new GsonBuilder().registerTypeAdapter(Integer.class, new IntSerializer()).registerTypeAdapter(Integer.class, new IntDeserializer()).registerTypeAdapter(Config.class, (InstanceCreator) type -> this).setPrettyPrinting().create(); diff --git a/enigma/build.gradle b/enigma/build.gradle index ffea473da..44f26808a 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -10,6 +10,7 @@ dependencies { implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0' implementation 'net.fabricmc:cfr:0.2.2' + implementation 'org.vineflower:vineflower:1.10.0' proGuard 'com.guardsquare:proguard-base:7.4.0-beta02' diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java index 48d673680..b3a7274f4 100644 --- a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java @@ -274,7 +274,17 @@ public Stream decompileStream(ProgressListener progress, Decompiler progress.init(classes.size(), I18n.translate("progress.classes.decompiling")); //create a common instance outside the loop as mappings shouldn't be changing while this is happening - Decompiler decompiler = decompilerService.create(compiled::get, new SourceSettings(false, false)); + Decompiler decompiler = decompilerService.create(new ClassProvider() { + @Override + public Collection getClassNames() { + return compiled.keySet(); + } + + @Override + public ClassNode get(String name) { + return compiled.get(name); + } + }, new SourceSettings(false, false)); AtomicInteger count = new AtomicInteger(); diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java index 45dac2c16..10bc436a2 100644 --- a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java +++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java @@ -55,8 +55,9 @@ private void registerEnumNamingService(EnigmaPluginContext ctx) { } private void registerDecompilerServices(EnigmaPluginContext ctx) { - ctx.registerService("enigma:procyon", DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON); + ctx.registerService("enigma:vineflower", DecompilerService.TYPE, ctx1 -> Decompilers.VINEFLOWER); ctx.registerService("enigma:cfr", DecompilerService.TYPE, ctx1 -> Decompilers.CFR); + ctx.registerService("enigma:procyon", DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON); ctx.registerService("enigma:bytecode", DecompilerService.TYPE, ctx1 -> Decompilers.BYTECODE); } diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java index eaba6df2f..28a93927e 100644 --- a/enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java @@ -1,5 +1,6 @@ package cuchaz.enigma.classprovider; +import java.util.Collection; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -21,6 +22,11 @@ public CachingClassProvider(ClassProvider classProvider) { this.classProvider = classProvider; } + @Override + public Collection getClassNames() { + return classProvider.getClassNames(); + } + @Override @Nullable public ClassNode get(String name) { diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java index 6eec0f362..069e0a865 100644 --- a/enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java @@ -1,10 +1,17 @@ package cuchaz.enigma.classprovider; +import java.util.Collection; + import javax.annotation.Nullable; import org.objectweb.asm.tree.ClassNode; public interface ClassProvider { + /** + * @return Internal names of all contained classes. May be empty if the provider is lazy. + */ + Collection getClassNames(); + /** * Gets the {@linkplain ClassNode} for a class. The class provider may return a cached result, * so it's important to not mutate it. diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java index 224093f8d..b035cee53 100644 --- a/enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; import javax.annotation.Nullable; @@ -12,6 +14,11 @@ * Provides classes by loading them from the classpath. */ public class ClasspathClassProvider implements ClassProvider { + @Override + public Collection getClassNames() { + return Collections.emptyList(); + } + @Nullable @Override public ClassNode get(String name) { diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java index 6856540f4..1b20b8f04 100644 --- a/enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java @@ -1,5 +1,9 @@ package cuchaz.enigma.classprovider; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + import javax.annotation.Nullable; import org.objectweb.asm.tree.ClassNode; @@ -15,6 +19,14 @@ public CombiningClassProvider(ClassProvider... classProviders) { this.classProviders = classProviders; } + @Override + public Collection getClassNames() { + return Arrays.stream(classProviders) + .map(ClassProvider::getClassNames) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + @Override @Nullable public ClassNode get(String name) { diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java index 900a0c8bd..5dec5bebc 100644 --- a/enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java @@ -41,6 +41,7 @@ private static ImmutableSet collectClassNames(FileSystem fileSystem) thr return classNames.build(); } + @Override public Set getClassNames() { return classNames; } diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java index 604bf499d..543ce48c5 100644 --- a/enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java @@ -1,5 +1,7 @@ package cuchaz.enigma.classprovider; +import java.util.Collection; + import javax.annotation.Nullable; import org.objectweb.asm.ClassVisitor; @@ -38,6 +40,11 @@ public ObfuscationFixClassProvider(ClassProvider classProvider, JarIndex jarInde this.jarIndex = jarIndex; } + @Override + public Collection getClassNames() { + return classProvider.getClassNames(); + } + @Override @Nullable public ClassNode get(String name) { diff --git a/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java b/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java index 0e3244db4..121903028 100644 --- a/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java +++ b/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java @@ -3,9 +3,11 @@ import cuchaz.enigma.source.bytecode.BytecodeDecompiler; import cuchaz.enigma.source.cfr.CfrDecompiler; import cuchaz.enigma.source.procyon.ProcyonDecompiler; +import cuchaz.enigma.source.vineflower.VineflowerDecompiler; public class Decompilers { - public static final DecompilerService PROCYON = ProcyonDecompiler::new; + public static final DecompilerService VINEFLOWER = VineflowerDecompiler::new; public static final DecompilerService CFR = CfrDecompiler::new; + public static final DecompilerService PROCYON = ProcyonDecompiler::new; public static final DecompilerService BYTECODE = BytecodeDecompiler::new; } diff --git a/enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDumper.java similarity index 94% rename from enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java rename to enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDumper.java index fb5c4a7ff..950f518be 100644 --- a/enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java +++ b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDumper.java @@ -40,7 +40,7 @@ import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; import cuchaz.enigma.translation.representation.entry.MethodEntry; -public class EnigmaDumper extends StringStreamDumper { +public class CfrDumper extends StringStreamDumper { private final StringBuilder sb; private final SourceSettings sourceSettings; private final SourceIndex index; @@ -51,11 +51,11 @@ public class EnigmaDumper extends StringStreamDumper { private boolean muteLine = false; private MethodEntry contextMethod = null; - public EnigmaDumper(StringBuilder sb, SourceSettings sourceSettings, TypeUsageInformation typeUsage, Options options, @Nullable EntryRemapper mapper) { + public CfrDumper(StringBuilder sb, SourceSettings sourceSettings, TypeUsageInformation typeUsage, Options options, @Nullable EntryRemapper mapper) { this(sb, sourceSettings, typeUsage, options, mapper, new SourceIndex(), new MovableDumperContext()); } - protected EnigmaDumper(StringBuilder sb, SourceSettings sourceSettings, TypeUsageInformation typeUsage, Options options, @Nullable EntryRemapper mapper, SourceIndex index, MovableDumperContext context) { + protected CfrDumper(StringBuilder sb, SourceSettings sourceSettings, TypeUsageInformation typeUsage, Options options, @Nullable EntryRemapper mapper, SourceIndex index, MovableDumperContext context) { super((m, e) -> { }, sb, typeUsage, options, IllegalIdentifierDump.Nop.getInstance(), context); this.sb = sb; @@ -149,21 +149,15 @@ public Dumper dumpClassDoc(JavaTypeInstance owner) { } EntryMapping mapping = mapper.getDeobfMapping(getFieldEntry(owner, field.getFieldName(), field.getField().getDescriptor())); + String javadoc = mapping.javadoc(); - if (mapping == null) { - continue; - } - - String javaDoc = mapping.javadoc(); - - if (javaDoc != null) { - recordComponentDocs.add(String.format("@param %s %s", mapping.targetName(), javaDoc)); + if (javadoc != null) { + recordComponentDocs.add(String.format("@param %s %s", mapping.targetName(), javadoc)); } } } EntryMapping mapping = mapper.getDeobfMapping(getClassEntry(owner)); - String javadoc = null; if (mapping != null) { @@ -399,7 +393,7 @@ private void dumpClass(TypeContext context, JavaTypeInstance type, boolean defin */ @Override public Dumper withTypeUsageInformation(TypeUsageInformation innerclassTypeUsageInformation) { - return new EnigmaDumper(this.sb, sourceSettings, innerclassTypeUsageInformation, options, mapper, index, dumperContext); + return new CfrDumper(this.sb, sourceSettings, innerclassTypeUsageInformation, options, mapper, index, dumperContext); } @Override diff --git a/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java index 70b43ac32..cf6c52fd2 100644 --- a/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java +++ b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrSource.java @@ -82,7 +82,7 @@ private void ensureDecompiled() { TypeUsageCollectingDumper typeUsageCollector = new TypeUsageCollectingDumper(options, tree); tree.analyseTop(state, typeUsageCollector); - EnigmaDumper dumper = new EnigmaDumper(new StringBuilder(), settings, typeUsageCollector.getRealTypeUsageInformation(), options, mapper); + CfrDumper dumper = new CfrDumper(new StringBuilder(), settings, typeUsageCollector.getRealTypeUsageInformation(), options, mapper); tree.dump(state.getObfuscationMapping().wrap(dumper)); index = dumper.getIndex(); } diff --git a/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerContextSource.java b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerContextSource.java new file mode 100644 index 000000000..e0f2b053e --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerContextSource.java @@ -0,0 +1,127 @@ +package cuchaz.enigma.source.vineflower; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jetbrains.java.decompiler.main.extern.IContextSource; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; +import org.objectweb.asm.tree.ClassNode; + +import cuchaz.enigma.classprovider.ClassProvider; +import cuchaz.enigma.utils.AsmUtil; + +class VineflowerContextSource implements IContextSource { + private final IContextSource classpathSource = new ClasspathSource(); + private final ClassProvider classProvider; + private final String className; + private Entries entries; + + VineflowerContextSource(ClassProvider classProvider, String className) { + this.classProvider = classProvider; + this.className = className; + } + + public IContextSource getClasspath() { + return classpathSource; + } + + @Override + public String getName() { + return "Enigma-provided context for class " + className; + } + + @Override + public Entries getEntries() { + computeEntriesIfNecessary(); + return entries; + } + + private void computeEntriesIfNecessary() { + if (entries != null) { + return; + } + + synchronized (this) { + if (entries != null) return; + + List classNames = new ArrayList<>(); + classNames.add(className); + + int dollarIndex = className.indexOf('$'); + String outermostClass = dollarIndex == -1 ? className : className.substring(0, className.indexOf('$')); + String outermostClassSuffixed = outermostClass + "$"; + + for (String currentClass : classProvider.getClassNames()) { + if (currentClass.startsWith(outermostClassSuffixed) && !currentClass.equals(className)) { + classNames.add(currentClass); + } + } + + List classes = classNames.stream() + .map(Entry::atBase) + .toList(); + + entries = new Entries(classes, Collections.emptyList(), Collections.emptyList()); + } + } + + @Override + public InputStream getInputStream(String resource) { + ClassNode node = classProvider.get(resource.substring(0, resource.lastIndexOf(".class"))); + + if (node == null) { + return null; + } + + return new ByteArrayInputStream(AsmUtil.nodeToBytes(node)); + } + + @Override + public IOutputSink createOutputSink(IResultSaver saver) { + return new IOutputSink() { + @Override + public void begin() { } + + @Override + public void acceptClass(String qualifiedName, String fileName, String content, int[] mapping) { + if (qualifiedName.equals(VineflowerContextSource.this.className)) { + saver.saveClassFile(null, qualifiedName, fileName, content, mapping); + } + } + + @Override + public void acceptDirectory(String directory) { } + + @Override + public void acceptOther(String path) { } + + @Override + public void close() { } + }; + } + + public class ClasspathSource implements IContextSource { + @Override + public String getName() { + return "Enigma-provided classpath context for " + VineflowerContextSource.this.className; + } + + @Override + public Entries getEntries() { + return Entries.EMPTY; + } + + @Override + public boolean isLazy() { + return true; + } + + @Override + public InputStream getInputStream(String resource) { + return VineflowerContextSource.this.getInputStream(resource); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerDecompiler.java b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerDecompiler.java new file mode 100644 index 000000000..56fd0b91e --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerDecompiler.java @@ -0,0 +1,24 @@ +package cuchaz.enigma.source.vineflower; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import cuchaz.enigma.classprovider.ClassProvider; +import cuchaz.enigma.source.Decompiler; +import cuchaz.enigma.source.Source; +import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.translation.mapping.EntryRemapper; + +public class VineflowerDecompiler implements Decompiler { + private final ClassProvider classProvider; + private final SourceSettings settings; + + public VineflowerDecompiler(ClassProvider classProvider, SourceSettings sourceSettings) { + this.settings = sourceSettings; + this.classProvider = classProvider; + } + + @Override + public Source getSource(String className, @Nullable EntryRemapper remapper) { + return new VineflowerSource(new VineflowerContextSource(classProvider, className), remapper, settings); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerJavadocProvider.java b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerJavadocProvider.java new file mode 100644 index 000000000..3757b02d4 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerJavadocProvider.java @@ -0,0 +1,139 @@ +package cuchaz.enigma.source.vineflower; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import net.fabricmc.fernflower.api.IFabricJavadocProvider; +import org.jetbrains.java.decompiler.struct.StructClass; +import org.jetbrains.java.decompiler.struct.StructField; +import org.jetbrains.java.decompiler.struct.StructMethod; +import org.jetbrains.java.decompiler.struct.StructRecordComponent; +import org.objectweb.asm.Opcodes; + +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +class VineflowerJavadocProvider implements IFabricJavadocProvider { + private final EntryRemapper remapper; + + VineflowerJavadocProvider(EntryRemapper remapper) { + this.remapper = remapper; + } + + @Override + public String getClassDoc(StructClass cls) { + if (remapper == null) return null; + + List recordComponentDocs = new LinkedList<>(); + + if (isRecord(cls)) { + for (StructRecordComponent component : cls.getRecordComponents()) { + EntryMapping mapping = remapper.getDeobfMapping(fieldEntryOf(cls, component)); + String javadoc = mapping.javadoc(); + + if (javadoc != null) { + recordComponentDocs.add(String.format("@param %s %s", mapping.targetName(), javadoc)); + } + } + } + + EntryMapping mapping = remapper.getDeobfMapping(classEntryOf(cls)); + StringBuilder builder = new StringBuilder(); + String javadoc = mapping.javadoc(); + + if (javadoc != null) { + builder.append(javadoc); + } + + if (!recordComponentDocs.isEmpty()) { + if (javadoc != null) { + builder.append('\n'); + } + + for (String recordComponentDoc : recordComponentDocs) { + builder.append('\n').append(recordComponentDoc); + } + } + + javadoc = builder.toString(); + + return javadoc.isBlank() ? null : javadoc.trim(); + } + + @Override + public String getFieldDoc(StructClass cls, StructField fld) { + boolean isRecordComponent = isRecord(cls) && !fld.hasModifier(Opcodes.ACC_STATIC); + + if (remapper == null || isRecordComponent) { + return null; + } + + EntryMapping mapping = remapper.getDeobfMapping(fieldEntryOf(cls, fld)); + String javadoc = mapping.javadoc(); + + return javadoc == null || javadoc.isBlank() ? null : javadoc.trim(); + } + + @Override + public String getMethodDoc(StructClass cls, StructMethod mth) { + if (remapper == null) return null; + + MethodEntry entry = methodEntryOf(cls, mth); + EntryMapping mapping = remapper.getDeobfMapping(entry); + StringBuilder builder = new StringBuilder(); + String javadoc = mapping.javadoc(); + + if (javadoc != null) { + builder.append(javadoc); + } + + Collection> children = remapper.getObfChildren(entry); + boolean addedLf = false; + + if (children != null && !children.isEmpty()) { + for (Entry each : children) { + if (each instanceof LocalVariableEntry) { + mapping = remapper.getDeobfMapping(each); + javadoc = mapping.javadoc(); + + if (javadoc != null) { + if (!addedLf) { + addedLf = true; + builder.append('\n'); + } + + builder.append(String.format("\n@param %s %s", mapping.targetName(), javadoc)); + } + } + } + } + + javadoc = builder.toString(); + + return javadoc.isBlank() ? null : javadoc.trim(); + } + + private boolean isRecord(StructClass cls) { + if (cls.superClass == null) return false; + + return cls.superClass.getString().equals("java/lang/Record"); + } + + private ClassEntry classEntryOf(StructClass cls) { + return ClassEntry.parse(cls.qualifiedName); + } + + private FieldEntry fieldEntryOf(StructClass cls, StructField fld) { + return FieldEntry.parse(cls.qualifiedName, fld.getName(), fld.getDescriptor()); + } + + private MethodEntry methodEntryOf(StructClass cls, StructMethod mth) { + return MethodEntry.parse(cls.qualifiedName, mth.getName(), mth.getDescriptor()); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerSource.java b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerSource.java new file mode 100644 index 000000000..27b6edcc8 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerSource.java @@ -0,0 +1,119 @@ +package cuchaz.enigma.source.vineflower; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.jar.Manifest; + +import net.fabricmc.fernflower.api.IFabricJavadocProvider; +import org.jetbrains.java.decompiler.main.decompiler.BaseDecompiler; +import org.jetbrains.java.decompiler.main.decompiler.PrintStreamLogger; +import org.jetbrains.java.decompiler.main.extern.IContextSource; +import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; +import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; +import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; + +import cuchaz.enigma.source.Source; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.translation.mapping.EntryRemapper; + +class VineflowerSource implements Source { + private final IContextSource contextSource; + private final IContextSource librarySource; + private final SourceSettings settings; + private EntryRemapper remapper; + private SourceIndex index; + + VineflowerSource(VineflowerContextSource contextSource, EntryRemapper remapper, SourceSettings settings) { + this.contextSource = contextSource; + this.librarySource = contextSource.getClasspath(); + this.remapper = remapper; + this.settings = settings; + } + + @Override + public String asString() { + ensureDecompiled(); + return index.getSource(); + } + + @Override + public Source withJavadocs(EntryRemapper remapper) { + this.remapper = remapper; + this.index = null; + return this; + } + + @Override + public SourceIndex index() { + ensureDecompiled(); + return index; + } + + private void ensureDecompiled() { + if (index != null) { + return; + } + + Map preferences = new HashMap<>(IFernflowerPreferences.DEFAULTS); + preferences.put(IFernflowerPreferences.INDENT_STRING, "\t"); + preferences.put(IFernflowerPreferences.LOG_LEVEL, IFernflowerLogger.Severity.WARN.name()); + preferences.put(IFernflowerPreferences.THREADS, String.valueOf(Math.max(1, Runtime.getRuntime().availableProcessors() - 2))); + preferences.put(IFabricJavadocProvider.PROPERTY_NAME, new VineflowerJavadocProvider(remapper)); + + if (settings.removeImports) { + preferences.put(IFernflowerPreferences.REMOVE_IMPORTS, "1"); + } + + index = new SourceIndex(); + IResultSaver saver = new ResultSaver(index); + IFernflowerLogger logger = new PrintStreamLogger(System.out); + BaseDecompiler decompiler = new BaseDecompiler(saver, preferences, logger); + + AtomicReference tokenCollector = new AtomicReference<>(); + TextTokenVisitor.addVisitor(next -> { + tokenCollector.set(new VineflowerTextTokenCollector(next)); + return tokenCollector.get(); + }); + + decompiler.addSource(contextSource); + + if (librarySource != null) { + decompiler.addLibrary(librarySource); + } + + decompiler.decompileContext(); + tokenCollector.get().accept(index); + } + + private class ResultSaver implements IResultSaver { + private final SourceIndex index; + + private ResultSaver(SourceIndex index) { + this.index = index; + } + + @Override + public void saveFolder(String path) { } + @Override + public void copyFile(String source, String path, String entryName) { } + + @Override + public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { + index.setSource(content); + } + + @Override + public void createArchive(String path, String archiveName, Manifest manifest) { } + @Override + public void saveDirEntry(String path, String archiveName, String entryName) { } + @Override + public void copyEntry(String source, String path, String archiveName, String entry) { } + @Override + public void closeArchive(String path, String archiveName) { } + @Override + public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) { } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerTextTokenCollector.java b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerTextTokenCollector.java new file mode 100644 index 000000000..3d4571218 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerTextTokenCollector.java @@ -0,0 +1,151 @@ +package cuchaz.enigma.source.vineflower; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; +import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor; +import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor; +import org.jetbrains.java.decompiler.util.Pair; +import org.jetbrains.java.decompiler.util.token.TextRange; + +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +class VineflowerTextTokenCollector extends TextTokenVisitor { + private final Map> declarations = new HashMap<>(); + private final Map, Entry>> references = new HashMap<>(); + private final Set tokens = new LinkedHashSet<>(); + private String content; + private MethodEntry currentMethod; + + VineflowerTextTokenCollector(TextTokenVisitor next) { + super(next); + } + + @Override + public void start(String content) { + this.content = content; + } + + @Override + public void visitClass(TextRange range, boolean declaration, String name) { + super.visitClass(range, declaration, name); + Token token = getToken(range); + + if (declaration) { + addDeclaration(token, classEntryOf(name)); + } else { + addReference(token, classEntryOf(name), currentMethod); + } + } + + @Override + public void visitField(TextRange range, boolean declaration, String className, String name, FieldDescriptor descriptor) { + super.visitField(range, declaration, className, name, descriptor); + Token token = getToken(range); + + if (declaration) { + addDeclaration(token, fieldEntryOf(className, name, descriptor)); + } else { + addReference(token, fieldEntryOf(className, name, descriptor), currentMethod); + } + } + + @Override + public void visitMethod(TextRange range, boolean declaration, String className, String name, MethodDescriptor descriptor) { + super.visitMethod(range, declaration, className, name, descriptor); + Token token = getToken(range); + + if (token.text.equals("new")) { + return; + } + + MethodEntry entry = methodEntryOf(className, name, descriptor); + + if (declaration) { + addDeclaration(token, entry); + currentMethod = entry; + } else { + addReference(token, entry, currentMethod); + } + } + + @Override + public void visitParameter(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int lvIndex, String name) { + super.visitParameter(range, declaration, className, methodName, methodDescriptor, lvIndex, name); + Token token = getToken(range); + MethodEntry parent = methodEntryOf(className, methodName, methodDescriptor); + + if (declaration) { + addDeclaration(token, argEntryOf(parent, lvIndex, name)); + } else { + addReference(token, argEntryOf(parent, lvIndex, name), currentMethod); + } + } + + @Override + public void visitLocal(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int lvIndex, String name) { + super.visitLocal(range, declaration, className, methodName, methodDescriptor, lvIndex, name); + Token token = getToken(range); + MethodEntry parent = methodEntryOf(className, methodName, methodDescriptor); + + if (declaration) { + addDeclaration(token, varEntryOf(parent, lvIndex, name)); + } else { + addReference(token, varEntryOf(parent, lvIndex, name), currentMethod); + } + } + + private ClassEntry classEntryOf(String name) { + return ClassEntry.parse(name); + } + + private FieldEntry fieldEntryOf(String className, String name, FieldDescriptor descriptor) { + return FieldEntry.parse(className, name, descriptor.descriptorString); + } + + private MethodEntry methodEntryOf(String className, String name, MethodDescriptor descriptor) { + return MethodEntry.parse(className, name, descriptor.toString()); + } + + private LocalVariableEntry argEntryOf(MethodEntry className, int lvIndex, String name) { + return new LocalVariableEntry(className, lvIndex, name, true, null); + } + + private LocalVariableEntry varEntryOf(MethodEntry className, int lvIndex, String name) { + return new LocalVariableEntry(className, lvIndex, name, false, null); + } + + private Token getToken(TextRange range) { + return new Token(range.start, range.start + range.length, content.substring(range.start, range.start + range.length)); + } + + private void addDeclaration(Token token, Entry entry) { + declarations.put(token, entry); + tokens.add(token); + } + + private void addReference(Token token, Entry entry, Entry context) { + references.put(token, Pair.of(entry, context)); + tokens.add(token); + } + + public void accept(SourceIndex index) { + for (Token token : tokens) { + if (declarations.get(token) != null) { + index.addDeclaration(token, declarations.get(token)); + } else { + Pair, Entry> reference = references.get(token); + index.addReference(token, reference.a, reference.b); + } + } + } +}