From ded4074436e70b394a7f5453b37fbd80d21cb4c5 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Sun, 26 Jun 2022 16:57:20 -0700 Subject: [PATCH] Add plugin remap tool --- build.gradle.kts | 4 +- buildSrc/build.gradle.kts | 12 + .../modified/mercury/RemapperVisitor.java | 514 ++++++++++++++++++ .../mercury/SimpleRemapperVisitor.java | 496 +++++++++++++++++ buildSrc/src/main/kotlin/MercuryRemapper.kt | 42 ++ .../src/main/kotlin/RemapPluginSources.kt | 98 ++++ buildSrc/src/main/kotlin/ReverseMappings.kt | 43 ++ .../main/kotlin/remap-plugin-src.gradle.kts | 46 ++ 8 files changed, 1254 insertions(+), 1 deletion(-) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/java/modified/mercury/RemapperVisitor.java create mode 100644 buildSrc/src/main/java/modified/mercury/SimpleRemapperVisitor.java create mode 100644 buildSrc/src/main/kotlin/MercuryRemapper.kt create mode 100644 buildSrc/src/main/kotlin/RemapPluginSources.kt create mode 100644 buildSrc/src/main/kotlin/ReverseMappings.kt create mode 100644 buildSrc/src/main/kotlin/remap-plugin-src.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts index 91f8680e..41c5fe13 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,9 +2,11 @@ import xyz.jpenilla.resourcefactory.bukkit.BukkitPluginYaml plugins { `java-library` - id("io.papermc.paperweight.userdev") version "1.6.0" + id("io.papermc.paperweight.userdev") id("xyz.jpenilla.run-paper") version "2.2.4" // Adds runServer and runMojangMappedServer tasks for testing id("xyz.jpenilla.resource-factory-bukkit-convention") version "1.1.1" // Generates plugin.yml based on the Gradle config + + `remap-plugin-src` } group = "io.papermc.paperweight" diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..ec69b05e --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + implementation("io.papermc.paperweight:paperweight-userdev:1.6.0") +} diff --git a/buildSrc/src/main/java/modified/mercury/RemapperVisitor.java b/buildSrc/src/main/java/modified/mercury/RemapperVisitor.java new file mode 100644 index 00000000..eb91fb51 --- /dev/null +++ b/buildSrc/src/main/java/modified/mercury/RemapperVisitor.java @@ -0,0 +1,514 @@ +/* + * Copyright (c) 2018 Cadix Development (https://www.cadixdev.org) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package modified.mercury; + +import static paper.libs.org.cadixdev.mercury.util.BombeBindings.isPackagePrivate; + +import paper.libs.org.cadixdev.lorenz.MappingSet; +import paper.libs.org.cadixdev.lorenz.model.ClassMapping; +import paper.libs.org.cadixdev.lorenz.model.InnerClassMapping; +import paper.libs.org.cadixdev.lorenz.model.Mapping; +import paper.libs.org.cadixdev.lorenz.model.TopLevelClassMapping; +import paper.libs.org.cadixdev.mercury.RewriteContext; +import paper.libs.org.cadixdev.mercury.jdt.rewrite.imports.ImportRewrite; +import paper.libs.org.cadixdev.mercury.util.GracefulCheck; +import paper.libs.org.eclipse.jdt.core.dom.AST; +import paper.libs.org.eclipse.jdt.core.dom.ASTNode; +import paper.libs.org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.AnnotatableType; +import paper.libs.org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.AnonymousClassDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.EnumDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.IBinding; +import paper.libs.org.eclipse.jdt.core.dom.IDocElement; +import paper.libs.org.eclipse.jdt.core.dom.ITypeBinding; +import paper.libs.org.eclipse.jdt.core.dom.ImportDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.Javadoc; +import paper.libs.org.eclipse.jdt.core.dom.Modifier; +import paper.libs.org.eclipse.jdt.core.dom.Name; +import paper.libs.org.eclipse.jdt.core.dom.NameQualifiedType; +import paper.libs.org.eclipse.jdt.core.dom.PackageDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.QualifiedName; +import paper.libs.org.eclipse.jdt.core.dom.QualifiedType; +import paper.libs.org.eclipse.jdt.core.dom.SimpleName; +import paper.libs.org.eclipse.jdt.core.dom.SimpleType; +import paper.libs.org.eclipse.jdt.core.dom.TagElement; +import paper.libs.org.eclipse.jdt.core.dom.TypeDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import paper.libs.org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import paper.libs.org.eclipse.jdt.internal.compiler.lookup.PackageBinding; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class RemapperVisitor extends SimpleRemapperVisitor { + + private final ImportRewrite importRewrite; + private final Deque importStack = new ArrayDeque<>(); + private final String simpleDeobfuscatedName; + + public RemapperVisitor(RewriteContext context, MappingSet mappings, boolean javadoc) { + super(context, mappings, javadoc); + + this.importRewrite = context.createImportRewrite(); + importRewrite.setUseContextToFilterImplicitImports(true); + + TopLevelClassMapping primary = mappings.getTopLevelClassMapping(context.getQualifiedPrimaryType()).orElse(null); + if (primary != null) { + context.setPackageName(primary.getDeobfuscatedPackage().replace('/', '.')); + this.importRewrite.setImplicitPackageName(context.getPackageName()); + + this.simpleDeobfuscatedName = primary.getSimpleDeobfuscatedName(); + context.setPrimaryType(simpleDeobfuscatedName); + + List implicitTypes = new ArrayList<>(); + String simpleObfuscatedName = primary.getSimpleObfuscatedName(); + + @SuppressWarnings("unchecked") + List types = context.getCompilationUnit().types(); + for (AbstractTypeDeclaration type : types) { + String name = type.getName().getIdentifier(); + if (name.equals(simpleObfuscatedName)) { + implicitTypes.add(simpleDeobfuscatedName); + } else { + implicitTypes.add(mappings.getTopLevelClassMapping(context.getPackageName() + '.' + name) + .map(Mapping::getSimpleDeobfuscatedName) + .orElse(name)); + } + } + this.importRewrite.setImplicitTypes(implicitTypes); + } else { + this.simpleDeobfuscatedName = null; + } + } + + private void remapType(SimpleName node, ITypeBinding binding) { + if (binding.isTypeVariable() || GracefulCheck.checkGracefully(this.context, binding)) { + return; + } + + if (binding.getBinaryName() == null) { + throw new IllegalStateException("Binary name for binding " + binding.getQualifiedName() + " is null. Did you forget to add a library to the classpath?"); + } + + ClassMapping mapping = this.mappings.computeClassMapping(binding.getBinaryName()).orElse(null); + + if (node.getParent() instanceof AbstractTypeDeclaration + || node.getParent() instanceof QualifiedType + || node.getParent() instanceof NameQualifiedType + || binding.isLocal()) { + if (mapping != null) { + updateIdentifier(node, mapping.getSimpleDeobfuscatedName()); + } + return; + } + + String qualifiedName = (mapping != null ? mapping.getFullDeobfuscatedName().replace('/', '.') : binding.getBinaryName()).replace('$', '.'); + String newName = this.importRewrite.addImport(qualifiedName, this.importStack.peek()); + + if (!node.getIdentifier().equals(newName) && !node.isVar()) { + if (newName.indexOf('.') == -1) { + this.context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, newName, null); + } else { + // Qualified name + this.context.createASTRewrite().replace(node, node.getAST().newName(newName), null); + } + } + } + + private void remapQualifiedType(QualifiedName node, ITypeBinding binding) { + String binaryName = binding.getBinaryName(); + if (binaryName == null) { + if (this.context.getMercury().isGracefulClasspathChecks() || this.context.getMercury().isGracefulJavadocClasspathChecks() && GracefulCheck.isJavadoc(node)) { + return; + } + throw new IllegalStateException("No binary name for " + binding.getQualifiedName()); + } + TopLevelClassMapping mapping = this.mappings.getTopLevelClassMapping(binaryName).orElse(null); + + if (mapping == null) { + return; + } + + String newName = mapping.getDeobfuscatedName().replace('/', '.'); + if (binaryName.equals(newName)) { + return; + } + + this.context.createASTRewrite().replace(node, node.getAST().newName(newName), null); + } + + private void remapInnerType(QualifiedName qualifiedName, ITypeBinding outerClass) { + final String binaryName = outerClass.getBinaryName(); + if (binaryName == null) { + if (this.context.getMercury().isGracefulClasspathChecks()) { + return; + } + throw new IllegalStateException("No binary name for " + outerClass.getQualifiedName()); + } + + ClassMapping outerClassMapping = this.mappings.computeClassMapping(binaryName).orElse(null); + if (outerClassMapping == null) { + return; + } + + SimpleName node = qualifiedName.getName(); + InnerClassMapping mapping = outerClassMapping.getInnerClassMapping(node.getIdentifier()).orElse(null); + if (mapping == null) { + return; + } + + updateIdentifier(node, mapping.getDeobfuscatedName()); + } + + @Override + protected void visit(SimpleName node, IBinding binding) { + switch (binding.getKind()) { + case IBinding.TYPE: + remapType(node, (ITypeBinding) binding); + break; + case IBinding.METHOD: + case IBinding.VARIABLE: + super.visit(node, binding); + break; + case IBinding.PACKAGE: + // This is ignored because it should be covered by separate handling + // of QualifiedName (for full-qualified class references), + // PackageDeclaration and ImportDeclaration + default: + throw new IllegalStateException("Unhandled binding: " + binding.getClass().getSimpleName() + " (" + binding.getKind() + ')'); + } + } + + @Override + public boolean visit(final TagElement tag) { + // We don't want to visit the names of some Javadoc tags, since they can't be remapped. + if (TagElement.TAG_LINK.equals(tag.getTagName())) { + // With a @link tag, the first fragment will be a name + if (tag.fragments().size() >= 1) { + final Object fragment = tag.fragments().get(0); + + // A package might be a SimpleName (test), or a QualifiedName (test.test) + if (fragment instanceof Name) { + final Name name = (Name) fragment; + final IBinding binding = name.resolveBinding(); + + if (binding != null) { + // We can't remap packages, so don't visit package names + if (binding.getKind() == IBinding.PACKAGE) { + return false; + } + } + } + } + } + + return super.visit(tag); + } + + @Override + public boolean visit(QualifiedName node) { + IBinding binding = node.resolveBinding(); + if (binding == null) { + if (this.context.getMercury().isGracefulClasspathChecks()) { + return false; + } + throw new IllegalStateException("No binding for qualified name node " + node.getFullyQualifiedName()); + } + + if (binding.getKind() != IBinding.TYPE) { + // Unpack the qualified name and remap method/field and type separately + return true; + } + + Name qualifier = node.getQualifier(); + IBinding qualifierBinding = qualifier.resolveBinding(); + switch (qualifierBinding.getKind()) { + case IBinding.PACKAGE: + // Remap full qualified type + remapQualifiedType(node, (ITypeBinding) binding); + break; + case IBinding.TYPE: + // Remap inner type separately + remapInnerType(node, (ITypeBinding) qualifierBinding); + + // Remap the qualifier + qualifier.accept(this); + break; + default: + throw new IllegalStateException("Unexpected qualifier binding: " + binding.getClass().getSimpleName() + " (" + binding.getKind() + ')'); + } + + return false; + } + + @Override + public boolean visit(NameQualifiedType node) { + // Annotated inner class -> com.package.Outer.@NonNull Inner + // existing mechanisms will handle + final IBinding qualBinding = node.getQualifier().resolveBinding(); + if (qualBinding != null && qualBinding.getKind() == IBinding.TYPE) { + return true; + } + + ITypeBinding binding = node.getName().resolveTypeBinding(); + if (binding == null) { + if (this.context.getMercury().isGracefulClasspathChecks()) { + return false; + } + throw new IllegalStateException("No binding for qualified name node " + node.getName()); + } + + final ClassMapping classMapping = this.mappings.computeClassMapping(binding.getBinaryName()).orElse(null); + if (classMapping == null) { + return false; + } + + // qualified -> default package (test.@NonNull ObfClass -> @NonNull Core): + final String deobfPackage = classMapping.getDeobfuscatedPackage(); + final ASTRewrite rewrite = this.context.createASTRewrite(); + if (deobfPackage == null || deobfPackage.isEmpty()) { + // if we have annotations, those need to be moved to a new SimpleType node + final ASTNode nameNode; + if (node.isAnnotatable() && !node.annotations().isEmpty()) { + final SimpleType type = node.getName().getAST().newSimpleType((Name) rewrite.createCopyTarget(node.getName())); + transferAnnotations(node, type); + nameNode = type; + } else { + nameNode = node.getName(); + } + rewrite.replace(node, nameNode, null); + } else { + // qualified -> other qualified: + rewrite.set(node, NameQualifiedType.QUALIFIER_PROPERTY, node.getAST().newName(deobfPackage.replace('/', '.')), null); + } + node.getName().accept(this); + + return false; + } + + @Override + public boolean visit(PackageDeclaration node) { + String currentPackage = node.getName().getFullyQualifiedName(); + + if (this.context.getPackageName().isEmpty()) { + // remove package declaration if remapped to root package + this.context.createASTRewrite().remove(node, null); + } else if (!currentPackage.equals(this.context.getPackageName())) { + this.context.createASTRewrite().replace(node.getName(), node.getAST().newName(this.context.getPackageName()), null); + } + + return false; + } + + @Override + public boolean visit(ImportDeclaration node) { + if (node.isStatic()) { + // Remap class/member reference separately + return true; + } + + IBinding binding = node.resolveBinding(); + if (binding != null) { + switch (binding.getKind()) { + case IBinding.TYPE: + ITypeBinding typeBinding = (ITypeBinding) binding; + String name = typeBinding.getBinaryName(); + if (name == null) { + if (this.context.getMercury().isGracefulClasspathChecks()) { + return false; + } + throw new IllegalStateException("No binary name for " + typeBinding.getQualifiedName() + ". Did you add the library to the classpath?"); + } + + ClassMapping mapping = this.mappings.computeClassMapping(name).orElse(null); + if (mapping != null && !name.equals(mapping.getFullDeobfuscatedName().replace('/', '.'))) { + this.importRewrite.removeImport(typeBinding.getQualifiedName()); + } else if (this.simpleDeobfuscatedName != null && this.simpleDeobfuscatedName.equals(typeBinding.getName())) { + this.importRewrite.removeImport(typeBinding.getQualifiedName()); + } + + break; + } + } + return false; + } + + private void pushImportContext(ITypeBinding binding) { + ImportContext context = new ImportContext(this.importRewrite.getDefaultImportRewriteContext(), this.importStack.peek()); + collectImportContext(context, binding); + this.importStack.push(context); + } + + private void collectImportContext(ImportContext context, ITypeBinding binding) { + if (binding == null) { + return; + } + + // Names from inner classes + for (ITypeBinding inner : binding.getDeclaredTypes()) { + if (GracefulCheck.checkGracefully(this.context, inner)) { + continue; + } + + int modifiers = inner.getModifiers(); + if (Modifier.isPrivate(modifiers)) { + // Inner type must be declared in this compilation unit + if (this.context.getCompilationUnit().findDeclaringNode(inner) == null) { + continue; + } + } + + ClassMapping mapping = this.mappings.computeClassMapping(inner.getBinaryName()).orElse(null); + + if (isPackagePrivate(modifiers)) { + // Must come from the same package + String packageName = mapping != null ? mapping.getDeobfuscatedPackage() : inner.getPackage().getName(); + if (!packageName.replace('/', '.').equals(this.context.getPackageName().replace('/', '.'))) { + continue; + } + } + + String simpleName; + String qualifiedName; + if (mapping != null) { + simpleName = mapping.getSimpleDeobfuscatedName(); + qualifiedName = mapping.getFullDeobfuscatedName().replace('/', '.').replace('$', '.'); + } else { + simpleName = inner.getName(); + qualifiedName = inner.getBinaryName().replace('$', '.'); + } + + if (!context.conflicts.contains(simpleName)) { + String current = context.implicit.putIfAbsent(simpleName, qualifiedName); + if (current != null && !current.equals(qualifiedName)) { + context.implicit.remove(simpleName); + context.conflicts.add(simpleName); + } + } + } + + // Inherited names + collectImportContext(context, binding.getSuperclass()); + for (ITypeBinding parent : binding.getInterfaces()) { + collectImportContext(context, parent); + } + } + + @Override + public boolean visit(AnnotationTypeDeclaration node) { + pushImportContext(node.resolveBinding()); + return true; + } + + @Override + public boolean visit(AnonymousClassDeclaration node) { + pushImportContext(node.resolveBinding()); + return true; + } + + @Override + public boolean visit(EnumDeclaration node) { + pushImportContext(node.resolveBinding()); + return true; + } + + @Override + public boolean visit(TypeDeclaration node) { + pushImportContext(node.resolveBinding()); + return true; + } + + @Override + public void endVisit(AnnotationTypeDeclaration node) { + this.importStack.pop(); + } + + @Override + public void endVisit(AnonymousClassDeclaration node) { + this.importStack.pop(); + } + + @Override + public void endVisit(EnumDeclaration node) { + this.importStack.pop(); + } + + @Override + public void endVisit(TypeDeclaration node) { + this.importStack.pop(); + } + + private void transferAnnotations(final AnnotatableType oldNode, final AnnotatableType newNode) { + // we don't support type annotations, ignore + if (newNode.getAST().apiLevel() < AST.JLS8) { + return; + } + if (oldNode.annotations().isEmpty()) { + // none to transfer + return; + } + + // transfer and visit + final ListRewrite rewrite = this.context.createASTRewrite().getListRewrite(newNode, newNode.getAnnotationsProperty()); + for (Object annotation : oldNode.annotations()) { + final ASTNode annotationNode = (ASTNode) annotation; + annotationNode.accept(this); + rewrite.insertLast(annotationNode, null); + } + } + + private static class ImportContext extends ImportRewrite.ImportRewriteContext { + private final ImportRewrite.ImportRewriteContext defaultContext; + final Map implicit; + final Set conflicts; + + ImportContext(ImportRewrite.ImportRewriteContext defaultContext, ImportContext parent) { + this.defaultContext = defaultContext; + if (parent != null) { + this.implicit = new HashMap<>(parent.implicit); + this.conflicts = new HashSet<>(parent.conflicts); + } else { + this.implicit = new HashMap<>(); + this.conflicts = new HashSet<>(); + } + } + + @Override + public int findInContext(String qualifier, String name, int kind) { + int result = this.defaultContext.findInContext(qualifier, name, kind); + if (result != RES_NAME_UNKNOWN) { + return result; + } + + if (kind == KIND_TYPE) { + String current = implicit.get(name); + if (current != null) { + return current.equals(qualifier + '.' + name) ? RES_NAME_FOUND : RES_NAME_CONFLICT; + } + + if (conflicts.contains(name)) { + return RES_NAME_CONFLICT; // TODO + } + } + + return RES_NAME_UNKNOWN; + } + } + +} diff --git a/buildSrc/src/main/java/modified/mercury/SimpleRemapperVisitor.java b/buildSrc/src/main/java/modified/mercury/SimpleRemapperVisitor.java new file mode 100644 index 00000000..9174a474 --- /dev/null +++ b/buildSrc/src/main/java/modified/mercury/SimpleRemapperVisitor.java @@ -0,0 +1,496 @@ +/* + * Copyright (c) 2018 Cadix Development (https://www.cadixdev.org) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package modified.mercury; + +import static paper.libs.org.cadixdev.mercury.util.BombeBindings.convertSignature; + +import paper.libs.org.cadixdev.bombe.analysis.InheritanceProvider; +import paper.libs.org.cadixdev.bombe.type.signature.FieldSignature; +import paper.libs.org.cadixdev.bombe.type.signature.MethodSignature; +import paper.libs.org.cadixdev.lorenz.MappingSet; +import paper.libs.org.cadixdev.lorenz.model.ClassMapping; +import paper.libs.org.cadixdev.lorenz.model.FieldMapping; +import paper.libs.org.cadixdev.lorenz.model.InnerClassMapping; +import paper.libs.org.cadixdev.lorenz.model.MemberMapping; +import paper.libs.org.cadixdev.lorenz.model.MethodMapping; +import paper.libs.org.cadixdev.lorenz.model.MethodParameterMapping; +import paper.libs.org.cadixdev.mercury.RewriteContext; +import paper.libs.org.cadixdev.mercury.analysis.MercuryInheritanceProvider; +import paper.libs.org.cadixdev.mercury.util.GracefulCheck; +import paper.libs.org.eclipse.jdt.core.dom.ASTNode; +import paper.libs.org.eclipse.jdt.core.dom.ASTVisitor; +import paper.libs.org.eclipse.jdt.core.dom.Block; +import paper.libs.org.eclipse.jdt.core.dom.IBinding; +import paper.libs.org.eclipse.jdt.core.dom.IMethodBinding; +import paper.libs.org.eclipse.jdt.core.dom.ITypeBinding; +import paper.libs.org.eclipse.jdt.core.dom.IVariableBinding; +import paper.libs.org.eclipse.jdt.core.dom.LambdaExpression; +import paper.libs.org.eclipse.jdt.core.dom.MethodDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.SimpleName; +import paper.libs.org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import paper.libs.org.eclipse.jdt.core.dom.VariableDeclaration; +import paper.libs.org.eclipse.jdt.internal.compiler.lookup.PackageBinding; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; + +/** + * Remaps only methods, fields, and parameters. + */ +public class SimpleRemapperVisitor extends ASTVisitor { + + private static final String LVT_NAMES_PROPERTY = "org.cadixdev.mercury.lvtNames"; + private static final String LOCAL_VARIABLE_NAME_PROPERTY = "org.cadixdev.mercury.localVariableName"; + private static final String NEW_PARAM_NAMES_PROPERTY = "org.cadixdev.mercury.newParamNames"; + + final RewriteContext context; + final MappingSet mappings; + private final InheritanceProvider inheritanceProvider; + + public SimpleRemapperVisitor(RewriteContext context, MappingSet mappings, boolean javadoc) { + super(javadoc); + this.context = context; + this.mappings = mappings; + this.inheritanceProvider = MercuryInheritanceProvider.get(context.getMercury()); + } + + final void updateIdentifier(SimpleName node, String newName) { + if (!node.getIdentifier().equals(newName) && !node.isVar()) { + this.context.createASTRewrite().set(node, SimpleName.IDENTIFIER_PROPERTY, newName, null); + } + } + + private void remapMethod(SimpleName node, IMethodBinding binding) { + ITypeBinding declaringClass = binding.getDeclaringClass(); + if (GracefulCheck.checkGracefully(this.context, declaringClass)) { + return; + } + final ClassMapping classMapping = this.mappings.getOrCreateClassMapping(declaringClass.getBinaryName()); + + if (binding.isConstructor()) { + updateIdentifier(node, classMapping.getSimpleDeobfuscatedName()); + } else { + final MethodMapping mapping = findMethodMapping(declaringClass, binding); + if (mapping == null) { + return; + } + + updateIdentifier(node, mapping.getDeobfuscatedName()); + } + } + + private void remapField(SimpleName node, IVariableBinding binding) { + if (!binding.isField()) { + if (binding.isParameter()) { + remapParameter(node, binding); + } else { + checkLocalVariable(node, binding); + } + + return; + } + + ITypeBinding declaringClass = binding.getDeclaringClass(); + if (declaringClass == null) { + return; + } + + ClassMapping classMapping = this.mappings.getClassMapping(declaringClass.getBinaryName()).orElse(null); + if (classMapping == null) { + return; + } + + FieldSignature bindingSignature = convertSignature(binding); + FieldMapping mapping = findMemberMapping(bindingSignature, classMapping, ClassMapping::computeFieldMapping); + if (mapping == null) { + return; + } + + updateIdentifier(node, mapping.getDeobfuscatedName()); + } + + private MethodMapping findMethodMapping(ITypeBinding declaringClass, IMethodBinding declaringMethod) { + final ClassMapping classMapping = this.mappings.getClassMapping(declaringClass.getBinaryName()).orElse(null); + if (classMapping == null) { + return null; + } + + final MethodSignature methodSig = convertSignature(declaringMethod); + MethodMapping methodMapping = findMemberMapping(methodSig, classMapping, ClassMapping::getMethodMapping); + if (methodMapping == null) { + classMapping.complete(this.inheritanceProvider, declaringClass); + methodMapping = classMapping.getMethodMapping(methodSig).orElse(null); + } + + return methodMapping; + } + + private , M> T findMemberMapping( + M matcher, + ClassMapping classMapping, + BiFunction, M, Optional> getMapping + ) { + T mapping = getMapping.apply(classMapping, matcher).orElse(null); + if (mapping != null) { + return mapping; + } + + if (!this.context.getMercury().isFlexibleAnonymousClassMemberLookups()) { + return null; + } + return findMemberMappingAnonClass(matcher, classMapping, getMapping); + } + + private , M> T findMemberMappingAnonClass( + M matcher, + ClassMapping classMapping, + BiFunction, M, Optional> getMapping + ) { + // If neither name is different then this method won't do anything + if (Objects.equals(classMapping.getObfuscatedName(), classMapping.getDeobfuscatedName())) { + return null; + } + // Anonymous classes must be inner classes + if (!(classMapping instanceof InnerClassMapping)) { + return null; + } + // Verify this is inner class is anonymous + if (!classMapping.getObfuscatedName().chars().allMatch(Character::isDigit)) { + return null; + } + ClassMapping parentMapping = ((InnerClassMapping) classMapping).getParent(); + if (parentMapping == null) { + return null; + } + + // Find a sibling anonymous class whose obfuscated name is our deobfuscated name + ClassMapping otherClassMapping = parentMapping + .getInnerClassMapping(classMapping.getDeobfuscatedName()).orElse(null); + if (otherClassMapping != null) { + T mapping = getMapping.apply(otherClassMapping, matcher).orElse(null); + if (mapping != null) { + return mapping; + } + } + + // Find a sibling anonymous class whose deobfuscated name is our obfuscated name + // We have to do something a little less direct for this case + for (InnerClassMapping innerClassMapping : parentMapping.getInnerClassMappings()) { + if (Objects.equals(classMapping.getObfuscatedName(), innerClassMapping.getDeobfuscatedName())) { + otherClassMapping = innerClassMapping; + break; + } + } + if (otherClassMapping == null) { + return null; + } + return getMapping.apply(otherClassMapping, matcher).orElse(null); + } + + private void remapParameter(SimpleName node, IVariableBinding binding) { + IMethodBinding declaringMethod = binding.getDeclaringMethod(); + if (declaringMethod == null) { + return; + } + + int index = -1; + + ASTNode n = context.getCompilationUnit().findDeclaringNode(declaringMethod); + + if (n instanceof MethodDeclaration) { + MethodDeclaration methodDeclaration = (MethodDeclaration) n; + + @SuppressWarnings("unchecked") + List parameters = methodDeclaration.parameters(); + + for (int i = 0; i < parameters.size(); i++) { + if (binding.equals(parameters.get(i).resolveBinding())) { + index = i; + } + } + } + + if (index == -1) { + return; + } + + final ITypeBinding declaringClass = declaringMethod.getDeclaringClass(); + if (declaringClass == null) { + return; + } + + final MethodMapping methodMapping = findMethodMapping(declaringClass, declaringMethod); + if (methodMapping == null) { + return; + } + + methodMapping.getParameterMapping(index).ifPresent(paramMapping -> updateIdentifier(node, paramMapping.getDeobfuscatedName())); + } + + /** + * Check if a local variable needs to be renamed because it conflicts with a new parameter name. This will also + * attempt to check cases where local variables are defined in lambda expressions. + * + * @param node The local variable node to check + * @param binding The variable binding corresponding to the local variable name + */ + private void checkLocalVariable(SimpleName node, IVariableBinding binding) { + final ASTNode bindingNode = this.context.getCompilationUnit().findDeclaringNode(binding); + final String localVariableName = bindingNode != null ? (String) bindingNode.getProperty(LOCAL_VARIABLE_NAME_PROPERTY) : null; // modified + if (localVariableName != null) { + updateIdentifier(node, localVariableName); + return; + } + + IMethodBinding declaringMethod = binding.getDeclaringMethod(); + if (declaringMethod == null) { + return; + } + + if (declaringMethod.getDeclaringMember() != null) { + // lambda method + final LambdaExpression lambdaExpr = getLambdaMethodDeclaration(declaringMethod); + if (lambdaExpr == null) { + return; + } + + // Climb out of declaring stack until we find a method which isn't a lambda + IMethodBinding outerMethod = declaringMethod; + while (outerMethod.getDeclaringMember() instanceof IMethodBinding) { + outerMethod = (IMethodBinding) outerMethod.getDeclaringMember(); + } + if (outerMethod == declaringMethod) { + // lookup failed, nothing we can do + return; + } + final ASTNode n = this.context.getCompilationUnit().findDeclaringNode(outerMethod); + if (!(n instanceof MethodDeclaration)) { + return; + } + final MethodDeclaration outerDeclaration = (MethodDeclaration) n; + + ASTNode body = lambdaExpr.getBody(); + // might be an expression + if (!(body instanceof Block)) { + body = null; + } + this.checkLocalVariableWithMappings(node, bindingNode, outerMethod, outerDeclaration, declaringMethod, (Block) body); + } else { + final ASTNode n = context.getCompilationUnit().findDeclaringNode(declaringMethod); + if (!(n instanceof MethodDeclaration)) { + return; + } + final MethodDeclaration methodDeclaration = (MethodDeclaration) n; + + this.checkLocalVariableWithMappings(node, bindingNode, declaringMethod, methodDeclaration, declaringMethod, methodDeclaration.getBody()); + } + } + + /** + * Using the given mappings and bindings, check if there are mappings for the method and if any of them conflict + * with the given local variable name. + * + * @param node The local variable name to check + * @param bindingNode The binding of the local variable declaration + * @param binding The binding of the mapped method to check + * @param declaration The declaration node of the mapped method to check + * @param blockDeclaringMethod The method binding of the method which defines the {@code block} + * @param body The method body to check for local variables + */ + private void checkLocalVariableWithMappings( + SimpleName node, + ASTNode bindingNode, + IMethodBinding binding, + MethodDeclaration declaration, + IMethodBinding blockDeclaringMethod, + Block body + ) { + final ITypeBinding declaringClass = binding.getDeclaringClass(); + this.mappings.getClassMapping(declaringClass.getBinaryName()) + .flatMap(classMapping -> { + classMapping.complete(this.inheritanceProvider, declaringClass); + return classMapping.getMethodMapping(convertSignature(binding)); + }) + .ifPresent(methodMapping -> { + if (!methodMapping.getParameterMappings().isEmpty()) { + final Set newParamNames = newParamNames(declaration, methodMapping); + checkLocalVariableForConflicts(node, bindingNode, blockDeclaringMethod, body, newParamNames); + } + }); + } + + /** + * Check the method's body defined by {@code methodDeclaration} to collect all local variable names in order to + * find a suitable replacement name for {@code node} if it clashes with a name in {@code newParamNames}. + * + * @param node The local variable node to check + * @param bindingNode The binding of the local variable declaration + * @param blockDeclaringMethod The method binding of the method which defines the {@code block} + * @param block The method body implementation to collect local variable names from + * @param newParamNames The set of parameter names after mapping + */ + private void checkLocalVariableForConflicts( + SimpleName node, + ASTNode bindingNode, + IMethodBinding blockDeclaringMethod, + Block block, + Set newParamNames + ) { + final String name = node.getIdentifier(); + if (!newParamNames.contains(name)) { + return; + } + + // the new param name will screw up this local variable + final Set localVariableNames = collectLocalVariableNames(blockDeclaringMethod, block); + int counter = 1; + String newName = name + counter; + while (localVariableNames.contains(newName) || newParamNames.contains(newName)) { + counter++; + newName = name + counter; + } + + localVariableNames.add(newName); + bindingNode.setProperty(LOCAL_VARIABLE_NAME_PROPERTY, newName); + updateIdentifier(node, newName); + } + + /** + * Find the declaration of the actual method block for the lambda method + * + * @param declaringMethod The method binding for the lambda method to check + * @return The {@link MethodDeclaration} corresponding to the code block of the lambda implementation + */ + private LambdaExpression getLambdaMethodDeclaration(IMethodBinding declaringMethod) { + final ASTNode node = this.context.getCompilationUnit().findDeclaringNode(declaringMethod.getKey()); + if (node instanceof LambdaExpression) { + return (LambdaExpression) node; + } + return null; + } + + /** + * Read the method body of {@code methodDeclaration} and return the set of local variable names defined inside of + * it. The set is cached on the {@code methodDeclaration} so it is only computed once. + * + * @param blockDeclaringMethod The method binding of the method which defines the {@code block} + * @param block The method body implementation to check. + * @return The set of local variable names defined in the method body. + */ + private Set collectLocalVariableNames(IMethodBinding blockDeclaringMethod, Block block) { + if (block == null) { + return Collections.emptySet(); + } + + Set result = checkProperty(LVT_NAMES_PROPERTY, block); + if (result != null) { + return result; + } + result = new HashSet<>(); + block.setProperty(LVT_NAMES_PROPERTY, result); + + final IVariableBinding[] synthLocals = blockDeclaringMethod.getSyntheticOuterLocals(); + for (final IVariableBinding synthLocal : synthLocals) { + final String name = synthLocal.getName(); + if (name.startsWith("val$")) { + result.add(name.substring(4)); + } + } + + @SuppressWarnings("unchecked") + final List statements = (List) block.statements(); + for (final ASTNode statement : statements) { + if (!(statement instanceof VariableDeclaration)) { + continue; + } + final VariableDeclaration declaration = (VariableDeclaration) statement; + result.add(declaration.getName().getIdentifier()); + } + + return result; + } + + /** + * Check the parameter names defined by the {@code methodDeclaration} and apply any mappings based on the given + * {@code mapping}. Return the list of params post-remap. + * + * @param methodDeclaration The method declaration to check the parameter names on. + * @param mapping The mapping to use to determine the new parameter names + * @return The set of parameter names after remapping them with {@code mapping}. + */ + private Set newParamNames(MethodDeclaration methodDeclaration, MethodMapping mapping) { + Set result = checkProperty(NEW_PARAM_NAMES_PROPERTY, methodDeclaration); + if (result != null) { + return result; + } + result = new HashSet<>(); + methodDeclaration.setProperty(NEW_PARAM_NAMES_PROPERTY, result); + + @SuppressWarnings("unchecked") + List parameters = methodDeclaration.parameters(); + for (int i = 0; i < parameters.size(); i++) { + final Optional paramMapping = mapping.getParameterMapping(i); + if (paramMapping.isPresent()) { + result.add(paramMapping.get().getDeobfuscatedName()); + } else { + result.add(parameters.get(i).getName().getIdentifier()); + } + } + + return result; + } + + /** + * Check if the given node contains a {@code Set} property named {@code propName} and return it if so. + * Returns {@code null} if not. + * + * @param propName The name of the property + * @param node The node to check the property on + * @return The set stored on the node or {@code null} if empty + */ + private static Set checkProperty(String propName, ASTNode node) { + if (node == null) { + return null; + } + final Object value = node.getProperty(propName); + if (value instanceof Set) { + @SuppressWarnings("unchecked") final Set result = (Set) value; + return result; + } + return null; + } + + protected void visit(SimpleName node, IBinding binding) { + switch (binding.getKind()) { + case IBinding.METHOD: + remapMethod(node, ((IMethodBinding) binding).getMethodDeclaration()); + break; + case IBinding.VARIABLE: + remapField(node, ((IVariableBinding) binding).getVariableDeclaration()); + break; + } + } + + @Override + public final boolean visit(SimpleName node) { + IBinding binding = node.resolveBinding(); + if (binding != null) { + visit(node, binding); + } + return false; + } + +} diff --git a/buildSrc/src/main/kotlin/MercuryRemapper.kt b/buildSrc/src/main/kotlin/MercuryRemapper.kt new file mode 100644 index 00000000..86a67ec2 --- /dev/null +++ b/buildSrc/src/main/kotlin/MercuryRemapper.kt @@ -0,0 +1,42 @@ +import modified.mercury.RemapperVisitor +import modified.mercury.SimpleRemapperVisitor +import paper.libs.org.cadixdev.lorenz.MappingSet +import paper.libs.org.cadixdev.mercury.RewriteContext +import paper.libs.org.cadixdev.mercury.SourceRewriter +import java.util.Objects + +class MercuryRemapper private constructor(mappings: MappingSet, simple: Boolean, javadoc: Boolean) : SourceRewriter { + private val mappings: MappingSet + private val simple: Boolean + private val javadoc: Boolean + + init { + this.mappings = Objects.requireNonNull(mappings, "mappings") + this.simple = simple + this.javadoc = javadoc + } + + override fun getFlags(): Int = SourceRewriter.FLAG_RESOLVE_BINDINGS + + override fun rewrite(context: RewriteContext) { + context.compilationUnit.accept(if (simple) SimpleRemapperVisitor(context, mappings, this.javadoc) else RemapperVisitor(context, mappings, this.javadoc)) + } + + companion object { + fun create(mappings: MappingSet): SourceRewriter { + return MercuryRemapper(mappings, false, true) + } + + fun create(mappings: MappingSet, javadoc: Boolean): SourceRewriter { + return MercuryRemapper(mappings, false, javadoc) + } + + fun createSimple(mappings: MappingSet): SourceRewriter { + return MercuryRemapper(mappings, true, true) + } + + fun createSimple(mappings: MappingSet, javadoc: Boolean): SourceRewriter { + return MercuryRemapper(mappings, true, javadoc) + } + } +} diff --git a/buildSrc/src/main/kotlin/RemapPluginSources.kt b/buildSrc/src/main/kotlin/RemapPluginSources.kt new file mode 100644 index 00000000..b16c9252 --- /dev/null +++ b/buildSrc/src/main/kotlin/RemapPluginSources.kt @@ -0,0 +1,98 @@ +import io.papermc.paperweight.tasks.JavaLauncherTask +import io.papermc.paperweight.tasks.RemapSources +import io.papermc.paperweight.util.MappingFormats +import io.papermc.paperweight.util.constants.DEOBF_NAMESPACE +import io.papermc.paperweight.util.constants.SPIGOT_NAMESPACE +import io.papermc.paperweight.util.deleteRecursive +import io.papermc.paperweight.util.path +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CompileClasspath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.submit +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import paper.libs.org.cadixdev.mercury.Mercury +import paper.libs.org.eclipse.jdt.core.JavaCore +import java.nio.file.Files +import javax.inject.Inject + +abstract class RemapPluginSources : JavaLauncherTask() { + @get:CompileClasspath + abstract val remapClasspath: ConfigurableFileCollection + + @get:InputDirectory + abstract val inputSources: DirectoryProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:InputFile + abstract val mappingsFile: RegularFileProperty + + @get:Input + abstract val addExplicitThis: Property + + @get:Inject + abstract val workerExecutor: WorkerExecutor + + override fun init() { + super.init() + addExplicitThis.convention(false) + } + + @TaskAction + fun run() { + val queue = workerExecutor.processIsolation { + forkOptions.jvmArgs("-Xmx2G") + forkOptions.executable(launcher.get().executablePath.path.toAbsolutePath().toString()) + } + + queue.submit(RemapPluginSourcesAction::class) { + remapClasspath.setFrom(this@RemapPluginSources.remapClasspath) + inputSources.set(this@RemapPluginSources.inputSources) + outputDir.set(this@RemapPluginSources.outputDir) + mappingsFile.set(this@RemapPluginSources.mappingsFile) + addExplicitThis.set(this@RemapPluginSources.addExplicitThis) + } + } + + abstract class RemapPluginSourcesAction : WorkAction { + interface Params : WorkParameters { + val remapClasspath: ConfigurableFileCollection + val inputSources: DirectoryProperty + val outputDir: DirectoryProperty + val mappingsFile: RegularFileProperty + val addExplicitThis: Property + } + + override fun execute() { + val mappingSet = MappingFormats.TINY.read( + parameters.mappingsFile.path, + SPIGOT_NAMESPACE, + DEOBF_NAMESPACE + ) + + val merc = Mercury() + merc.isGracefulClasspathChecks = true + merc.sourceCompatibility = JavaCore.VERSION_17 + merc.classPath.addAll(parameters.remapClasspath.map { it.toPath() }) + if (parameters.addExplicitThis.get()) { + merc.processors.add(RemapSources.ExplicitThisAdder) + } + merc.processors.add(MercuryRemapper.create(mappingSet, false)) + + parameters.outputDir.path.deleteRecursive() + Files.createDirectories(parameters.outputDir.path) + + merc.rewrite(parameters.inputSources.path, parameters.outputDir.path) + } + } +} diff --git a/buildSrc/src/main/kotlin/ReverseMappings.kt b/buildSrc/src/main/kotlin/ReverseMappings.kt new file mode 100644 index 00000000..96c4e32b --- /dev/null +++ b/buildSrc/src/main/kotlin/ReverseMappings.kt @@ -0,0 +1,43 @@ +import io.papermc.paperweight.util.MappingFormats +import io.papermc.paperweight.util.constants.DEOBF_NAMESPACE +import io.papermc.paperweight.util.constants.SPIGOT_NAMESPACE +import io.papermc.paperweight.util.path +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.nio.file.Files + +abstract class ReverseMappings : io.papermc.paperweight.tasks.BaseTask() { + @get:InputFile + abstract val inputMappings: RegularFileProperty + + @get:OutputFile + abstract val outputMappings: RegularFileProperty + + @get:Input + abstract val fromNs: Property + + @get:Input + abstract val toNs: Property + + @TaskAction + fun run() { + val mappingSet = MappingFormats.TINY.read( + inputMappings.path, + fromNs.get(), + toNs.get() + ).reverse() + + Files.deleteIfExists(outputMappings.path) + Files.createDirectories(outputMappings.path.parent) + MappingFormats.TINY.write( + mappingSet, + outputMappings.path, + toNs.get(), + fromNs.get() + ) + } +} diff --git a/buildSrc/src/main/kotlin/remap-plugin-src.gradle.kts b/buildSrc/src/main/kotlin/remap-plugin-src.gradle.kts new file mode 100644 index 00000000..3ee11611 --- /dev/null +++ b/buildSrc/src/main/kotlin/remap-plugin-src.gradle.kts @@ -0,0 +1,46 @@ +import io.papermc.paperweight.tasks.RemapJar +import io.papermc.paperweight.util.constants.DEOBF_NAMESPACE +import io.papermc.paperweight.util.constants.SPIGOT_NAMESPACE +import io.papermc.paperweight.util.registering + +plugins { + java + id("io.papermc.paperweight.userdev") +} + +tasks { + val reverseMappings by registering { + inputMappings.set(layout.projectDirectory.file(".gradle/caches/paperweight/setupCache/extractDevBundle.dir/data/mojang+yarn-spigot-reobf.tiny")) + outputMappings.set(layout.buildDirectory.file("reversed-reobf-mappings.tiny")) + + fromNs.set(DEOBF_NAMESPACE) + toNs.set(SPIGOT_NAMESPACE) + } + + val reobfPaperJar by tasks.registering { + // won't be runnable as we don't fixjarforreobf + // pretty hacky but should work + inputJar.set(layout.file(configurations.mojangMappedServerRuntime.flatMap { + it.elements.map { elements -> + elements.filter { element -> + val p = element.asFile.absolutePath + p.contains("ivyRepository") && p.contains("paper-server-userdev") + }.single().asFile + } + })) + outputJar.set(layout.buildDirectory.file("reobfPaper.jar")) + fromNamespace.set(DEOBF_NAMESPACE) + toNamespace.set(SPIGOT_NAMESPACE) + mappingsFile.set(reverseMappings.flatMap { it.outputMappings }) + remapper.from(configurations.remapper) + } + + @Suppress("unused_variable") + val remapPluginSources by registering { + remapClasspath.from(reobfPaperJar.flatMap { it.outputJar }) + remapClasspath.from(configurations.compileClasspath) + inputSources.set(layout.projectDirectory.dir("src/main/java")) + outputDir.set(layout.projectDirectory.dir("src/main/mojangMappedJava")) + mappingsFile.set(reverseMappings.flatMap { it.outputMappings }) + } +}