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

Java tutorial

Introduction

Here is the source code for org.spongepowered.asm.mixin.transformer.MixinInfo.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.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import net.minecraft.launchwrapper.Launch;
import net.minecraft.launchwrapper.LaunchClassLoader;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.spongepowered.asm.mixin.Implements;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import org.spongepowered.asm.mixin.transformer.ClassInfo.Method;
import org.spongepowered.asm.util.ASMHelper;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.collect.Lists;

/**
 * Runtime information bundle about a mixin
 */
class MixinInfo extends TreeInfo implements Comparable<MixinInfo>, IMixinInfo {

    /**
     * Global order of mixin infos, used to determine ordering between mixins
     * with equivalent priority
     */
    static int mixinOrder = 0;

    static final Set<String> invalidClasses = MixinInfo.$getInvalidClassesSet();

    /**
     * Logger
     */
    private final transient Logger logger = LogManager.getLogger("mixin");

    /**
     * Parent configuration which declares this mixin 
     */
    private final transient MixinConfig parent;

    /**
     * Simple name 
     */
    private final String name;

    /**
     * Name of the mixin class itself, dotted notation
     */
    private final String className;

    /**
     * Mixin ClassInfo
     */
    private final transient ClassInfo classInfo;

    /**
     * Mixin priority, read from the {@link Mixin} annotation on the mixin class
     */
    private final int priority;

    /**
     * Mixin targets, read from the {@link Mixin} annotation on the mixin class
     */
    private final List<ClassInfo> targetClasses;

    /**
     * Names of target classes 
     */
    private final List<String> targetClassNames;

    /**
     * Intrinsic order (for sorting mixins with identical priority)
     */
    private final transient int order = MixinInfo.mixinOrder++;

    /**
     * Mixin bytes (read once, generate tree on demand)
     */
    private final transient byte[] mixinBytes;

    /**
     * Configuration plugin
     */
    private final transient IMixinConfigPlugin plugin;

    /**
     * All interfaces implemented by this mixin, including soft implementations
     */
    private final transient Set<String> interfaces = new HashSet<String>();

    /**
     * Interfaces soft-implemented using {@link Implements} 
     */
    private final transient List<InterfaceInfo> softImplements = new ArrayList<InterfaceInfo>();

    /**
     * Initial ClassNode created for mixin validation, not used for actual
     * application 
     */
    private ClassNode validationClassNode;

    /**
     * True if the superclass of the mixin is <b>not</b> the direct superclass
     * of one or more targets 
     */
    private boolean detachedSuper;

    /**
     * Internal ctor, called by {@link MixinConfig}
     * 
     * @param parent
     * @param mixinName
     * @param runTransformers
     * @param plugin 
     * @param suppressPlugin 
     * @throws ClassNotFoundException 
     */
    MixinInfo(MixinConfig parent, String mixinName, boolean runTransformers, IMixinConfigPlugin plugin,
            boolean suppressPlugin) throws ClassNotFoundException {
        this.parent = parent;
        this.name = mixinName;
        this.className = parent.getMixinPackage() + mixinName;
        this.plugin = plugin;

        // Read the class bytes and transform
        this.mixinBytes = this.loadMixinClass(this.className, runTransformers);

        ClassNode classNode = this.getClassNode(0);
        this.classInfo = ClassInfo.fromClassNode(classNode);
        this.priority = this.readPriority(classNode);
        this.targetClasses = this.readTargetClasses(classNode, suppressPlugin);
        this.targetClassNames = Collections
                .unmodifiableList(Lists.transform(this.targetClasses, Functions.toStringFunction()));
        this.validationClassNode = classNode;
    }

    void validate() {
        this.detachedSuper = this.validateTargetClasses(this.validationClassNode);
        this.validateMixin(this.validationClassNode);
        this.readImplementations(this.validationClassNode);
        this.prepare(this.validationClassNode);
        this.validationClassNode = null;
    }

    /**
     * Read the target class names from the {@link Mixin} annotation
     * 
     * @param classNode
     * @param suppressPlugin 
     * @return
     */
    private List<ClassInfo> readTargetClasses(ClassNode classNode, boolean suppressPlugin) {
        AnnotationNode mixin = ASMHelper.getInvisibleAnnotation(classNode, Mixin.class);
        if (mixin == null) {
            throw new InvalidMixinException(this,
                    String.format("The mixin '%s' is missing an @Mixin annotation", this.className));
        }

        List<ClassInfo> targets = new ArrayList<ClassInfo>();
        List<Type> publicTargets = ASMHelper.getAnnotationValue(mixin, "value");
        List<String> privateTargets = ASMHelper.getAnnotationValue(mixin, "targets");

        if (publicTargets != null) {
            this.readTargets(targets, Lists.transform(publicTargets, new Function<Type, String>() {
                @Override
                public String apply(Type input) {
                    return input.getClassName();
                };
            }), suppressPlugin, false);
        }

        if (privateTargets != null) {
            this.readTargets(targets, privateTargets, suppressPlugin, true);
        }

        return targets;
    }

    /**
     * Reads a target list into the outTargets list
     */
    private void readTargets(List<ClassInfo> outTargets, List<String> inTargets, boolean suppressPlugin,
            boolean checkPublic) {
        for (String targetClassName : inTargets) {
            targetClassName = targetClassName.replace('/', '.');
            if (this.plugin == null || suppressPlugin
                    || this.plugin.shouldApplyMixin(targetClassName, this.className)) {
                ClassInfo targetInfo = ClassInfo.forName(targetClassName);
                if (targetInfo.isInterface()) {
                    throw new InvalidMixinException(this,
                            "@Mixin target " + targetClassName + " is an interface in " + this);
                }
                if (checkPublic && targetInfo.isPublic()) {
                    throw new InvalidMixinException(this, "@Mixin target " + targetClassName + " is public in "
                            + this + " and must be specified in value");
                }
                if (!outTargets.contains(targetInfo)) {
                    outTargets.add(targetInfo);
                    targetInfo.addMixin(this);
                }
            }
        }
    }

    /**
     * Read the priority from the {@link Mixin} annotation
     * 
     * @param classNode
     * @return
     */
    private int readPriority(ClassNode classNode) {
        AnnotationNode mixin = ASMHelper.getInvisibleAnnotation(classNode, Mixin.class);
        if (mixin == null) {
            throw new InvalidMixinException(this,
                    String.format("The mixin '%s' is missing an @Mixin annotation", this.className));
        }

        Integer priority = ASMHelper.getAnnotationValue(mixin, "priority");
        return priority == null ? 1000 : priority.intValue();
    }

    private boolean validateTargetClasses(ClassNode classNode) {
        boolean detached = false;

        for (ClassInfo targetClass : this.targetClasses) {

            if (classNode.superName.equals(targetClass.getSuperName())) {
                continue;
            }

            if (!targetClass.hasSuperClass(classNode.superName, ClassInfo.Traversal.IMMEDIATE)) {
                throw new InvalidMixinException(this,
                        "Super class '" + classNode.superName.replace('/', '.') + "' of " + this.name
                                + " was not found in the hierarchy of target class '" + targetClass + "'");
            }

            detached = true;
        }

        return detached;
    }

    /**
     * Performs pre-flight checks on the mixin
     * 
     * @param classNode
     */
    private void validateMixin(ClassNode classNode) {
        // isInner (shouldn't) return true for static inner classes
        if (this.classInfo.isInner()) {
            throw new InvalidMixinException(this, "Inner class mixin must be declared static");
        }

        // Can't have remappable fields or methods on a multi-target mixin, because after obfuscation the fields will remap to conflicting names
        if (this.targetClasses.size() > 1) {
            for (FieldNode field : classNode.fields) {
                this.checkRemappable(Shadow.class, field.name, ASMHelper.getVisibleAnnotation(field, Shadow.class));
            }

            for (MethodNode method : classNode.methods) {
                this.checkRemappable(Shadow.class, method.name,
                        ASMHelper.getVisibleAnnotation(method, Shadow.class));
                AnnotationNode overwrite = ASMHelper.getVisibleAnnotation(method, Overwrite.class);
                if (overwrite != null && ((method.access & Opcodes.ACC_STATIC) == 0
                        || (method.access & Opcodes.ACC_PUBLIC) == 0)) {
                    throw new InvalidMixinException(this,
                            "Found @Overwrite annotation on " + method.name + " in " + this);
                }
            }
        }
    }

    private void checkRemappable(Class<Shadow> annotationClass, String name, AnnotationNode annotation) {
        if (annotation != null && ASMHelper.getAnnotationValue(annotation, "remap", Boolean.TRUE)) {
            throw new InvalidMixinException(this, "Found a remappable @" + annotationClass.getSimpleName()
                    + " annotation on " + name + " in " + this);
        }
    }

    /**
     * Prepare the mixin, applies any pre-processing transformations
     */
    private ClassNode prepare(ClassNode classNode) {
        this.findRenamedMethods(classNode);
        this.transformMethods(classNode);
        return classNode;
    }

    /**
     * Read and process any {@link Implements} annotations on the mixin
     */
    private void readImplementations(ClassNode classNode) {
        this.interfaces.addAll(classNode.interfaces);

        AnnotationNode implementsAnnotation = ASMHelper.getInvisibleAnnotation(classNode, Implements.class);
        if (implementsAnnotation == null) {
            return;
        }

        List<AnnotationNode> interfaces = ASMHelper.getAnnotationValue(implementsAnnotation);
        if (interfaces == null) {
            return;
        }

        for (AnnotationNode interfaceNode : interfaces) {
            InterfaceInfo interfaceInfo = InterfaceInfo.fromAnnotation(this, interfaceNode);
            this.softImplements.add(interfaceInfo);
            this.interfaces.add(interfaceInfo.getInternalName());
        }
    }

    /**
     * Let's do this
     */
    private void findRenamedMethods(ClassNode classNode) {
        for (MethodNode mixinMethod : classNode.methods) {
            Method method = this.classInfo.findMethod(mixinMethod);

            AnnotationNode shadowAnnotation = ASMHelper.getVisibleAnnotation(mixinMethod, Shadow.class);
            if (shadowAnnotation != null) {
                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;
                }
            }

            for (InterfaceInfo iface : this.softImplements) {
                if (iface.renameMethod(mixinMethod)) {
                    method.renameTo(mixinMethod.name);
                }
            }
        }
    }

    /**
     * Apply discovered method renames to method invokations in the mixin
     */
    private void transformMethods(ClassNode classNode) {
        for (MethodNode mixinMethod : 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.classInfo.findMethodInHierarchy(methodNode, true);
                    if (method != null && method.isRenamed()) {
                        methodNode.name = method.getName();
                    }
                }
            }
        }
    }

    ClassInfo getClassInfo() {
        return this.classInfo;
    }

    /**
     * Get the parent config which declares this mixin
     */
    public MixinConfig getParent() {
        return this.parent;
    }

    /**
     * Get the simple name of the mixin
     */
    @Override
    public String getName() {
        return this.name;
    }

    /**
     * Get the name of the mixin class
     */
    @Override
    public String getClassName() {
        return this.className;
    }

    /**
     * Get the ref (internal name) of the mixin class
     */
    @Override
    public String getClassRef() {
        return this.classInfo.getName();
    }

    /**
     * Get the class bytecode
     */
    @Override
    public byte[] getClassBytes() {
        return this.mixinBytes;
    }

    /**
     * True if the superclass of the mixin is <b>not</b> the direct superclass
     * of one or more targets
     */
    @Override
    public boolean isDetachedSuper() {
        return this.detachedSuper;
    }

    /**
     * Get the logging level for this mixin
     */
    public Level getLoggingLevel() {
        return this.parent.getLoggingLevel();
    }

    /**
     * Get a new tree for the class bytecode
     */
    @Override
    public ClassNode getClassNode(int flags) {
        MixinClassNode classNode = new MixinClassNode(this);
        ClassReader classReader = new ClassReader(this.mixinBytes);
        classReader.accept(classNode, flags);
        return classNode;
    }

    /**
     * Get the target class names for this mixin
     */
    @Override
    public List<String> getTargetClasses() {
        return this.targetClassNames;
    }

    /**
     * Get the target class list for this mixin
     */
    public List<ClassInfo> getTargets() {
        return Collections.unmodifiableList(this.targetClasses);
    }

    /**
     * Get the mixin priority
     */
    @Override
    public int getPriority() {
        return this.priority;
    }

    /**
     * Get all interfaces for this mixin
     * 
     * @return mixin interfaces
     */
    public Set<String> getInterfaces() {
        return this.interfaces;
    }

    /**
     * Get a new mixin target context object for the specified target
     * 
     * @param target
     * @return
     */
    public MixinTargetContext createContextForTarget(ClassNode target) {
        ClassNode classNode = this.getClassNode(ClassReader.EXPAND_FRAMES);
        return new MixinTargetContext(this, this.prepare(classNode), target);
    }

    /**
     * @param mixinClassName
     * @param runTransformers
     * @return
     * @throws ClassNotFoundException 
     */
    private byte[] loadMixinClass(String mixinClassName, boolean runTransformers) throws ClassNotFoundException {
        byte[] mixinBytes = null;

        try {
            mixinBytes = TreeInfo.loadClass(mixinClassName, runTransformers);
        } catch (ClassNotFoundException ex) {
            throw new ClassNotFoundException(
                    String.format("The specified mixin '%s' was not found", mixinClassName));
        } catch (IOException ex) {
            this.logger.warn("Failed to load mixin %s, the specified mixin will not be applied", mixinClassName);
            throw new InvalidMixinException(this, "An error was encountered whilst loading the mixin class", ex);
        }

        // Inject the mixin class name into the LaunchClassLoader's invalid
        // classes set so that any classes referencing the mixin directly will
        // cause the game to crash
        if (MixinInfo.invalidClasses != null) {
            MixinInfo.invalidClasses.add(mixinClassName);
        }

        return mixinBytes;
    }

    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(MixinInfo other) {
        if (other == null) {
            return 0;
        }
        if (other.priority == this.priority) {
            return this.order - other.order;
        }
        return (this.priority - other.priority);
    }

    /**
     * Called immediately before the mixin is applied to targetClass
     */
    public void preApply(String transformedName, ClassNode targetClass) {
        if (this.plugin != null) {
            this.plugin.preApply(transformedName, targetClass, this.className, this);
        }
    }

    /**
     * Called immediately after the mixin is applied to targetClass
     */
    public void postApply(String transformedName, ClassNode targetClass) {
        if (this.plugin != null) {
            this.plugin.postApply(transformedName, targetClass, this.className, this);
        }
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return String.format("%s:%s", this.parent.getName(), this.name);
    }

    @SuppressWarnings("unchecked")
    private static Set<String> $getInvalidClassesSet() {
        try {
            Field invalidClasses = LaunchClassLoader.class.getDeclaredField("invalidClasses");
            invalidClasses.setAccessible(true);
            return (Set<String>) invalidClasses.get(Launch.classLoader);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}