org.spongepowered.asm.mixin.transformer.MixinPreProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.spongepowered.asm.mixin.transformer.MixinPreProcessor.java

Source

/*
 * This file is part of Sponge, licensed under the MIT License (MIT).
 *
 * Copyright (c) SpongePowered.org <http://www.spongepowered.org>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.spongepowered.asm.mixin.transformer;

import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.transformer.ClassInfo.Field;
import org.spongepowered.asm.mixin.transformer.ClassInfo.Method;
import org.spongepowered.asm.util.ASMHelper;
import org.spongepowered.asm.util.Constants;

/**
 * <p>Mixin bytecode pre-processor. This class is responsible for bytecode pre-
 * processing tasks required to be performed on mixin bytecode before the mixin
 * can be applied. In previous versions the duties performed by this class were
 * performed by {@link MixinInfo}.</p>
 * 
 * <p>Before a mixin can be applied to the target class, it is necessary to
 * convert certain aspects of the mixin bytecode into the intended final form of
 * the mixin, this involves for example stripping the prefix from shadow and
 * soft-implemented methods. This preparation is done in two stages: first the
 * target-context-insensitive transformations are applied (this also acts as a
 * validation pass when the mixin is first loaded) and then transformations
 * which depend on the target class are applied in a second stage.</p>
 * 
 * <p>The validation pass propagates method renames into the metadata tree and
 * thus changes made during this phase are visible to all other mixins. The
 * target-context-sensitive pass on the other hand can only operate on private
 * class members for obvious reasons.</p>  
 */
class MixinPreProcessor {

    /**
     * The mixin
     */
    private final MixinInfo mixin;

    /**
     * Mixin class node
     */
    private final ClassNode classNode;

    private boolean prepared, attached;

    MixinPreProcessor(MixinInfo mixin, ClassNode classNode) {
        this.mixin = mixin;
        this.classNode = classNode;
    }

    /**
     * Run the first pass. Propagates changes into the metadata tree.
     * 
     * @return Prepared classnode
     */
    ClassNode prepare() {
        if (!this.prepared) {
            this.prepared = true;

            for (MethodNode mixinMethod : this.classNode.methods) {
                Method method = this.mixin.getClassInfo().findMethod(mixinMethod);
                this.prepareShadow(mixinMethod, method);
                this.prepareSoftImplements(mixinMethod, method);
            }
        }

        return this.classNode;
    }

    private void prepareShadow(MethodNode mixinMethod, Method method) {
        AnnotationNode shadowAnnotation = ASMHelper.getVisibleAnnotation(mixinMethod, Shadow.class);
        if (shadowAnnotation == null) {
            return;
        }

        String prefix = ASMHelper.<String>getAnnotationValue(shadowAnnotation, "prefix", Shadow.class);
        if (mixinMethod.name.startsWith(prefix)) {
            ASMHelper.setVisibleAnnotation(mixinMethod, MixinRenamed.class, "originalName", mixinMethod.name);
            String newName = mixinMethod.name.substring(prefix.length());
            method.renameTo(newName);
            mixinMethod.name = newName;
        }
    }

    private void prepareSoftImplements(MethodNode mixinMethod, Method method) {
        for (InterfaceInfo iface : this.mixin.getSoftImplements()) {
            if (iface.renameMethod(mixinMethod)) {
                method.renameTo(mixinMethod.name);
            }
        }
    }

    MixinTargetContext createContextFor(ClassNode target) {
        this.prepare();
        MixinTargetContext context = new MixinTargetContext(this.mixin, this.classNode, target);
        this.attach(context);
        return context;
    }

    /**
     * Run the second pass, attach to the specified context
     * 
     * @param context
     */
    void attach(MixinTargetContext context) {
        if (this.attached) {
            throw new IllegalStateException("Preprocessor was already attached");
        }

        this.attached = true;

        // Perform context-sensitive attachment phase
        this.attachMethods(context);
        this.attachFields(context);

        // Apply transformations to the mixin bytecode
        this.transform(context);
    }

    private void attachMethods(MixinTargetContext context) {
        for (MethodNode mixinMethod : this.classNode.methods) {
            AnnotationNode shadowAnnotation = ASMHelper.getVisibleAnnotation(mixinMethod, Shadow.class);
            if (shadowAnnotation == null) {
                continue;
            }

            Method method = this.mixin.getClassInfo().findMethod(mixinMethod, true);
            MethodNode target = MixinPreProcessor.findMethod(context.getTargetClass(), mixinMethod,
                    shadowAnnotation);

            if (target == null) {
                throw new InvalidMixinException(this.mixin,
                        "Shadow method " + mixinMethod.name + " was not located in the target class");
            }

            if (Constants.INIT.equals(target.name)) {
                throw new InvalidMixinException(this.mixin, "Nice try! Cannot alias a constructor!");
            }

            if (!target.name.equals(mixinMethod.name)) {
                if ((target.access & Opcodes.ACC_PRIVATE) == 0) {
                    throw new InvalidMixinException(this.mixin,
                            "Non-private method cannot be aliased. Found " + target.name);
                }

                mixinMethod.name = target.name;
                method.renameTo(target.name);
            }
        }
    }

    private void attachFields(MixinTargetContext context) {
        for (Iterator<FieldNode> iter = this.classNode.fields.iterator(); iter.hasNext();) {
            FieldNode mixinField = iter.next();
            AnnotationNode shadow = ASMHelper.getVisibleAnnotation(mixinField, Shadow.class);
            if (!this.validateField(context, mixinField, shadow)) {
                iter.remove();
                continue;
            }

            context.transformDescriptor(mixinField);

            Field field = this.mixin.getClassInfo().findField(mixinField);
            FieldNode target = this.findField(context.getTargetClass(), mixinField, shadow);
            if (target == null) {
                // If this field is a shadow field but is NOT found in the target class, that's bad, mmkay
                if (shadow != null) {
                    throw new InvalidMixinException(this.mixin,
                            "Shadow field " + mixinField.name + " was not located in the target class");
                }
            } else {
                // Check that the shadow field has a matching descriptor
                if (!target.desc.equals(mixinField.desc)) {
                    throw new InvalidMixinException(this.mixin,
                            "The field " + mixinField.name + " in the target class has a conflicting signature");
                }

                if (!target.name.equals(mixinField.name)) {
                    if ((target.access & Opcodes.ACC_PRIVATE) == 0) {
                        throw new InvalidMixinException(this.mixin,
                                "Non-private field cannot be aliased. Found " + target.name);
                    }

                    mixinField.name = target.name;
                    field.renameTo(target.name);
                }

                // Shadow fields get stripped from the mixin class
                iter.remove();
            }
        }
    }

    private boolean validateField(MixinTargetContext context, FieldNode field, AnnotationNode shadow) {
        // Public static fields will fall foul of early static binding in java, including them in a mixin is an error condition
        if (MixinTransformer.hasFlag(field, Opcodes.ACC_STATIC)
                && !MixinTransformer.hasFlag(field, Opcodes.ACC_PRIVATE)) {
            throw new InvalidMixinException(context, String
                    .format("Mixin classes cannot contain visible static methods or fields, found %s", field.name));
        }

        // Shadow fields can't have prefixes, it's meaningless for them anyway
        String prefix = ASMHelper.<String>getAnnotationValue(shadow, "prefix", Shadow.class);
        if (field.name.startsWith(prefix)) {
            throw new InvalidMixinException(context, String.format(
                    "Shadow field %s in %s has a shadow prefix. This is not allowed.", field.name, context));
        }

        // Imaginary super fields get stripped from the class, but first we validate them
        if (Constants.IMAGINARY_SUPER.equals(field.name)) {
            if (field.access != Opcodes.ACC_PRIVATE) {
                throw new InvalidMixinException(this.mixin,
                        "Imaginary super field " + field.name + " must be private and non-final");
            }
            if (!field.desc.equals("L" + this.mixin.getClassRef() + ";")) {
                throw new InvalidMixinException(this.mixin,
                        "Imaginary super field " + field.name + " must have the same type as the parent mixin");
            }
            return false;
        }

        return true;
    }

    /**
     * Apply discovered method and field renames to method invocations and field
     * accesses in the mixin
     */
    private void transform(MixinTargetContext context) {
        for (MethodNode mixinMethod : this.classNode.methods) {
            for (Iterator<AbstractInsnNode> iter = mixinMethod.instructions.iterator(); iter.hasNext();) {
                AbstractInsnNode insn = iter.next();
                if (insn instanceof MethodInsnNode) {
                    MethodInsnNode methodNode = (MethodInsnNode) insn;
                    Method method = this.mixin.getClassInfo().findMethodInHierarchy(methodNode, true, true);
                    if (method != null && method.isRenamed()) {
                        methodNode.name = method.getName();
                    }
                } else if (insn instanceof FieldInsnNode) {
                    FieldInsnNode fieldNode = (FieldInsnNode) insn;
                    Field field = this.mixin.getClassInfo().findField(fieldNode, true);
                    if (field != null && field.isRenamed()) {
                        fieldNode.name = field.getName();
                    }
                }
            }
        }
    }

    private static MethodNode findMethod(ClassNode classNode, MethodNode method, AnnotationNode shadow) {
        Deque<String> aliases = new LinkedList<String>();
        aliases.add(method.name);
        if (shadow != null) {
            List<String> aka = ASMHelper.<List<String>>getAnnotationValue(shadow, "aliases");
            if (aka != null) {
                aliases.addAll(aka);
            }
        }

        return MixinPreProcessor.findMethod(classNode, aliases, method.desc);
    }

    private static MethodNode findMethod(ClassNode classNode, Deque<String> aliases, String desc) {
        String alias = aliases.poll();
        if (alias == null) {
            return null;
        }

        for (MethodNode target : classNode.methods) {
            if (target.name.equals(alias) && target.desc.equals(desc)) {
                return target;
            }
        }

        return MixinPreProcessor.findMethod(classNode, aliases, desc);
    }

    private FieldNode findField(ClassNode classNode, FieldNode field, AnnotationNode shadow) {
        Deque<String> aliases = new LinkedList<String>();
        aliases.add(field.name);
        if (shadow != null) {
            List<String> aka = ASMHelper.<List<String>>getAnnotationValue(shadow, "aliases");
            if (aka != null) {
                aliases.addAll(aka);
            }
        }

        return this.findField(classNode, aliases, field.desc);
    }

    /**
     * Finds a field in the target class
     * 
     * @param aliases 
     * @param desc
     * @return Target field  or null if not found
     */
    private FieldNode findField(ClassNode classNode, Deque<String> aliases, String desc) {
        String alias = aliases.poll();
        if (alias == null) {
            return null;
        }

        for (FieldNode target : classNode.fields) {
            if (target.name.equals(alias) && target.desc.equals(desc)) {
                return target;
            }
        }

        return this.findField(classNode, aliases, desc);
    }
}