From 31b6e9e188fb2abbab040ee07f8a8c14cec2571d Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Sat, 6 Apr 2024 13:05:49 +0200 Subject: Add Vineflower decompiler (#541) * Add Vineflower integration * Fix some confusing names * Rename method to reflect visitor pattern naming schemes * Remove dead null check--- enigma/build.gradle | 1 + .../src/main/java/cuchaz/enigma/EnigmaProject.java | 12 +- .../java/cuchaz/enigma/analysis/BuiltinPlugin.java | 3 +- .../enigma/classprovider/CachingClassProvider.java | 6 + .../cuchaz/enigma/classprovider/ClassProvider.java | 7 + .../classprovider/ClasspathClassProvider.java | 7 + .../classprovider/CombiningClassProvider.java | 12 + .../enigma/classprovider/JarClassProvider.java | 1 + .../classprovider/ObfuscationFixClassProvider.java | 7 + .../java/cuchaz/enigma/source/Decompilers.java | 4 +- .../java/cuchaz/enigma/source/cfr/CfrDumper.java | 425 ++++++++++++++++++++ .../java/cuchaz/enigma/source/cfr/CfrSource.java | 2 +- .../cuchaz/enigma/source/cfr/EnigmaDumper.java | 431 --------------------- .../source/vineflower/VineflowerContextSource.java | 127 ++++++ .../source/vineflower/VineflowerDecompiler.java | 24 ++ .../vineflower/VineflowerJavadocProvider.java | 139 +++++++ .../enigma/source/vineflower/VineflowerSource.java | 119 ++++++ .../vineflower/VineflowerTextTokenCollector.java | 151 ++++++++ 18 files changed, 1043 insertions(+), 435 deletions(-) create mode 100644 enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDumper.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerContextSource.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerDecompiler.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerJavadocProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerSource.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/vineflower/VineflowerTextTokenCollector.java (limited to 'enigma') diff --git a/enigma/build.gradle b/enigma/build.gradle index ffea473..44f2680 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 48d6736..b3a7274 100644 --- a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java @@ -274,7 +274,17 @@ public class EnigmaProject { 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 45dac2c..10bc436 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 @@ public final class BuiltinPlugin implements EnigmaPlugin { } 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 eaba6df..28a9392 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 class CachingClassProvider implements 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 6eec0f3..069e0a8 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 224093f..b035cee 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 @@ package cuchaz.enigma.classprovider; import java.io.IOException; import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; import javax.annotation.Nullable; @@ -12,6 +14,11 @@ import org.objectweb.asm.tree.ClassNode; * 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 6856540..1b20b8f 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 class CombiningClassProvider implements ClassProvider { 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 900a0c8..5dec5be 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 @@ public class JarClassProvider implements AutoCloseable, ClassProvider { 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 604bf49..543ce48 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 class ObfuscationFixClassProvider implements ClassProvider { 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 0e3244d..1219030 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 @@ package cuchaz.enigma.source; 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/CfrDumper.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDumper.java new file mode 100644 index 0000000..950f518 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDumper.java @@ -0,0 +1,425 @@ +package cuchaz.enigma.source.cfr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.benf.cfr.reader.bytecode.analysis.loc.HasByteCodeLoc; +import org.benf.cfr.reader.bytecode.analysis.types.JavaRefTypeInstance; +import org.benf.cfr.reader.bytecode.analysis.types.JavaTypeInstance; +import org.benf.cfr.reader.bytecode.analysis.types.MethodPrototype; +import org.benf.cfr.reader.bytecode.analysis.variables.NamedVariable; +import org.benf.cfr.reader.entities.AccessFlag; +import org.benf.cfr.reader.entities.ClassFile; +import org.benf.cfr.reader.entities.ClassFileField; +import org.benf.cfr.reader.entities.Field; +import org.benf.cfr.reader.entities.Method; +import org.benf.cfr.reader.state.TypeUsageInformation; +import org.benf.cfr.reader.util.getopt.Options; +import org.benf.cfr.reader.util.output.Dumper; +import org.benf.cfr.reader.util.output.IllegalIdentifierDump; +import org.benf.cfr.reader.util.output.MovableDumperContext; +import org.benf.cfr.reader.util.output.StringStreamDumper; +import org.benf.cfr.reader.util.output.TypeContext; +import org.checkerframework.checker.nullness.qual.Nullable; + +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +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; + +public class CfrDumper extends StringStreamDumper { + private final StringBuilder sb; + private final SourceSettings sourceSettings; + private final SourceIndex index; + private final @Nullable EntryRemapper mapper; + private final Map> refs = new HashMap<>(); + private final TypeUsageInformation typeUsage; + private final MovableDumperContext dumperContext; + private boolean muteLine = false; + private MethodEntry contextMethod = null; + + public CfrDumper(StringBuilder sb, SourceSettings sourceSettings, TypeUsageInformation typeUsage, Options options, @Nullable EntryRemapper mapper) { + this(sb, sourceSettings, typeUsage, options, mapper, new SourceIndex(), new MovableDumperContext()); + } + + 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; + this.sourceSettings = sourceSettings; + this.typeUsage = typeUsage; + this.mapper = mapper; + this.dumperContext = context; + this.index = index; + } + + private MethodEntry getMethodEntry(MethodPrototype method) { + if (method == null || method.getOwner() == null) { + return null; + } + + MethodDescriptor desc = new MethodDescriptor(method.getOriginalDescriptor()); + + return new MethodEntry(getClassEntry(method.getOwner()), method.getName(), desc); + } + + private LocalVariableEntry getParameterEntry(MethodPrototype method, int parameterIndex, String name) { + MethodEntry owner = getMethodEntry(method); + + // params may be not computed if cfr creates a lambda expression fallback, e.g. in PointOfInterestSet + if (owner == null || !method.parametersComputed()) { + return null; + } + + int variableIndex = method.getParameterLValues().get(parameterIndex).localVariable.getIdx(); + + return new LocalVariableEntry(owner, variableIndex, name, true, null); + } + + private FieldEntry getFieldEntry(JavaTypeInstance owner, String name, String desc) { + return new FieldEntry(getClassEntry(owner), name, new TypeDescriptor(desc)); + } + + private ClassEntry getClassEntry(JavaTypeInstance type) { + return new ClassEntry(type.getRawName().replace('.', '/')); + } + + @Override + public Dumper packageName(JavaRefTypeInstance t) { + if (sourceSettings.removeImports) { + return this; + } + + return super.packageName(t); + } + + @Override + public Dumper keyword(String s) { + if (sourceSettings.removeImports && s.startsWith("import")) { + muteLine = true; + return this; + } + + return super.keyword(s); + } + + @Override + public Dumper endCodeln() { + if (muteLine) { + muteLine = false; + return this; + } + + return super.endCodeln(); + } + + @Override + public Dumper print(String s) { + if (muteLine) { + return this; + } + + return super.print(s); + } + + @Override + public Dumper dumpClassDoc(JavaTypeInstance owner) { + if (mapper != null) { + List recordComponentDocs = new LinkedList<>(); + + if (isRecord(owner)) { + ClassFile classFile = ((JavaRefTypeInstance) owner).getClassFile(); + + for (ClassFileField field : classFile.getFields()) { + if (field.getField().testAccessFlag(AccessFlag.ACC_STATIC)) { + continue; + } + + EntryMapping mapping = mapper.getDeobfMapping(getFieldEntry(owner, field.getFieldName(), field.getField().getDescriptor())); + String javadoc = mapping.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) { + javadoc = mapping.javadoc(); + } + + if (javadoc != null || !recordComponentDocs.isEmpty()) { + print("/**").newln(); + + if (javadoc != null) { + for (String line : javadoc.split("\\R")) { + print(" * ").print(line).newln(); + } + + if (!recordComponentDocs.isEmpty()) { + print(" * ").newln(); + } + } + + for (String componentDoc : recordComponentDocs) { + print(" * ").print(componentDoc).newln(); + } + + print(" */").newln(); + } + } + + return this; + } + + @Override + public Dumper dumpMethodDoc(MethodPrototype method) { + if (mapper != null) { + List lines = new ArrayList<>(); + MethodEntry methodEntry = getMethodEntry(method); + EntryMapping mapping = mapper.getDeobfMapping(methodEntry); + + if (mapping != null) { + String javadoc = mapping.javadoc(); + + if (javadoc != null) { + lines.addAll(Arrays.asList(javadoc.split("\\R"))); + } + } + + Collection> children = mapper.getObfChildren(methodEntry); + + if (children != null && !children.isEmpty()) { + for (Entry each : children) { + if (each instanceof LocalVariableEntry) { + EntryMapping paramMapping = mapper.getDeobfMapping(each); + + if (paramMapping != null) { + String javadoc = paramMapping.javadoc(); + + if (javadoc != null) { + lines.addAll(Arrays.asList(("@param " + paramMapping.targetName() + " " + javadoc).split("\\R"))); + } + } + } + } + } + + if (!lines.isEmpty()) { + print("/**").newln(); + + for (String line : lines) { + print(" * ").print(line).newln(); + } + + print(" */").newln(); + } + } + + return this; + } + + @Override + public Dumper dumpFieldDoc(Field field, JavaTypeInstance owner) { + boolean recordComponent = isRecord(owner) && !field.testAccessFlag(AccessFlag.ACC_STATIC); + + if (mapper != null && !recordComponent) { + EntryMapping mapping = mapper.getDeobfMapping(getFieldEntry(owner, field.getFieldName(), field.getDescriptor())); + + if (mapping != null) { + String javadoc = mapping.javadoc(); + + if (javadoc != null) { + print("/**").newln(); + + for (String line : javadoc.split("\\R")) { + print(" * ").print(line).newln(); + } + + print(" */").newln(); + } + } + } + + return this; + } + + @Override + public Dumper methodName(String name, MethodPrototype method, boolean special, boolean defines) { + Entry entry = getMethodEntry(method); + super.methodName(name, method, special, defines); + int now = sb.length(); + Token token = new Token(now - name.length(), now, name); + + // Skip constructor references + if (entry != null && !name.equals("new")) { + if (defines) { + index.addDeclaration(token, entry); // override as cfr reuses local vars + } else { + index.addReference(token, entry, contextMethod); + } + } + + return this; + } + + @Override + public Dumper parameterName(String name, Object ref, MethodPrototype method, int index, boolean defines) { + super.parameterName(name, ref, method, index, defines); + int now = sb.length(); + Token token = new Token(now - name.length(), now, name); + Entry entry; + + if (defines) { + refs.put(ref, entry = getParameterEntry(method, index, name)); + } else { + entry = refs.get(ref); + } + + if (entry != null) { + if (defines) { + this.index.addDeclaration(token, entry); + } else { + this.index.addReference(token, entry, contextMethod); + } + } + + return this; + } + + @Override + public Dumper variableName(String name, NamedVariable variable, boolean defines) { + // todo catch var declarations in the future + return super.variableName(name, variable, defines); + } + + @Override + public Dumper identifier(String name, Object ref, boolean defines) { + super.identifier(name, ref, defines); + Entry entry; + + if (defines) { + refs.remove(ref); + return this; + } + + if ((entry = refs.get(ref)) == null) { + return this; + } + + int now = sb.length(); + Token token = new Token(now - name.length(), now, name); + index.addReference(token, entry, contextMethod); + return this; + } + + @Override + public Dumper fieldName(String name, String descriptor, JavaTypeInstance owner, boolean hiddenDeclaration, boolean isStatic, boolean defines) { + super.fieldName(name, descriptor, owner, hiddenDeclaration, isStatic, defines); + int now = sb.length(); + Token token = new Token(now - name.length(), now, name); + + if (descriptor != null) { + Entry entry = getFieldEntry(owner, name, descriptor); + + if (defines) { + index.addDeclaration(token, entry); + } else { + index.addReference(token, entry, contextMethod); + } + } + + return this; + } + + @Override + public Dumper dump(JavaTypeInstance type) { + dumpClass(TypeContext.None, type, false); + return this; + } + + @Override + public Dumper dump(JavaTypeInstance type, boolean defines) { + dumpClass(TypeContext.None, type, defines); + return this; + } + + @Override + public Dumper dump(JavaTypeInstance type, TypeContext context) { + dumpClass(context, type, false); + return this; + } + + private void dumpClass(TypeContext context, JavaTypeInstance type, boolean defines) { + if (type instanceof JavaRefTypeInstance) { + type.dumpInto(this, typeUsage, context); + String name = typeUsage.getName(type, context); // the actually used name, dump will indent + int now = sb.length(); + Token token = new Token(now - name.length(), now, name); + + if (defines) { + index.addDeclaration(token, getClassEntry(type)); + } else { + index.addReference(token, getClassEntry(type), contextMethod); + } + + return; + } + + type.dumpInto(this, typeUsage, context); + } + + /** + * {@inheritDoc} + * + *

Otherwise the type usage override dumper will not go through the type instance dump + * we have here. + */ + @Override + public Dumper withTypeUsageInformation(TypeUsageInformation innerclassTypeUsageInformation) { + return new CfrDumper(this.sb, sourceSettings, innerclassTypeUsageInformation, options, mapper, index, dumperContext); + } + + @Override + public void informBytecodeLoc(HasByteCodeLoc loc) { + Collection methods = loc.getLoc().getMethods(); + + if (!methods.isEmpty()) { + this.contextMethod = getMethodEntry(methods.iterator().next().getMethodPrototype()); + } + } + + public SourceIndex getIndex() { + index.setSource(getString()); + return index; + } + + public String getString() { + return sb.toString(); + } + + private boolean isRecord(JavaTypeInstance javaTypeInstance) { + if (javaTypeInstance instanceof JavaRefTypeInstance) { + ClassFile classFile = ((JavaRefTypeInstance) javaTypeInstance).getClassFile(); + return classFile.getClassSignature().getSuperClass().getRawName().equals("java.lang.Record"); + } + + return false; + } +} 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 70b43ac..cf6c52f 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 @@ public class CfrSource implements Source { 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/cfr/EnigmaDumper.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java deleted file mode 100644 index fb5c4a7..0000000 --- a/enigma/src/main/java/cuchaz/enigma/source/cfr/EnigmaDumper.java +++ /dev/null @@ -1,431 +0,0 @@ -package cuchaz.enigma.source.cfr; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.benf.cfr.reader.bytecode.analysis.loc.HasByteCodeLoc; -import org.benf.cfr.reader.bytecode.analysis.types.JavaRefTypeInstance; -import org.benf.cfr.reader.bytecode.analysis.types.JavaTypeInstance; -import org.benf.cfr.reader.bytecode.analysis.types.MethodPrototype; -import org.benf.cfr.reader.bytecode.analysis.variables.NamedVariable; -import org.benf.cfr.reader.entities.AccessFlag; -import org.benf.cfr.reader.entities.ClassFile; -import org.benf.cfr.reader.entities.ClassFileField; -import org.benf.cfr.reader.entities.Field; -import org.benf.cfr.reader.entities.Method; -import org.benf.cfr.reader.state.TypeUsageInformation; -import org.benf.cfr.reader.util.getopt.Options; -import org.benf.cfr.reader.util.output.Dumper; -import org.benf.cfr.reader.util.output.IllegalIdentifierDump; -import org.benf.cfr.reader.util.output.MovableDumperContext; -import org.benf.cfr.reader.util.output.StringStreamDumper; -import org.benf.cfr.reader.util.output.TypeContext; -import org.checkerframework.checker.nullness.qual.Nullable; - -import cuchaz.enigma.source.SourceIndex; -import cuchaz.enigma.source.SourceSettings; -import cuchaz.enigma.source.Token; -import cuchaz.enigma.translation.mapping.EntryMapping; -import cuchaz.enigma.translation.mapping.EntryRemapper; -import cuchaz.enigma.translation.representation.MethodDescriptor; -import cuchaz.enigma.translation.representation.TypeDescriptor; -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; - -public class EnigmaDumper extends StringStreamDumper { - private final StringBuilder sb; - private final SourceSettings sourceSettings; - private final SourceIndex index; - private final @Nullable EntryRemapper mapper; - private final Map> refs = new HashMap<>(); - private final TypeUsageInformation typeUsage; - private final MovableDumperContext dumperContext; - private boolean muteLine = false; - private MethodEntry contextMethod = null; - - public EnigmaDumper(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) { - super((m, e) -> { - }, sb, typeUsage, options, IllegalIdentifierDump.Nop.getInstance(), context); - this.sb = sb; - this.sourceSettings = sourceSettings; - this.typeUsage = typeUsage; - this.mapper = mapper; - this.dumperContext = context; - this.index = index; - } - - private MethodEntry getMethodEntry(MethodPrototype method) { - if (method == null || method.getOwner() == null) { - return null; - } - - MethodDescriptor desc = new MethodDescriptor(method.getOriginalDescriptor()); - - return new MethodEntry(getClassEntry(method.getOwner()), method.getName(), desc); - } - - private LocalVariableEntry getParameterEntry(MethodPrototype method, int parameterIndex, String name) { - MethodEntry owner = getMethodEntry(method); - - // params may be not computed if cfr creates a lambda expression fallback, e.g. in PointOfInterestSet - if (owner == null || !method.parametersComputed()) { - return null; - } - - int variableIndex = method.getParameterLValues().get(parameterIndex).localVariable.getIdx(); - - return new LocalVariableEntry(owner, variableIndex, name, true, null); - } - - private FieldEntry getFieldEntry(JavaTypeInstance owner, String name, String desc) { - return new FieldEntry(getClassEntry(owner), name, new TypeDescriptor(desc)); - } - - private ClassEntry getClassEntry(JavaTypeInstance type) { - return new ClassEntry(type.getRawName().replace('.', '/')); - } - - @Override - public Dumper packageName(JavaRefTypeInstance t) { - if (sourceSettings.removeImports) { - return this; - } - - return super.packageName(t); - } - - @Override - public Dumper keyword(String s) { - if (sourceSettings.removeImports && s.startsWith("import")) { - muteLine = true; - return this; - } - - return super.keyword(s); - } - - @Override - public Dumper endCodeln() { - if (muteLine) { - muteLine = false; - return this; - } - - return super.endCodeln(); - } - - @Override - public Dumper print(String s) { - if (muteLine) { - return this; - } - - return super.print(s); - } - - @Override - public Dumper dumpClassDoc(JavaTypeInstance owner) { - if (mapper != null) { - List recordComponentDocs = new LinkedList<>(); - - if (isRecord(owner)) { - ClassFile classFile = ((JavaRefTypeInstance) owner).getClassFile(); - - for (ClassFileField field : classFile.getFields()) { - if (field.getField().testAccessFlag(AccessFlag.ACC_STATIC)) { - continue; - } - - EntryMapping mapping = mapper.getDeobfMapping(getFieldEntry(owner, field.getFieldName(), field.getField().getDescriptor())); - - if (mapping == null) { - continue; - } - - String javaDoc = mapping.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) { - javadoc = mapping.javadoc(); - } - - if (javadoc != null || !recordComponentDocs.isEmpty()) { - print("/**").newln(); - - if (javadoc != null) { - for (String line : javadoc.split("\\R")) { - print(" * ").print(line).newln(); - } - - if (!recordComponentDocs.isEmpty()) { - print(" * ").newln(); - } - } - - for (String componentDoc : recordComponentDocs) { - print(" * ").print(componentDoc).newln(); - } - - print(" */").newln(); - } - } - - return this; - } - - @Override - public Dumper dumpMethodDoc(MethodPrototype method) { - if (mapper != null) { - List lines = new ArrayList<>(); - MethodEntry methodEntry = getMethodEntry(method); - EntryMapping mapping = mapper.getDeobfMapping(methodEntry); - - if (mapping != null) { - String javadoc = mapping.javadoc(); - - if (javadoc != null) { - lines.addAll(Arrays.asList(javadoc.split("\\R"))); - } - } - - Collection> children = mapper.getObfChildren(methodEntry); - - if (children != null && !children.isEmpty()) { - for (Entry each : children) { - if (each instanceof LocalVariableEntry) { - EntryMapping paramMapping = mapper.getDeobfMapping(each); - - if (paramMapping != null) { - String javadoc = paramMapping.javadoc(); - - if (javadoc != null) { - lines.addAll(Arrays.asList(("@param " + paramMapping.targetName() + " " + javadoc).split("\\R"))); - } - } - } - } - } - - if (!lines.isEmpty()) { - print("/**").newln(); - - for (String line : lines) { - print(" * ").print(line).newln(); - } - - print(" */").newln(); - } - } - - return this; - } - - @Override - public Dumper dumpFieldDoc(Field field, JavaTypeInstance owner) { - boolean recordComponent = isRecord(owner) && !field.testAccessFlag(AccessFlag.ACC_STATIC); - - if (mapper != null && !recordComponent) { - EntryMapping mapping = mapper.getDeobfMapping(getFieldEntry(owner, field.getFieldName(), field.getDescriptor())); - - if (mapping != null) { - String javadoc = mapping.javadoc(); - - if (javadoc != null) { - print("/**").newln(); - - for (String line : javadoc.split("\\R")) { - print(" * ").print(line).newln(); - } - - print(" */").newln(); - } - } - } - - return this; - } - - @Override - public Dumper methodName(String name, MethodPrototype method, boolean special, boolean defines) { - Entry entry = getMethodEntry(method); - super.methodName(name, method, special, defines); - int now = sb.length(); - Token token = new Token(now - name.length(), now, name); - - // Skip constructor references - if (entry != null && !name.equals("new")) { - if (defines) { - index.addDeclaration(token, entry); // override as cfr reuses local vars - } else { - index.addReference(token, entry, contextMethod); - } - } - - return this; - } - - @Override - public Dumper parameterName(String name, Object ref, MethodPrototype method, int index, boolean defines) { - super.parameterName(name, ref, method, index, defines); - int now = sb.length(); - Token token = new Token(now - name.length(), now, name); - Entry entry; - - if (defines) { - refs.put(ref, entry = getParameterEntry(method, index, name)); - } else { - entry = refs.get(ref); - } - - if (entry != null) { - if (defines) { - this.index.addDeclaration(token, entry); - } else { - this.index.addReference(token, entry, contextMethod); - } - } - - return this; - } - - @Override - public Dumper variableName(String name, NamedVariable variable, boolean defines) { - // todo catch var declarations in the future - return super.variableName(name, variable, defines); - } - - @Override - public Dumper identifier(String name, Object ref, boolean defines) { - super.identifier(name, ref, defines); - Entry entry; - - if (defines) { - refs.remove(ref); - return this; - } - - if ((entry = refs.get(ref)) == null) { - return this; - } - - int now = sb.length(); - Token token = new Token(now - name.length(), now, name); - index.addReference(token, entry, contextMethod); - return this; - } - - @Override - public Dumper fieldName(String name, String descriptor, JavaTypeInstance owner, boolean hiddenDeclaration, boolean isStatic, boolean defines) { - super.fieldName(name, descriptor, owner, hiddenDeclaration, isStatic, defines); - int now = sb.length(); - Token token = new Token(now - name.length(), now, name); - - if (descriptor != null) { - Entry entry = getFieldEntry(owner, name, descriptor); - - if (defines) { - index.addDeclaration(token, entry); - } else { - index.addReference(token, entry, contextMethod); - } - } - - return this; - } - - @Override - public Dumper dump(JavaTypeInstance type) { - dumpClass(TypeContext.None, type, false); - return this; - } - - @Override - public Dumper dump(JavaTypeInstance type, boolean defines) { - dumpClass(TypeContext.None, type, defines); - return this; - } - - @Override - public Dumper dump(JavaTypeInstance type, TypeContext context) { - dumpClass(context, type, false); - return this; - } - - private void dumpClass(TypeContext context, JavaTypeInstance type, boolean defines) { - if (type instanceof JavaRefTypeInstance) { - type.dumpInto(this, typeUsage, context); - String name = typeUsage.getName(type, context); // the actually used name, dump will indent - int now = sb.length(); - Token token = new Token(now - name.length(), now, name); - - if (defines) { - index.addDeclaration(token, getClassEntry(type)); - } else { - index.addReference(token, getClassEntry(type), contextMethod); - } - - return; - } - - type.dumpInto(this, typeUsage, context); - } - - /** - * {@inheritDoc} - * - *

Otherwise the type usage override dumper will not go through the type instance dump - * we have here. - */ - @Override - public Dumper withTypeUsageInformation(TypeUsageInformation innerclassTypeUsageInformation) { - return new EnigmaDumper(this.sb, sourceSettings, innerclassTypeUsageInformation, options, mapper, index, dumperContext); - } - - @Override - public void informBytecodeLoc(HasByteCodeLoc loc) { - Collection methods = loc.getLoc().getMethods(); - - if (!methods.isEmpty()) { - this.contextMethod = getMethodEntry(methods.iterator().next().getMethodPrototype()); - } - } - - public SourceIndex getIndex() { - index.setSource(getString()); - return index; - } - - public String getString() { - return sb.toString(); - } - - private boolean isRecord(JavaTypeInstance javaTypeInstance) { - if (javaTypeInstance instanceof JavaRefTypeInstance) { - ClassFile classFile = ((JavaRefTypeInstance) javaTypeInstance).getClassFile(); - return classFile.getClassSignature().getSuperClass().getRawName().equals("java.lang.Record"); - } - - return false; - } -} 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 0000000..e0f2b05 --- /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 0000000..56fd0b9 --- /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 0000000..3757b02 --- /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 0000000..27b6edc --- /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 0000000..3d45712 --- /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); + } + } + } +} -- cgit v1.2.3