diff options
author | Jonas Herzig <me@johni0702.de> | 2019-08-30 16:48:00 +0200 |
---|---|---|
committer | Jonas Herzig <me@johni0702.de> | 2019-08-30 16:48:00 +0200 |
commit | cfdc125366b756a7d164502ebcde22e2976c9319 (patch) | |
tree | 03559547dd1cc52ec9a3f2a13c490da3d02182e6 | |
parent | 06279d658496a3fb0a1909bad1ebdb9a60a37aed (diff) | |
download | Remap-cfdc125366b756a7d164502ebcde22e2976c9319.tar.gz Remap-cfdc125366b756a7d164502ebcde22e2976c9319.tar.bz2 Remap-cfdc125366b756a7d164502ebcde22e2976c9319.zip |
Convert implementation and build script to Kotlin
14 files changed, 751 insertions, 873 deletions
diff --git a/build.gradle.kts b/build.gradle.kts index c00ca4c..12b502b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,31 +1,34 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { - id 'java' - id 'maven-publish' + kotlin("jvm") version "1.3.40" + `maven-publish` } -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +tasks.withType<KotlinCompile>().configureEach { + kotlinOptions.jvmTarget = "1.8" +} -group = 'com.github.replaymod' -version = 'SNAPSHOT' +group = "com.github.replaymod" +version = "SNAPSHOT" repositories { mavenCentral() } dependencies { - compile 'org.jetbrains.kotlin:kotlin-compiler:1.3.40' - compile 'org.cadixdev:lorenz:0.5.0' + compile("org.jetbrains.kotlin:kotlin-compiler:1.3.40") + compile("org.cadixdev:lorenz:0.5.0") } -jar { - archiveBaseName.set('remap') +tasks.named<Jar>("jar") { + archiveBaseName.set("remap") } publishing { publications { - maven(MavenPublication) { - from components.java + create("maven", MavenPublication::class) { + from(components["java"]) } } } diff --git a/src/main/java/com/replaymod/gradle/remap/PsiMapper.java b/src/main/java/com/replaymod/gradle/remap/PsiMapper.java deleted file mode 100644 index c1b2d41..0000000 --- a/src/main/java/com/replaymod/gradle/remap/PsiMapper.java +++ /dev/null @@ -1,429 +0,0 @@ -package com.replaymod.gradle.remap; - -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.psi.JavaRecursiveElementVisitor; -import com.intellij.psi.PsiAnnotation; -import com.intellij.psi.PsiAnnotationMemberValue; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiClassObjectAccessExpression; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiIdentifier; -import com.intellij.psi.PsiJavaCodeReferenceElement; -import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiNameValuePair; -import com.intellij.psi.PsiPackage; -import com.intellij.psi.PsiQualifiedNamedElement; -import com.intellij.psi.PsiSwitchLabelStatement; -import com.intellij.psi.PsiTypeElement; -import org.cadixdev.bombe.type.signature.MethodSignature; -import org.cadixdev.lorenz.MappingSet; -import org.cadixdev.lorenz.model.ClassMapping; -import org.cadixdev.lorenz.model.Mapping; -import org.cadixdev.lorenz.model.MethodMapping; - -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.TreeMap; - -import static com.replaymod.gradle.remap.PsiUtils.getSignature; - -class PsiMapper { - private static final String CLASS_MIXIN = "org.spongepowered.asm.mixin.Mixin"; - private static final String CLASS_ACCESSOR = "org.spongepowered.asm.mixin.gen.Accessor"; - private static final String CLASS_AT = "org.spongepowered.asm.mixin.injection.At"; - private static final String CLASS_INJECT = "org.spongepowered.asm.mixin.injection.Inject"; - private static final String CLASS_REDIRECT = "org.spongepowered.asm.mixin.injection.Redirect"; - - private final MappingSet map; - private final Map<String, ClassMapping<?, ?>> mixinMappings = new HashMap<>(); - private final PsiFile file; - private boolean error; - private TreeMap<TextRange, String> changes = new TreeMap<>(Comparator.comparing(TextRange::getStartOffset)); - - PsiMapper(MappingSet map, PsiFile file) { - this.map = map; - this.file = file; - } - - private void error(PsiElement at, String message) { - int line = StringUtil.offsetToLineNumber(file.getText(), at.getTextOffset()); - System.err.println(file.getName() + ":" + line + ": " + message); - error = true; - } - - private void replace(PsiElement e, String with) { - changes.put(e.getTextRange(), with); - } - - private void replaceIdentifier(PsiElement parent, String with) { - for (PsiElement child : parent.getChildren()) { - if (child instanceof PsiIdentifier) { - replace(child, with); - return; - } - } - } - - private boolean valid(PsiElement e) { - TextRange range = e.getTextRange(); - TextRange before = changes.ceilingKey(range); - return before == null || !before.intersects(range); - } - - private String getResult(String text) { - if (error) { - return null; - } - for (Map.Entry<TextRange, String> change : changes.descendingMap().entrySet()) { - text = change.getKey().replace(text, change.getValue()); - } - return text; - } - - private static boolean isSwitchCase(PsiElement e) { - if (e instanceof PsiSwitchLabelStatement) { - return true; - } - PsiElement parent = e.getParent(); - return parent != null && isSwitchCase(parent); - } - - private void map(PsiElement expr, PsiField field) { - PsiClass declaringClass = field.getContainingClass(); - if (declaringClass == null) return; - String name = declaringClass.getQualifiedName(); - if (name == null) return; - ClassMapping<?, ?> mapping = this.mixinMappings.get(name); - if (mapping == null) { - mapping = map.getClassMapping(name).orElse(null); - } - if (mapping == null) return; - String mapped = mapping.getFieldMapping(field.getName()).map(Mapping::getDeobfuscatedName).orElse(null); - if (mapped == null || mapped.equals(field.getName())) return; - replaceIdentifier(expr, mapped); - - if (expr instanceof PsiJavaCodeReferenceElement - && !((PsiJavaCodeReferenceElement) expr).isQualified() // qualified access is fine - && !isSwitchCase(expr) // referencing constants in case statements is fine - ) { - error(expr, "Implicit member reference to remapped field \"" + field.getName() + "\". " + - "This can cause issues if the remapped reference becomes shadowed by a local variable and is therefore forbidden. " + - "Use \"this." + field.getName() + "\" instead."); - } - } - - private void map(PsiElement expr, PsiMethod method) { - if (method.isConstructor()) return; - - PsiClass declaringClass = method.getContainingClass(); - if (declaringClass == null) return; - ArrayDeque<PsiClass> parentQueue = new ArrayDeque<>(); - parentQueue.offer(declaringClass); - ClassMapping<?, ?> mapping = null; - - String name = declaringClass.getQualifiedName(); - if (name != null) { - mapping = mixinMappings.get(name); - } - while (true) { - if (mapping != null) { - String mapped = mapping.getMethodMapping(getSignature(method)).map(Mapping::getDeobfuscatedName).orElse(null); - if (mapped != null) { - if (!mapped.equals(method.getName())) { - replaceIdentifier(expr, mapped); - } - return; - } - mapping = null; - } - while (mapping == null) { - declaringClass = parentQueue.poll(); - if (declaringClass == null) return; - - PsiClass superClass = declaringClass.getSuperClass(); - if (superClass != null) { - parentQueue.offer(superClass); - } - for (PsiClass anInterface : declaringClass.getInterfaces()) { - parentQueue.offer(anInterface); - } - - name = declaringClass.getQualifiedName(); - if (name == null) continue; - mapping = map.getClassMapping(name).orElse(null); - } - } - } - - private void map(PsiElement expr, PsiQualifiedNamedElement resolved) { - String name = resolved.getQualifiedName(); - if (name == null) return; - ClassMapping<?, ?> mapping = map.getClassMapping(name).orElse(null); - if (mapping == null) return; - String mapped = mapping.getDeobfuscatedName(); - if (mapped.equals(name)) return; - mapped = mapped.replace('/', '.'); - - if (expr.getText().equals(name)) { - replace(expr, mapped); - return; - } - replaceIdentifier(expr, mapped.substring(mapped.lastIndexOf('.') + 1)); - } - - private void map(PsiElement expr, PsiElement resolved) { - if (resolved instanceof PsiField) { - map(expr, (PsiField) resolved); - } else if (resolved instanceof PsiMethod) { - map(expr, (PsiMethod) resolved); - } else if (resolved instanceof PsiClass || resolved instanceof PsiPackage) { - map(expr, (PsiQualifiedNamedElement) resolved); - } - } - - // Note: Supports only Mixins with a single target (ignores others) and only ones specified via class literals - private PsiClass getMixinTarget(PsiAnnotation annotation) { - for (PsiNameValuePair pair : annotation.getParameterList().getAttributes()) { - String name = pair.getName(); - if (name != null && !"value".equals(name)) continue; - PsiAnnotationMemberValue value = pair.getValue(); - if (!(value instanceof PsiClassObjectAccessExpression)) continue; - PsiTypeElement type = ((PsiClassObjectAccessExpression) value).getOperand(); - PsiJavaCodeReferenceElement reference = type.getInnermostComponentReferenceElement(); - if (reference == null) continue; - return (PsiClass) reference.resolve(); - } - return null; - } - - private void remapAccessors(ClassMapping<?, ?> mapping) { - file.accept(new JavaRecursiveElementVisitor() { - @Override - public void visitMethod(PsiMethod method) { - PsiAnnotation annotation = method.getAnnotation(CLASS_ACCESSOR); - if (annotation == null) return; - - String targetByName = method.getName(); - if (targetByName.startsWith("is")) { - targetByName = targetByName.substring(2); - } else if (targetByName.startsWith("get") || targetByName.startsWith("set")) { - targetByName = targetByName.substring(3); - } else { - targetByName = null; - } - if (targetByName != null) { - targetByName = targetByName.substring(0, 1).toLowerCase() + targetByName.substring(1); - } - - String target = Arrays.stream(annotation.getParameterList().getAttributes()) - .filter(it -> it.getName() == null || it.getName().equals("value")) - .map(PsiNameValuePair::getLiteralValue) - .findAny() - .orElse(targetByName); - - if (target == null) { - throw new IllegalArgumentException("Cannot determine accessor target for " + method); - } - - String mapped = mapping.getFieldMapping(target).map(Mapping::getDeobfuscatedName).orElse(null); - if (mapped != null && !mapped.equals(target)) { - // Update accessor target - String parameterList; - if (mapped.equals(targetByName)) { - // Mapped name matches implied target, can just remove the explict target - parameterList = ""; - } else { - // Mapped name does not match implied target, need to set the target as annotation value - parameterList = "(\"" + StringUtil.escapeStringCharacters(mapped) + "\")"; - } - replace(annotation.getParameterList(), parameterList); - } - } - }); - } - - private void remapInjectsAndRedirects(ClassMapping<?, ?> mapping) { - file.accept(new JavaRecursiveElementVisitor() { - @Override - public void visitMethod(PsiMethod method) { - PsiAnnotation annotation = method.getAnnotation(CLASS_INJECT); - if (annotation == null) { - annotation = method.getAnnotation(CLASS_REDIRECT); - } - if (annotation == null) return; - - for (PsiNameValuePair attribute : annotation.getParameterList().getAttributes()) { - if (!"method".equals(attribute.getName())) continue; - // Note: mixin supports multiple targets, we do not (yet) - String literalValue = attribute.getLiteralValue(); - if (literalValue == null) continue; - String mapped; - if (literalValue.contains("(")) { - mapped = mapping.getMethodMapping(MethodSignature.of(literalValue)).map(Mapping::getDeobfuscatedName).orElse(null); - } else { - mapped = null; - for (MethodMapping methodMapping : mapping.getMethodMappings()) { - if (methodMapping.getObfuscatedName().equals(literalValue)) { - String name = methodMapping.getDeobfuscatedName(); - if (mapped != null && !mapped.equals(name)) { - error(attribute, "Ambiguous mixin method \"" + literalValue + "\" maps to \"" + mapped + "\" and \"" + name + "\""); - } - mapped = name; - } - } - } - if (mapped != null && !mapped.equals(literalValue)) { - PsiAnnotationMemberValue value = attribute.getValue(); - assert value != null; - replace(value, '"' + mapped + '"'); - } - } - } - }); - } - - private ClassMapping<?, ?> remapInternalType(String internalType, StringBuilder result) { - if (internalType.charAt(0) == 'L') { - String type = internalType.substring(1, internalType.length() - 1).replace('/', '.'); - ClassMapping<?, ?> mapping = map.getClassMapping(type).orElse(null); - if (mapping != null) { - result.append('L').append(mapping.getFullDeobfuscatedName()).append(';'); - return mapping; - } - } - result.append(internalType); - return null; - } - - private String remapFullyQualifiedMethodOrField(String signature) { - int ownerEnd = signature.indexOf(';'); - int argsBegin = signature.indexOf('('); - int argsEnd = signature.indexOf(')'); - boolean method = argsBegin != -1; - if (!method) { - argsBegin = argsEnd = signature.indexOf(':'); - } - String owner = signature.substring(0, ownerEnd + 1); - String name = signature.substring(ownerEnd + 1, argsBegin); - String returnType = signature.substring(argsEnd + 1); - - StringBuilder builder = new StringBuilder(signature.length() + 32); - ClassMapping<?, ?> mapping = remapInternalType(owner, builder); - String mapped = null; - if (mapping != null) { - mapped = (method - ? mapping.getMethodMapping(MethodSignature.of(signature.substring(ownerEnd + 1))) - : mapping.getFieldMapping(name)) - .map(Mapping::getDeobfuscatedName) - .orElse(null); - } - builder.append(mapped != null ? mapped : name); - if (method) { - builder.append('('); - String args = signature.substring(argsBegin + 1, argsEnd); - for (int i = 0; i < args.length(); i++) { - char c = args.charAt(i); - if (c != 'L') { - builder.append(c); - continue; - } - int end = args.indexOf(';', i); - String arg = args.substring(i, end + 1); - remapInternalType(arg, builder); - i = end; - } - builder.append(')'); - } else { - builder.append(':'); - } - remapInternalType(returnType, builder); - return builder.toString(); - } - - private void remapAtTargets() { - file.accept(new JavaRecursiveElementVisitor() { - @Override - public void visitAnnotation(PsiAnnotation annotation) { - if (!CLASS_AT.equals(annotation.getQualifiedName())) { - super.visitAnnotation(annotation); - return; - } - - for (PsiNameValuePair attribute : annotation.getParameterList().getAttributes()) { - if (!"target".equals(attribute.getName())) continue; - String signature = attribute.getLiteralValue(); - if (signature == null) continue; - String newSignature = remapFullyQualifiedMethodOrField(signature); - if (!newSignature.equals(signature)) { - PsiAnnotationMemberValue value = attribute.getValue(); - assert value != null; - replace(value, '"' + newSignature + '"'); - } - } - } - }); - } - - String remapFile() { - file.accept(new JavaRecursiveElementVisitor() { - @Override - public void visitClass(PsiClass psiClass) { - PsiAnnotation annotation = psiClass.getAnnotation(CLASS_MIXIN); - if (annotation == null) return; - - remapAtTargets(); - - PsiClass target = getMixinTarget(annotation); - if (target == null) return; - String qualifiedName = target.getQualifiedName(); - if (qualifiedName == null) return; - - ClassMapping<?, ?> mapping = map.getClassMapping(qualifiedName).orElse(null); - if (mapping == null) return; - - mixinMappings.put(psiClass.getQualifiedName(), mapping); - - if (!mapping.getFieldMappings().isEmpty()) { - remapAccessors(mapping); - } - if (!mapping.getMethodMappings().isEmpty()) { - remapInjectsAndRedirects(mapping); - } - } - }); - - file.accept(new JavaRecursiveElementVisitor() { - @Override - public void visitField(PsiField field) { - if (valid(field)) { - map(field, field); - } - super.visitField(field); - } - - @Override - public void visitMethod(PsiMethod method) { - if (valid(method)) { - map(method, method); - } - super.visitMethod(method); - } - - @Override - public void visitReferenceElement(PsiJavaCodeReferenceElement reference) { - if (valid(reference)) { - map(reference, reference.resolve()); - } - super.visitReferenceElement(reference); - } - }); - - return getResult(file.getText()); - } -} diff --git a/src/main/java/com/replaymod/gradle/remap/PsiUtils.java b/src/main/java/com/replaymod/gradle/remap/PsiUtils.java deleted file mode 100644 index ec4d1c8..0000000 --- a/src/main/java/com/replaymod/gradle/remap/PsiUtils.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.replaymod.gradle.remap; - -import com.intellij.psi.PsiArrayType; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiClassType; -import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiPrimitiveType; -import com.intellij.psi.PsiType; -import com.intellij.psi.util.TypeConversionUtil; -import org.cadixdev.bombe.type.ArrayType; -import org.cadixdev.bombe.type.FieldType; -import org.cadixdev.bombe.type.MethodDescriptor; -import org.cadixdev.bombe.type.ObjectType; -import org.cadixdev.bombe.type.Type; -import org.cadixdev.bombe.type.VoidType; -import org.cadixdev.bombe.type.signature.MethodSignature; - -import java.util.Arrays; -import java.util.stream.Collectors; - -class PsiUtils { - static MethodSignature getSignature(PsiMethod method) { - return new MethodSignature(method.getName(), getDescriptor(method)); - } - - private static MethodDescriptor getDescriptor(PsiMethod method) { - return new MethodDescriptor( - Arrays.stream(method.getParameterList().getParameters()) - .map(it -> getFieldType(it.getType())) - .collect(Collectors.toList()), - getType(method.getReturnType()) - ); - } - - private static FieldType getFieldType(PsiType type) { - type = TypeConversionUtil.erasure(type); - if (type instanceof PsiPrimitiveType) { - return FieldType.of(((PsiPrimitiveType) type).getKind().getBinaryName()); - } else if (type instanceof PsiArrayType) { - PsiArrayType array = (PsiArrayType) type; - return new ArrayType(array.getArrayDimensions(), getFieldType(array.getDeepComponentType())); - } else if (type instanceof PsiClassType) { - PsiClass resolved = ((PsiClassType) type).resolve(); - if (resolved == null) throw new NullPointerException("Failed to resolve type " + type); - String qualifiedName = resolved.getQualifiedName(); - if (qualifiedName == null) throw new NullPointerException("Type " + type + " has no qualified name."); - return new ObjectType(qualifiedName); - } else { - throw new IllegalArgumentException("Cannot translate type " + type); - } - } - - private static Type getType(PsiType type) { - if (TypeConversionUtil.isVoidType(type)) { - return VoidType.INSTANCE; - } else { - return getFieldType(type); - } - } - -} diff --git a/src/main/java/com/replaymod/gradle/remap/Transformer.java b/src/main/java/com/replaymod/gradle/remap/Transformer.java deleted file mode 100644 index 5b25856..0000000 --- a/src/main/java/com/replaymod/gradle/remap/Transformer.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.replaymod.gradle.remap; - -import com.intellij.codeInsight.CustomExceptionHandler; -import com.intellij.mock.MockProject; -import com.intellij.openapi.extensions.ExtensionPoint; -import com.intellij.openapi.extensions.Extensions; -import com.intellij.openapi.extensions.ExtensionsArea; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.vfs.StandardFileSystems; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.VirtualFileManager; -import com.intellij.openapi.vfs.local.CoreLocalFileSystem; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; -import com.replaymod.gradle.remap.legacy.LegacyMapping; -import org.cadixdev.lorenz.MappingSet; -import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys; -import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot; -import org.jetbrains.kotlin.cli.common.messages.MessageRenderer; -import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector; -import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles; -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; -import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace; -import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM; -import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot; -import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot; -import org.jetbrains.kotlin.config.CommonConfigurationKeys; -import org.jetbrains.kotlin.config.CompilerConfiguration; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class Transformer { - private MappingSet map; - private String[] classpath; - private boolean fail; - - public static void main(String[] args) throws IOException { - MappingSet mappings; - if (args[0].isEmpty()) { - mappings = MappingSet.create(); - } else { - mappings = LegacyMapping.readMappingSet(new File(args[0]).toPath(), args[1].equals("true")); - } - Transformer transformer = new Transformer(mappings); - - BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); - - String[] classpath = new String[Integer.parseInt(args[2])]; - for (int i = 0; i < classpath.length; i++) { - classpath[i] = reader.readLine(); - } - transformer.setClasspath(classpath); - - Map<String, String> sources = new HashMap<>(); - while (true) { - String name = reader.readLine(); - if (name == null || name.isEmpty()) { - break; - } - - String[] lines = new String[Integer.parseInt(reader.readLine())]; - for (int i = 0; i < lines.length; i++) { - lines[i] = reader.readLine(); - } - String source = String.join("\n", lines); - - sources.put(name, source); - } - - Map<String, String> results = transformer.remap(sources); - - for (String name : sources.keySet()) { - System.out.println(name); - String[] lines = results.get(name).split("\n"); - System.out.println(lines.length); - for (String line : lines) { - System.out.println(line); - } - } - - if (transformer.fail) { - System.exit(1); - } - } - - public Transformer(MappingSet mappings) { - this.map = mappings; - } - - public String[] getClasspath() { - return classpath; - } - - public void setClasspath(String[] classpath) { - this.classpath = classpath; - } - - public Map<String, String> remap(Map<String, String> sources) throws IOException { - Path tmpDir = Files.createTempDirectory("remap"); - try { - for (Entry<String, String> entry : sources.entrySet()) { - String unitName = entry.getKey(); - String source = entry.getValue(); - - Path path = tmpDir.resolve(unitName); - Files.createDirectories(path.getParent()); - Files.write(path, source.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); - } - - CompilerConfiguration config = new CompilerConfiguration(); - config.put(CommonConfigurationKeys.MODULE_NAME, "main"); - config.add(CLIConfigurationKeys.CONTENT_ROOTS, new JavaSourceRoot(tmpDir.toFile(), "")); - config.add(CLIConfigurationKeys.CONTENT_ROOTS, new KotlinSourceRoot(tmpDir.toAbsolutePath().toString(), false)); - config.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, new PrintingMessageCollector(System.err, MessageRenderer.GRADLE_STYLE, true)); - - KotlinCoreEnvironment environment = KotlinCoreEnvironment.createForProduction( - Disposer.newDisposable(), - config, - EnvironmentConfigFiles.JVM_CONFIG_FILES - ); - ExtensionsArea rootArea = Extensions.getRootArea(); - if (!rootArea.hasExtensionPoint(CustomExceptionHandler.KEY)) { - rootArea.registerExtensionPoint(CustomExceptionHandler.KEY.getName(), CustomExceptionHandler.class.getName(), ExtensionPoint.Kind.INTERFACE); - } - - MockProject project = (MockProject) environment.getProject(); - - environment.updateClasspath(Stream.of(getClasspath()).map(it -> new JvmClasspathRoot(new File(it))).collect(Collectors.toList())); - - TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration( - project, - Collections.emptyList(), - new NoScopeRecordCliBindingTrace(), - environment.getConfiguration(), - environment::createPackagePartProvider - ); - - CoreLocalFileSystem vfs = (CoreLocalFileSystem) VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL); - Map<String, String> results = new HashMap<>(); - for (String name : sources.keySet()) { - VirtualFile file = vfs.findFileByIoFile(tmpDir.resolve(name).toFile()); - assert file != null; - PsiFile psiFile = PsiManager.getInstance(project).findFile(file); - assert psiFile != null; - - String mapped = new PsiMapper(map, psiFile).remapFile(); - if (mapped == null) { - fail = true; - continue; - } - results.put(name, mapped); - } - return results; - } finally { - Files.walk(tmpDir).map(Path::toFile).sorted(Comparator.reverseOrder()).forEach(File::delete); - } - } - -} diff --git a/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMapping.java b/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMapping.java deleted file mode 100644 index 05a85d9..0000000 --- a/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMapping.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.replaymod.gradle.remap.legacy; - -import org.cadixdev.lorenz.MappingSet; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class LegacyMapping { - public static MappingSet readMappingSet(Path mappingFile, boolean invert) throws IOException { - return new LegacyMappingsReader(readMappings(mappingFile, invert)).read(); - } - - public static Map<String, LegacyMapping> readMappings(Path mappingFile, boolean invert) throws IOException { - Map<String, LegacyMapping> mappings = new HashMap<>(); - Map<String, LegacyMapping> revMappings = new HashMap<>(); - int lineNumber = 0; - for (String line : Files.readAllLines(mappingFile, StandardCharsets.UTF_8)) { - lineNumber++; - if (line.trim().startsWith("#") || line.trim().isEmpty()) continue; - - String[] parts = line.split(" "); - if (parts.length < 2 || line.contains(";")) { - throw new IllegalArgumentException("Failed to parse line " + lineNumber + " in " + mappingFile + "."); - } - - LegacyMapping mapping = mappings.get(parts[0]); - if (mapping == null) { - mapping = new LegacyMapping(); - mapping.oldName = mapping.newName = parts[0]; - mappings.put(mapping.oldName, mapping); - } - - if (parts.length == 2) { - // Class mapping - mapping.newName = parts[1]; - // Possibly merge with reverse mapping - LegacyMapping revMapping = revMappings.remove(mapping.newName); - if (revMapping != null) { - mapping.fields.putAll(revMapping.fields); - mapping.methods.putAll(revMapping.methods); - } - revMappings.put(mapping.newName, mapping); - } else if (parts.length == 3 || parts.length == 4) { - String fromName = parts[1]; - String toName; - LegacyMapping revMapping; - if (parts.length == 4) { - toName = parts[3]; - revMapping = revMappings.get(parts[2]); - if (revMapping == null) { - revMapping = new LegacyMapping(); - revMapping.oldName = revMapping.newName = parts[2]; - revMappings.put(revMapping.newName, revMapping); - } - } else { - toName = parts[2]; - revMapping = mapping; - } - if (fromName.endsWith("()")) { - // Method mapping - fromName = fromName.substring(0, fromName.length() - 2); - toName = toName.substring(0, toName.length() - 2); - mapping.methods.put(fromName, toName); - revMapping.methods.put(fromName, toName); - } else { - // Field mapping - mapping.fields.put(fromName, toName); - revMapping.fields.put(fromName, toName); - } - } else { - throw new IllegalArgumentException("Failed to parse line " + lineNumber + " in " + mappingFile + "."); - } - } - if (invert) { - Stream.concat( - mappings.values().stream(), - revMappings.values().stream() - ).distinct().forEach(it -> { - String oldName = it.oldName; - it.oldName = it.newName; - it.newName = oldName; - it.fields = it.fields.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); - it.methods = it.methods.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); - }); - } - return Stream.concat( - mappings.values().stream(), - revMappings.values().stream() - ).collect(Collectors.toMap(mapping -> mapping.oldName, Function.identity(), (mapping, other) -> { - if (!other.oldName.equals(other.newName)) { - if (!mapping.oldName.equals(mapping.newName) - && !other.oldName.equals(mapping.oldName) - && !other.newName.equals(mapping.newName)) { - throw new IllegalArgumentException("Conflicting mappings: " - + mapping.oldName + " -> " + mapping.newName - + " and " + other.oldName + " -> " + other.newName); - } - mapping.oldName = other.oldName; - mapping.newName = other.newName; - } - mapping.fields.putAll(other.fields); - mapping.methods.putAll(other.methods); - return mapping; - })); - } - - public String oldName; - public String newName; - public Map<String, String> fields = new HashMap<>(); - public Map<String, String> methods = new HashMap<>(); -} diff --git a/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.java b/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.java deleted file mode 100644 index eb72788..0000000 --- a/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.replaymod.gradle.remap.legacy; - -import org.cadixdev.bombe.type.signature.MethodSignature; -import org.cadixdev.lorenz.MappingSet; -import org.cadixdev.lorenz.impl.MappingSetModelFactoryImpl; -import org.cadixdev.lorenz.impl.model.TopLevelClassMappingImpl; -import org.cadixdev.lorenz.model.MethodMapping; -import org.cadixdev.lorenz.model.TopLevelClassMapping; - -import java.util.Optional; - -public class LegacyMappingSetModelFactory extends MappingSetModelFactoryImpl { - @Override - public TopLevelClassMapping createTopLevelClassMapping(MappingSet parent, String obfuscatedName, String deobfuscatedName) { - return new TopLevelClassMappingImpl(parent, obfuscatedName, deobfuscatedName) { - private MethodSignature stripDesc(MethodSignature signature) { - // actual descriptor isn't included in legacy format - return MethodSignature.of(signature.getName(), "()V"); - } - - @Override - public boolean hasMethodMapping(MethodSignature signature) { - return super.hasMethodMapping(signature) || super.hasMethodMapping(stripDesc(signature)); - } - - @Override - public Optional<MethodMapping> getMethodMapping(MethodSignature signature) { - Optional<MethodMapping> maybeMapping = super.getMethodMapping(signature); - if (!maybeMapping.isPresent() || !maybeMapping.get().hasMappings()) { - maybeMapping = super.getMethodMapping(stripDesc(signature)); - } - return maybeMapping; - } - }; - } -} diff --git a/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.java b/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.java deleted file mode 100644 index 502f7ba..0000000 --- a/src/main/java/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.replaymod.gradle.remap.legacy; - -import org.cadixdev.lorenz.MappingSet; -import org.cadixdev.lorenz.io.MappingsReader; -import org.cadixdev.lorenz.model.ClassMapping; - -import java.util.Map; - -public class LegacyMappingsReader extends MappingsReader { - private final Map<String, LegacyMapping> map; - - public LegacyMappingsReader(Map<String, LegacyMapping> map) { - this.map = map; - } - - @Override - public MappingSet read() { - return read(MappingSet.create(new LegacyMappingSetModelFactory())); - } - - @Override - public MappingSet read(MappingSet mappings) { - if (!(mappings.getModelFactory() instanceof LegacyMappingSetModelFactory)) { - throw new IllegalArgumentException("legacy mappings must use legacy model factory, use read() instead"); - } - for (LegacyMapping legacyMapping : map.values()) { - ClassMapping classMapping = mappings.getOrCreateClassMapping(legacyMapping.oldName) - .setDeobfuscatedName(legacyMapping.newName); - for (Map.Entry<String, String> entry : legacyMapping.fields.entrySet()) { - classMapping.getOrCreateFieldMapping(entry.getKey()) - .setDeobfuscatedName(entry.getValue()); - } - for (Map.Entry<String, String> entry : legacyMapping.methods.entrySet()) { - classMapping.getOrCreateMethodMapping(entry.getKey(), "()V") - .setDeobfuscatedName(entry.getValue()); - } - } - return mappings; - } - - @Override - public void close() { - } -} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/LorenzExtensions.kt b/src/main/kotlin/com/replaymod/gradle/remap/LorenzExtensions.kt new file mode 100644 index 0000000..cd04918 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/LorenzExtensions.kt @@ -0,0 +1,11 @@ +package com.replaymod.gradle.remap + +import org.cadixdev.bombe.type.signature.MethodSignature +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.model.ClassMapping +import org.cadixdev.lorenz.model.FieldMapping +import org.cadixdev.lorenz.model.MethodMapping + +fun MappingSet.findClassMapping(obfuscatedName: String): ClassMapping<*, *>? = getClassMapping(obfuscatedName).orElse(null) +fun ClassMapping<*, *>.findFieldMapping(obfuscatedName: String): FieldMapping? = getFieldMapping(obfuscatedName).orElse(null) +fun ClassMapping<*, *>.findMethodMapping(signature: MethodSignature): MethodMapping? = getMethodMapping(signature).orElse(null) diff --git a/src/main/kotlin/com/replaymod/gradle/remap/PsiMapper.kt b/src/main/kotlin/com/replaymod/gradle/remap/PsiMapper.kt new file mode 100644 index 0000000..95ee858 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/PsiMapper.kt @@ -0,0 +1,365 @@ +package com.replaymod.gradle.remap + +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.* +import com.replaymod.gradle.remap.PsiUtils.getSignature +import org.cadixdev.bombe.type.signature.MethodSignature +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.model.ClassMapping +import java.util.* + +internal class PsiMapper(private val map: MappingSet, private val file: PsiFile) { + private val mixinMappings = mutableMapOf<String, ClassMapping<*, *>>() + private var error: Boolean = false + private val changes = TreeMap<TextRange, String>(Comparator.comparing<TextRange, Int> { it.startOffset }) + + private fun error(at: PsiElement, message: String) { + val line = StringUtil.offsetToLineNumber(file.text, at.textOffset) + System.err.println(file.name + ":" + line + ": " + message) + error = true + } + + private fun replace(e: PsiElement, with: String) { + changes[e.textRange] = with + } + + private fun replaceIdentifier(parent: PsiElement, with: String) { + for (child in parent.children) { + if (child is PsiIdentifier) { + replace(child, with) + return + } + } + } + + private fun valid(e: PsiElement): Boolean { + val range = e.textRange + val before = changes.ceilingKey(range) + return before == null || !before.intersects(range) + } + + private fun getResult(text: String): String? { + if (error) { + return null + } + var result = text + for ((key, value) in changes.descendingMap()) { + result = key.replace(result, value) + } + return result + } + + private fun map(expr: PsiElement, field: PsiField) { + val fieldName = field.name ?: return + val declaringClass = field.containingClass ?: return + val name = declaringClass.qualifiedName ?: return + var mapping: ClassMapping<*, *>? = this.mixinMappings[name] + if (mapping == null) { + mapping = map.findClassMapping(name) + } + if (mapping == null) return + val mapped = mapping.findFieldMapping(fieldName)?.deobfuscatedName + if (mapped == null || mapped == fieldName) return + replaceIdentifier(expr, mapped) + + if (expr is PsiJavaCodeReferenceElement + && !expr.isQualified // qualified access is fine + && !isSwitchCase(expr) // referencing constants in case statements is fine + ) { + error(expr, "Implicit member reference to remapped field \"$fieldName\". " + + "This can cause issues if the remapped reference becomes shadowed by a local variable and is therefore forbidden. " + + "Use \"this.$fieldName\" instead.") + } + } + + private fun map(expr: PsiElement, method: PsiMethod) { + if (method.isConstructor) return + + var declaringClass: PsiClass? = method.containingClass ?: return + val parentQueue = ArrayDeque<PsiClass>() + parentQueue.offer(declaringClass) + var mapping: ClassMapping<*, *>? = null + + var name = declaringClass!!.qualifiedName + if (name != null) { + mapping = mixinMappings[name] + } + while (true) { + if (mapping != null) { + val mapped = mapping.findMethodMapping(getSignature(method))?.deobfuscatedName + if (mapped != null) { + if (mapped != method.name) { + replaceIdentifier(expr, mapped) + } + return + } + mapping = null + } + while (mapping == null) { + declaringClass = parentQueue.poll() + if (declaringClass == null) return + + val superClass = declaringClass.superClass + if (superClass != null) { + parentQueue.offer(superClass) + } + for (anInterface in declaringClass.interfaces) { + parentQueue.offer(anInterface) + } + + name = declaringClass.qualifiedName + if (name == null) continue + mapping = map.findClassMapping(name) + } + } + } + + private fun map(expr: PsiElement, resolved: PsiQualifiedNamedElement) { + val name = resolved.qualifiedName ?: return + val mapping = map.findClassMapping(name) ?: return + var mapped = mapping.deobfuscatedName + if (mapped == name) return + mapped = mapped.replace('/', '.') + + if (expr.text == name) { + replace(expr, mapped) + return + } + replaceIdentifier(expr, mapped.substring(mapped.lastIndexOf('.') + 1)) + } + + private fun map(expr: PsiElement, resolved: PsiElement?) { + when (resolved) { + is PsiField -> map(expr, resolved) + is PsiMethod -> map(expr, resolved) + is PsiClass, is PsiPackage -> map(expr, resolved as PsiQualifiedNamedElement) + } + } + + // Note: Supports only Mixins with a single target (ignores others) and only ones specified via class literals + private fun getMixinTarget(annotation: PsiAnnotation): PsiClass? { + for (pair in annotation.parameterList.attributes) { + val name = pair.name + if (name != null && "value" != name) continue + val value = pair.value + if (value !is PsiClassObjectAccessExpression) continue + val type = value.operand + val reference = type.innermostComponentReferenceElement ?: continue + return reference.resolve() as PsiClass? + } + return null + } + + private fun remapAccessors(mapping: ClassMapping<*, *>) { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitMethod(method: PsiMethod) { + val annotation = method.getAnnotation(CLASS_ACCESSOR) ?: return + + val methodName = method.name + val targetByName = when { + methodName.startsWith("is") -> methodName.substring(2) + methodName.startsWith("get") || methodName.startsWith("set") -> methodName.substring(3) + else -> null + }?.decapitalize() + + val target = annotation.parameterList.attributes.find { + it.name == null || it.name == "value" + }?.literalValue ?: targetByName ?: throw IllegalArgumentException("Cannot determine accessor target for $method") + + val mapped = mapping.findFieldMapping(target)?.deobfuscatedName + if (mapped != null && mapped != target) { + // Update accessor target + replace(annotation.parameterList, if (mapped == targetByName) { + // Mapped name matches implied target, can just remove the explict target + "" + } else { + // Mapped name does not match implied target, need to set the target as annotation value + "(\"" + StringUtil.escapeStringCharacters(mapped) + "\")" + }) + } + } + }) + } + + private fun remapInjectsAndRedirects(mapping: ClassMapping<*, *>) { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitMethod(method: PsiMethod) { + val annotation = method.getAnnotation(CLASS_INJECT) ?: method.getAnnotation(CLASS_REDIRECT) ?: return + + for (attribute in annotation.parameterList.attributes) { + if ("method" != attribute.name) continue + // Note: mixin supports multiple targets, we do not (yet) + val literalValue = attribute.literalValue ?: continue + var mapped: String? + if (literalValue.contains("(")) { + mapped = mapping.findMethodMapping(MethodSignature.of(literalValue))?.deobfuscatedName + } else { + mapped = null + for (methodMapping in mapping.methodMappings) { + if (methodMapping.obfuscatedName == literalValue) { + val name = methodMapping.deobfuscatedName + if (mapped != null && mapped != name) { + error(attribute, "Ambiguous mixin method \"$literalValue\" maps to \"$mapped\" and \"$name\"") + } + mapped = name + } + } + } + if (mapped != null && mapped != literalValue) { + val value = attribute.value!! + replace(value, '"'.toString() + mapped + '"'.toString()) + } + } + } + }) + } + + private fun remapInternalType(internalType: String, result: StringBuilder): ClassMapping<*, *>? { + if (internalType[0] == 'L') { + val type = internalType.substring(1, internalType.length - 1).replace('/', '.') + val mapping = map.findClassMapping(type) + if (mapping != null) { + result.append('L').append(mapping.fullDeobfuscatedName).append(';') + return mapping + } + } + result.append(internalType) + return null + } + + private fun remapFullyQualifiedMethodOrField(signature: String): String { + val ownerEnd = signature.indexOf(';') + var argsBegin = signature.indexOf('(') + var argsEnd = signature.indexOf(')') + val method = argsBegin != -1 + if (!method) { + argsEnd = signature.indexOf(':') + argsBegin = argsEnd + } + val owner = signature.substring(0, ownerEnd + 1) + val name = signature.substring(ownerEnd + 1, argsBegin) + val returnType = signature.substring(argsEnd + 1) + + val builder = StringBuilder(signature.length + 32) + val mapping = remapInternalType(owner, builder) + var mapped: String? = null + if (mapping != null) { + mapped = (if (method) { + mapping.findMethodMapping(MethodSignature.of(signature.substring(ownerEnd + 1))) + } else { + mapping.findFieldMapping(name) + })?.deobfuscatedName + } + builder.append(mapped ?: name) + if (method) { + builder.append('(') + val args = signature.substring(argsBegin + 1, argsEnd) + var i = 0 + while (i < args.length) { + val c = args[i] + if (c != 'L') { + builder.append(c) + i++ + continue + } + val end = args.indexOf(';', i) + val arg = args.substring(i, end + 1) + remapInternalType(arg, builder) + i = end + i++ + } + builder.append(')') + } else { + builder.append(':') + } + remapInternalType(returnType, builder) + return builder.toString() + } + + private fun remapAtTargets() { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (CLASS_AT != annotation.qualifiedName) { + super.visitAnnotation(annotation) + return + } + + for (attribute in annotation.parameterList.attributes) { + if ("target" != attribute.name) continue + val signature = attribute.literalValue ?: continue + val newSignature = remapFullyQualifiedMethodOrField(signature) + if (newSignature != signature) { + val value = attribute.value!! + replace(value, "\"$newSignature\"") + } + } + } + }) + } + + fun remapFile(): String? { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitClass(psiClass: PsiClass) { + val annotation = psiClass.getAnnotation(CLASS_MIXIN) ?: return + + remapAtTargets() + + val target = getMixinTarget(annotation) ?: return + val qualifiedName = target.qualifiedName ?: return + + val mapping = map.findClassMapping(qualifiedName) ?: return + + mixinMappings[psiClass.qualifiedName!!] = mapping + + if (!mapping.fieldMappings.isEmpty()) { + remapAccessors(mapping) + } + if (!mapping.methodMappings.isEmpty()) { + remapInjectsAndRedirects(mapping) + } + } + }) + + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitField(field: PsiField) { + if (valid(field)) { + map(field, field) + } + super.visitField(field) + } + + override fun visitMethod(method: PsiMethod) { + if (valid(method)) { + map(method, method) + } + super.visitMethod(method) + } + + override fun visitReferenceElement(reference: PsiJavaCodeReferenceElement) { + if (valid(reference)) { + map(reference, reference.resolve()) + } + super.visitReferenceElement(reference) + } + }) + + return getResult(file.text) + } + + companion object { + private const val CLASS_MIXIN = "org.spongepowered.asm.mixin.Mixin" + private const val CLASS_ACCESSOR = "org.spongepowered.asm.mixin.gen.Accessor" + private const val CLASS_AT = "org.spongepowered.asm.mixin.injection.At" + private const val CLASS_INJECT = "org.spongepowered.asm.mixin.injection.Inject" + private const val CLASS_REDIRECT = "org.spongepowered.asm.mixin.injection.Redirect" + + private fun isSwitchCase(e: PsiElement): Boolean { + if (e is PsiSwitchLabelStatement) { + return true + } + val parent = e.parent + return parent != null && isSwitchCase(parent) + } + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/PsiUtils.kt b/src/main/kotlin/com/replaymod/gradle/remap/PsiUtils.kt new file mode 100644 index 0000000..de29956 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/PsiUtils.kt @@ -0,0 +1,41 @@ +package com.replaymod.gradle.remap + +import com.intellij.psi.* +import com.intellij.psi.util.TypeConversionUtil +import org.cadixdev.bombe.type.ArrayType +import org.cadixdev.bombe.type.FieldType +import org.cadixdev.bombe.type.MethodDescriptor +import org.cadixdev.bombe.type.ObjectType +import org.cadixdev.bombe.type.Type +import org.cadixdev.bombe.type.VoidType +import org.cadixdev.bombe.type.signature.MethodSignature + +internal object PsiUtils { + fun getSignature(method: PsiMethod): MethodSignature = MethodSignature(method.name, getDescriptor(method)) + + private fun getDescriptor(method: PsiMethod): MethodDescriptor = MethodDescriptor( + method.parameterList.parameters.map { getFieldType(it.type) }, + getType(method.returnType) + ) + + private fun getFieldType(type: PsiType?): FieldType = when (val erasedType = TypeConversionUtil.erasure(type)) { + is PsiPrimitiveType -> FieldType.of(erasedType.kind.binaryName) + is PsiArrayType -> { + val array = erasedType as PsiArrayType? + ArrayType(array!!.arrayDimensions, getFieldType(array.deepComponentType)) + } + is PsiClassType -> { + val resolved = erasedType.resolve() ?: throw NullPointerException("Failed to resolve type $erasedType") + val qualifiedName = resolved.qualifiedName + ?: throw NullPointerException("Type $erasedType has no qualified name.") + ObjectType(qualifiedName) + } + else -> throw IllegalArgumentException("Cannot translate type " + erasedType!!) + } + + private fun getType(type: PsiType?): Type = if (TypeConversionUtil.isVoidType(type)) { + VoidType.INSTANCE + } else { + getFieldType(type) + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt b/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt new file mode 100644 index 0000000..303ee42 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt @@ -0,0 +1,149 @@ +package com.replaymod.gradle.remap + +import com.intellij.codeInsight.CustomExceptionHandler +import com.intellij.mock.MockProject +import com.intellij.openapi.extensions.ExtensionPoint +import com.intellij.openapi.extensions.Extensions +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.local.CoreLocalFileSystem +import com.intellij.psi.PsiManager +import com.intellij.psi.search.GlobalSearchScope +import com.replaymod.gradle.remap.legacy.LegacyMapping +import org.cadixdev.lorenz.MappingSet +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.config.ContentRoot +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration +import java.io.BufferedReader +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.StandardOpenOption +import java.util.* +import kotlin.system.exitProcess + +class Transformer(private val map: MappingSet) { + var classpath: Array<String>? = null + private var fail: Boolean = false + + @Throws(IOException::class) + fun remap(sources: Map<String, String>): Map<String, String> { + val tmpDir = Files.createTempDirectory("remap") + try { + for ((unitName, source) in sources) { + val path = tmpDir.resolve(unitName) + Files.createDirectories(path.parent) + Files.write(path, source.toByteArray(StandardCharsets.UTF_8), StandardOpenOption.CREATE) + } + + val config = CompilerConfiguration() + config.put(CommonConfigurationKeys.MODULE_NAME, "main") + config.add<ContentRoot>(CLIConfigurationKeys.CONTENT_ROOTS, JavaSourceRoot(tmpDir.toFile(), "")) + config.add<ContentRoot>(CLIConfigurationKeys.CONTENT_ROOTS, KotlinSourceRoot(tmpDir.toAbsolutePath().toString(), false)) + config.put<MessageCollector>(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, PrintingMessageCollector(System.err, MessageRenderer.GRADLE_STYLE, true)) + + val environment = KotlinCoreEnvironment.createForProduction( + Disposer.newDisposable(), + config, + EnvironmentConfigFiles.JVM_CONFIG_FILES + ) + val rootArea = Extensions.getRootArea() + if (!rootArea.hasExtensionPoint(CustomExceptionHandler.KEY)) { + rootArea.registerExtensionPoint(CustomExceptionHandler.KEY.name, CustomExceptionHandler::class.java.name, ExtensionPoint.Kind.INTERFACE) + } + + val project = environment.project as MockProject + + environment.updateClasspath(classpath!!.map { JvmClasspathRoot(File(it)) }) + + TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration( + project, + emptyList(), + NoScopeRecordCliBindingTrace(), + environment.configuration, + { scope: GlobalSearchScope -> environment.createPackagePartProvider(scope) } + ) + + val vfs = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) as CoreLocalFileSystem + val results = HashMap<String, String>() + for (name in sources.keys) { + val file = vfs.findFileByIoFile(tmpDir.resolve(name).toFile())!! + val psiFile = PsiManager.getInstance(project).findFile(file)!! + + val mapped = PsiMapper(map, psiFile).remapFile() + if (mapped == null) { + fail = true + continue + } + results[name] = mapped + } + return results + } finally { + Files.walk(tmpDir).map<File> { it.toFile() }.sorted(Comparator.reverseOrder()).forEach { it.delete() } + } + } + + companion object { + + @Throws(IOException::class) + @JvmStatic + fun main(args: Array<String>) { + val mappings: MappingSet = if (args[0].isEmpty()) { + MappingSet.create() + } else { + LegacyMapping.readMappingSet(File(args[0]).toPath(), args[1] == "true") + } + val transformer = Transformer(mappings) + + val reader = BufferedReader(InputStreamReader(System.`in`)) + + transformer.classpath = (1..Integer.parseInt(args[2])).map { reader.readLine() }.toTypedArray() + + val sources = mutableMapOf<String, String>() + while (true) { + val name = reader.readLine() + if (name == null || name.isEmpty()) { + break + } + + val lines = arrayOfNulls<String>(Integer.parseInt(reader.readLine())) + for (i in lines.indices) { + lines[i] = reader.readLine() + } + val source = lines.joinToString("\n") + + sources[name] = source + } + + val results = transformer.remap(sources) + + for (name in sources.keys) { + println(name) + val lines = results.getValue(name).split("\n").dropLastWhile { it.isEmpty() }.toTypedArray() + println(lines.size) + for (line in lines) { + println(line) + } + } + + if (transformer.fail) { + exitProcess(1) + } + } + } + +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMapping.kt b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMapping.kt new file mode 100644 index 0000000..389b1c9 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMapping.kt @@ -0,0 +1,109 @@ +package com.replaymod.gradle.remap.legacy + +import org.cadixdev.lorenz.MappingSet + +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.util.HashMap + +class LegacyMapping(var oldName: String, var newName: String) { + var fields: MutableMap<String, String> = mutableMapOf() + var methods: MutableMap<String, String> = mutableMapOf() + + companion object { + @Throws(IOException::class) + fun readMappingSet(mappingFile: Path, invert: Boolean): MappingSet { + return LegacyMappingsReader(readMappings(mappingFile, invert)).read() + } + + @Throws(IOException::class) + fun readMappings(mappingFile: Path, invert: Boolean): Map<String, LegacyMapping> { + val mappings = HashMap<String, LegacyMapping>() + val revMappings = HashMap<String, LegacyMapping>() + var lineNumber = 0 + for (line in Files.readAllLines(mappingFile, StandardCharsets.UTF_8)) { + lineNumber++ + if (line.trim { it <= ' ' }.startsWith("#") || line.trim { it <= ' ' }.isEmpty()) continue + + val parts = line.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + require(!(parts.size < 2 || line.contains(";"))) { "Failed to parse line $lineNumber in $mappingFile." } + + var mapping: LegacyMapping? = mappings[parts[0]] + if (mapping == null) { + mapping = LegacyMapping(parts[0], parts[0]) + mappings[mapping.oldName] = mapping + } + + if (parts.size == 2) { + // Class mapping + mapping.newName = parts[1] + // Possibly merge with reverse mapping + val revMapping = revMappings.remove(mapping.newName) + if (revMapping != null) { + mapping.fields.putAll(revMapping.fields) + mapping.methods.putAll(revMapping.methods) + } + revMappings[mapping.newName] = mapping + } else if (parts.size == 3 || parts.size == 4) { + var fromName = parts[1] + var toName: String + var revMapping: LegacyMapping? + if (parts.size == 4) { + toName = parts[3] + revMapping = revMappings[parts[2]] + if (revMapping == null) { + revMapping = LegacyMapping(parts[2], parts[2]) + revMappings[revMapping.newName] = revMapping + } + } else { + toName = parts[2] + revMapping = mapping + } + if (fromName.endsWith("()")) { + // Method mapping + fromName = fromName.substring(0, fromName.length - 2) + toName = toName.substring(0, toName.length - 2) + mapping.methods[fromName] = toName + revMapping.methods[fromName] = toName + } else { + // Field mapping + mapping.fields[fromName] = toName + revMapping.fields[fromName] = toName + } + } else { + throw IllegalArgumentException("Failed to parse line $lineNumber in $mappingFile.") + } + } + if (invert) { + + (mappings.values + revMappings.values).distinct().forEach { mapping -> + mapping.oldName = mapping.newName.also { mapping.newName = mapping.oldName } + mapping.fields = mapping.fields.map { it.value to it.key }.toMap(mutableMapOf()) + mapping.methods = mapping.methods.map { it.value to it.key }.toMap(mutableMapOf()) + } + } + val result = mutableMapOf<String, LegacyMapping>() + for (mapping in (mappings.values + revMappings.values)) { + val key = mapping.oldName + val other = result[key] + result[key] = if (other != null) { + if (other.oldName != other.newName) { + require(mapping.oldName == mapping.newName || other.oldName == mapping.oldName || other.newName == mapping.newName) { + "Conflicting mappings: ${mapping.oldName} -> ${mapping.newName} and ${other.oldName} -> ${other.newName}" + } + mapping.oldName = other.oldName + mapping.newName = other.newName + } + mapping.fields.putAll(other.fields) + mapping.methods.putAll(other.methods) + mapping + } else { + mapping + } + } + return result + } + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.kt b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.kt new file mode 100644 index 0000000..7737200 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.kt @@ -0,0 +1,33 @@ +package com.replaymod.gradle.remap.legacy + +import org.cadixdev.bombe.type.signature.MethodSignature +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.impl.MappingSetModelFactoryImpl +import org.cadixdev.lorenz.impl.model.TopLevelClassMappingImpl +import org.cadixdev.lorenz.model.MethodMapping +import org.cadixdev.lorenz.model.TopLevelClassMapping + +import java.util.Optional + +class LegacyMappingSetModelFactory : MappingSetModelFactoryImpl() { + override fun createTopLevelClassMapping(parent: MappingSet, obfuscatedName: String, deobfuscatedName: String): TopLevelClassMapping { + return object : TopLevelClassMappingImpl(parent, obfuscatedName, deobfuscatedName) { + private fun stripDesc(signature: MethodSignature): MethodSignature { + // actual descriptor isn't included in legacy format + return MethodSignature.of(signature.name, "()V") + } + + override fun hasMethodMapping(signature: MethodSignature): Boolean { + return super.hasMethodMapping(signature) || super.hasMethodMapping(stripDesc(signature)) + } + + override fun getMethodMapping(signature: MethodSignature): Optional<MethodMapping> { + var maybeMapping = super.getMethodMapping(signature) + if (!maybeMapping.isPresent || !maybeMapping.get().hasMappings()) { + maybeMapping = super.getMethodMapping(stripDesc(signature)) + } + return maybeMapping + } + } + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.kt b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.kt new file mode 100644 index 0000000..8a6144e --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.kt @@ -0,0 +1,28 @@ +package com.replaymod.gradle.remap.legacy + +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.io.MappingsReader + +class LegacyMappingsReader(private val map: Map<String, LegacyMapping>) : MappingsReader() { + + override fun read(): MappingSet { + return read(MappingSet.create(LegacyMappingSetModelFactory())) + } + + override fun read(mappings: MappingSet): MappingSet { + require(mappings.modelFactory is LegacyMappingSetModelFactory) { "legacy mappings must use legacy model factory, use read() instead" } + for (legacyMapping in map.values) { + val classMapping = mappings.getOrCreateClassMapping(legacyMapping.oldName) + .setDeobfuscatedName(legacyMapping.newName) + for ((key, value) in legacyMapping.fields) { + classMapping.getOrCreateFieldMapping(key).deobfuscatedName = value + } + for ((key, value) in legacyMapping.methods) { + classMapping.getOrCreateMethodMapping(key, "()V").deobfuscatedName = value + } + } + return mappings + } + + override fun close() {} +} |