aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/replaymod/gradle/remap/PsiMapper.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/replaymod/gradle/remap/PsiMapper.java')
-rw-r--r--src/main/java/com/replaymod/gradle/remap/PsiMapper.java396
1 files changed, 396 insertions, 0 deletions
diff --git a/src/main/java/com/replaymod/gradle/remap/PsiMapper.java b/src/main/java/com/replaymod/gradle/remap/PsiMapper.java
new file mode 100644
index 0000000..17d0783
--- /dev/null
+++ b/src/main/java/com/replaymod/gradle/remap/PsiMapper.java
@@ -0,0 +1,396 @@
+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 com.replaymod.gradle.remap.Transformer.Mapping;
+
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+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 Map<String, Mapping> map;
+ private final Map<String, Mapping> mixinMappings = new HashMap<>();
+ private final PsiFile file;
+ private boolean error;
+ private TreeMap<TextRange, String> changes = new TreeMap<>(Comparator.comparing(TextRange::getStartOffset));
+
+ PsiMapper(Map<String, Mapping> map, PsiFile file) {
+ this.map = map;
+ this.file = file;
+ }
+
+ 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;
+ Mapping mapping = this.mixinMappings.get(name);
+ if (mapping == null) {
+ mapping = map.get(name);
+ }
+ if (mapping == null) return;
+ String mapped = mapping.fields.get(field.getName());
+ 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
+ ) {
+ int line = StringUtil.offsetToLineNumber(file.getText(), expr.getTextOffset());
+ System.err.println(file.getName() + ":" + line + ": 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.");
+ error = true;
+ }
+ }
+
+ private void map(PsiElement expr, PsiMethod method) {
+ PsiClass declaringClass = method.getContainingClass();
+ if (declaringClass == null) return;
+ ArrayDeque<PsiClass> parentQueue = new ArrayDeque<>();
+ parentQueue.offer(declaringClass);
+ Mapping mapping = null;
+
+ String name = declaringClass.getQualifiedName();
+ if (name != null) {
+ mapping = mixinMappings.get(name);
+ }
+ while (true) {
+ if (mapping != null) {
+ String mapped = mapping.methods.get(method.getName());
+ 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.get(name);
+ }
+ }
+ }
+
+ private void map(PsiElement expr, PsiQualifiedNamedElement resolved) {
+ String name = resolved.getQualifiedName();
+ if (name == null) return;
+ Mapping mapping = map.get(name);
+ if (mapping == null) return;
+ String mapped = mapping.newName;
+ if (mapped.equals(name)) return;
+
+ 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(Mapping 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.fields.get(target);
+ 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(Mapping 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 = mapping.methods.get(literalValue);
+ if (mapped != null && !mapped.equals(literalValue)) {
+ PsiAnnotationMemberValue value = attribute.getValue();
+ assert value != null;
+ replace(value, '"' + mapped + '"');
+ }
+ }
+ }
+ });
+ }
+
+ private Mapping remapInternalType(String internalType, StringBuilder result) {
+ if (internalType.charAt(0) == 'L') {
+ String type = internalType.substring(1, internalType.length() - 1).replace('/', '.');
+ Mapping mapping = map.get(type);
+ if (mapping != null) {
+ result.append('L').append(mapping.newName.replace('.', '/')).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);
+ Mapping mapping = remapInternalType(owner, builder);
+ String mapped = null;
+ if (mapping != null) {
+ mapped = (method ? mapping.methods : mapping.fields).get(name);
+ }
+ 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;
+
+ Mapping mapping = map.get(target.getQualifiedName());
+ if (mapping == null) return;
+
+ mixinMappings.put(psiClass.getQualifiedName(), mapping);
+
+ if (!mapping.fields.isEmpty()) {
+ remapAccessors(mapping);
+ }
+ if (!mapping.methods.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());
+ }
+}