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

Java tutorial

Introduction

Here is the source code for org.spongepowered.asm.mixin.transformer.ClassInfo.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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.objectweb.asm.Opcodes;
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 com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

/**
 * Information about a class, used as a way of keeping track of class hierarchy
 * information needed to support more complex mixin behaviour such as detached
 * superclass and mixin inheritance.  
 */
class ClassInfo extends TreeInfo {

    /**
     * <p>To all intents and purposes, the "real" class hierarchy and the mixin
     * class hierarchy exist in parallel, this means that for some hierarchy
     * validation operations we need to walk <em>across</em> to the other
     * hierarchy in order to allow meaningful validation to occur.</p>
     * 
     * <p>This enum defines the type of traversal operations which are allowed
     * for a particular lookup.</p>
     *  
     * <p>Each traversal type has a <code>next</code> property which defines
     * the traversal type to use on the <em>next</em> step of the hierarchy
     * validation. For example, the type {@link #IMMEDIATE} which requires an 
     * immediate match falls through to {@link #NONE} on the next step, which
     * prevents further traversals from occurring in the lookup.</p>
     */
    static enum Traversal {

        /**
         * No traversals are allowed.
         */
        NONE(null, false),

        /**
         * Traversal is allowed at all stages.
         */
        ALL(null, true),

        /**
         * Traversal is allowed at the bottom of the hierarchy but no further.
         */
        IMMEDIATE(Traversal.NONE, true),

        /**
         * Traversal is allowed only on superclasses and not at the bottom of
         * the hierarchy.
         */
        SUPER(Traversal.ALL, false);

        private final Traversal next;

        private final boolean traverse;

        private Traversal(Traversal next, boolean traverse) {
            this.next = next != null ? next : this;
            this.traverse = traverse;
        }

        public Traversal next() {
            return this.next;
        }

        public boolean canTraverse() {
            return this.traverse;
        }
    }

    /**
     * Information about a method in this class
     */
    class Method {

        /**
         * The original name of the method 
         */
        private final String methodName;

        /**
         * The method signature
         */
        private final String methodDesc;

        /**
         * True if this method was injected by a mixin, false if it was
         * originally part of the class
         */
        private final boolean isInjected;

        /**
         * Current name of the method, may be different from {@link methodName}
         * if the method has been renamed
         */
        private String currentName;

        public Method(MethodNode method) {
            this(method, false);
        }

        public Method(Method method) {
            this(method.methodName, method.methodDesc, method.isInjected);
            this.currentName = method.currentName;
        }

        public Method(MethodNode method, boolean injected) {
            this(method.name, method.desc, injected);
        }

        public Method(String name, String desc) {
            this(name, desc, false);
        }

        public Method(String name, String desc, boolean injected) {
            this.methodName = name;
            this.methodDesc = desc;
            this.isInjected = injected;
            this.currentName = name;
        }

        public String getOriginalName() {
            return this.methodName;
        }

        public String getName() {
            return this.currentName;
        }

        public String getDesc() {
            return this.methodDesc;
        }

        public boolean isInjected() {
            return this.isInjected;
        }

        public boolean isRenamed() {
            return this.currentName != this.methodName;
        }

        public ClassInfo getOwner() {
            return ClassInfo.this;
        }

        public void renameTo(String name) {
            this.currentName = name;
        }

        public boolean equals(String name, String desc) {
            return (this.methodName.equals(name) || this.currentName.equals(name)) && this.methodDesc.equals(desc);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Method)) {
                return false;
            }

            Method other = (Method) obj;
            return (other.methodName.equals(this.methodName) || other.currentName.equals(this.currentName))
                    && other.methodDesc.equals(this.methodDesc);
        }

        @Override
        public int hashCode() {
            return this.toString().hashCode();
        }

        @Override
        public String toString() {
            return this.methodName + this.methodDesc;
        }
    }

    private static final String JAVA_LANG_OBJECT = "java/lang/Object";

    /**
     * Loading and parsing classes is expensive, so keep a cache of all the
     * information we generate
     */
    private static final Map<String, ClassInfo> cache = new HashMap<String, ClassInfo>();

    private static final ClassInfo OBJECT = new ClassInfo();

    static {
        ClassInfo.cache.put(ClassInfo.JAVA_LANG_OBJECT, ClassInfo.OBJECT);
    }

    /**
     * Class name (binary name)
     */
    private final String name;

    /**
     * Class superclass name (binary name)
     */
    private final String superName;

    /**
     * Outer class name
     */
    private final String outerName;

    /**
     * Interfaces
     */
    private final List<String> interfaces;

    /**
     * Public and protected methods (instance) methods in this class 
     */
    private final Set<Method> methods;

    /**
     * Public and protected fields in this class
     */
    private final List<String> fields;

    /**
     * Mixins which target this class
     */
    private final Set<MixinInfo> mixins = new HashSet<MixinInfo>();

    /**
     * Map of mixin types to corresponding supertypes, to avoid repeated 
     * lookups 
     */
    private final Map<ClassInfo, ClassInfo> correspondingTypes = new HashMap<ClassInfo, ClassInfo>();

    /**
     * Mixin info if this class is a mixin itself 
     */
    private final MixinInfo mixin;

    /**
     * True if this is a mixin rather than a class 
     */
    private final boolean isMixin;

    /**
     * True if this is an interface 
     */
    private final boolean isInterface;

    /**
     * Access flags
     */
    private final int access;

    /**
     * Superclass reference, not initialised until required 
     */
    private ClassInfo superClass;

    /**
     * Outer class reference, not initialised until required 
     */
    private ClassInfo outerClass;

    /**
     * Private constructor used to initialise the ClassInfo for {@link Object}
     */
    private ClassInfo() {
        this.name = ClassInfo.JAVA_LANG_OBJECT;
        this.superName = null;
        this.outerName = null;
        this.methods = ImmutableSet.<Method>of(new Method("getClass", "()Ljava/lang/Class;"),
                new Method("hashCode", "()I"), new Method("equals", "(Ljava/lang/Object;)Z"),
                new Method("clone", "()Ljava/lang/Object;"), new Method("toString", "()Ljava/lang/String;"),
                new Method("notify", "()V"), new Method("notifyAll", "()V"), new Method("wait", "(J)V"),
                new Method("wait", "(JI)V"), new Method("wait", "()V"), new Method("finalize", "()V"));
        this.fields = Collections.<String>emptyList();
        this.isInterface = false;
        this.interfaces = Collections.<String>emptyList();
        this.access = Opcodes.ACC_PUBLIC;
        this.isMixin = false;
        this.mixin = null;
    }

    /**
     * Initialise a ClassInfo from the supplied {@link ClassNode}
     * 
     * @param classNode Class node to inspect
     */
    private ClassInfo(ClassNode classNode) {
        this.name = classNode.name;
        this.superName = classNode.superName != null ? classNode.superName : ClassInfo.JAVA_LANG_OBJECT;
        this.methods = new HashSet<Method>();
        this.fields = new ArrayList<String>();
        this.isInterface = ((classNode.access & Opcodes.ACC_INTERFACE) != 0);
        this.interfaces = Collections.unmodifiableList(classNode.interfaces);
        this.access = classNode.access;
        this.isMixin = classNode instanceof MixinClassNode;
        this.mixin = this.isMixin ? ((MixinClassNode) classNode).getMixin() : null;

        for (MethodNode method : classNode.methods) {
            this.addMethod(method, this.isMixin);
        }

        String outerName = classNode.outerClass;
        if (outerName == null) {
            for (FieldNode field : classNode.fields) {
                if ((field.access & Opcodes.ACC_SYNTHETIC) != 0) {
                    if (field.name.startsWith("this$")) {
                        outerName = field.desc;
                        if (outerName.startsWith("L")) {
                            outerName = outerName.substring(1, outerName.length() - 1);
                        }
                    }
                } else if ((field.access & Opcodes.ACC_PRIVATE) == 0) {
                    this.fields.add(field.name + "()" + field.desc);
                }
            }
        }

        this.outerName = outerName;
    }

    void addMethod(MethodNode method) {
        this.addMethod(method, true);
    }

    private void addMethod(MethodNode method, boolean injected) {
        if (!method.name.startsWith("<") && (method.access & Opcodes.ACC_PRIVATE) == 0
                && (method.access & Opcodes.ACC_STATIC) == 0) {
            this.methods.add(new Method(method, injected));
        }
    }

    /**
     * Add a mixin which targets this class
     */
    void addMixin(MixinInfo mixin) {
        if (this.isMixin) {
            throw new IllegalArgumentException("Cannot add target " + this.name + " for " + mixin.getClassName()
                    + " because the target is a mixin");
        }
        this.mixins.add(mixin);
    }

    /**
     * Get all mixins which target this class
     */
    public Set<MixinInfo> getMixins() {
        return Collections.unmodifiableSet(this.mixins);
    }

    /**
     * Get whether this class is a mixin
     */
    public boolean isMixin() {
        return this.isMixin;
    }

    /**
     * Get whether this class has ACC_PUBLIC
     */
    public boolean isPublic() {
        return (this.access & Opcodes.ACC_PUBLIC) != 0;
    }

    /**
     * Get whether this class is an inner class
     */
    public boolean isInner() {
        return this.outerName != null;
    }

    /**
     * Get whether this is an interface or not
     */
    public boolean isInterface() {
        return this.isInterface;
    }

    /**
     * Returns the answer to life, the universe and everything
     */
    public List<String> getInterfaces() {
        return this.interfaces;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public int getAccess() {
        return this.access;
    }

    /**
     * Get the class name (binary name)
     */
    public String getName() {
        return this.name;
    }

    /**
     * Get the superclass name (binary name)
     */
    public String getSuperName() {
        return this.superName;
    }

    /**
     * Get the superclass info, can return null if the superclass cannot be
     * resolved
     */
    public ClassInfo getSuperClass() {
        if (this.superClass == null && this.superName != null) {
            this.superClass = ClassInfo.forName(this.superName);
        }

        return this.superClass;
    }

    /**
     * Get the name of the outer class, or null if this is not an inner class
     */
    public String getOuterName() {
        return this.outerName;
    }

    /**
     * Get the outer class info, can return null if the outer class cannot be
     * resolved or if this is not an inner class
     */
    public ClassInfo getOuterClass() {
        if (this.outerClass == null && this.outerName != null) {
            this.outerClass = ClassInfo.forName(this.outerName);
        }

        return this.outerClass;
    }

    /**
     * Class targets
     */
    protected List<ClassInfo> getTargets() {
        if (this.mixin != null) {
            List<ClassInfo> targets = new ArrayList<ClassInfo>();
            targets.add(this);
            targets.addAll(this.mixin.getTargets());
            return targets;
        }

        return ImmutableList.<ClassInfo>of(this);
    }

    /**
     * Get class/interface methods
     * 
     * @return read-only view of class methods
     */
    public Set<Method> getMethods() {
        return Collections.unmodifiableSet(this.methods);
    }

    /**
     * Test whether this class has the specified superclass in its hierarchy
     * 
     * @param superClass Name of the superclass to search for in the hierarchy
     * @return true if the specified class appears in the class's hierarchy
     * anywhere
     */
    public boolean hasSuperClass(String superClass) {
        return this.hasSuperClass(superClass, Traversal.NONE);
    }

    /**
     * Test whether this class has the specified superclass in its hierarchy
     * 
     * @param superClass Name of the superclass to search for in the hierarchy
     * @param traversal Traversal type to allow during this lookup
     * @return true if the specified class appears in the class's hierarchy
     * anywhere
     */
    public boolean hasSuperClass(String superClass, Traversal traversal) {
        if (ClassInfo.JAVA_LANG_OBJECT.equals(superClass)) {
            return true;
        }

        return this.findSuperClass(superClass, traversal) != null;
    }

    /**
     * Test whether this class has the specified superclass in its hierarchy
     * 
     * @param superClass Superclass to search for in the hierarchy
     * @return true if the specified class appears in the class's hierarchy
     * anywhere
     */
    public boolean hasSuperClass(ClassInfo superClass) {
        return this.hasSuperClass(superClass, Traversal.NONE);
    }

    /**
     * Test whether this class has the specified superclass in its hierarchy
     * 
     * @param superClass Superclass to search for in the hierarchy
     * @param traversal Traversal type to allow during this lookup
     * @return true if the specified class appears in the class's hierarchy
     * anywhere
     */
    public boolean hasSuperClass(ClassInfo superClass, Traversal traversal) {
        if (ClassInfo.OBJECT == superClass) {
            return true;
        }

        return this.findSuperClass(superClass.name, traversal) != null;
    }

    /**
     * Search for the specified superclass in this class's hierarchy. If found
     * returns the ClassInfo, otherwise returns null
     * 
     * @param superClass Superclass name to search for
     * @return Matched superclass or null if not found 
     */
    public ClassInfo findSuperClass(String superClass) {
        return this.findSuperClass(superClass, Traversal.NONE);
    }

    /**
     * Search for the specified superclass in this class's hierarchy. If found
     * returns the ClassInfo, otherwise returns null
     * 
     * @param superClass Superclass name to search for
     * @param traversal Traversal type to allow during this lookup
     * @return Matched superclass or null if not found 
     */
    public ClassInfo findSuperClass(String superClass, Traversal traversal) {
        ClassInfo superClassInfo = this.getSuperClass();
        if (superClassInfo != null) {
            List<ClassInfo> targets = superClassInfo.getTargets();
            for (ClassInfo superTarget : targets) {
                if (superClass.equals(superTarget.getName())) {
                    return superClassInfo;
                }

                ClassInfo found = superTarget.findSuperClass(superClass, traversal.next());
                if (found != null) {
                    return found;
                }
            }
        }

        if (traversal.canTraverse()) {
            for (MixinInfo mixin : this.mixins) {
                ClassInfo targetSuper = mixin.getClassInfo().findSuperClass(superClass, traversal);
                if (targetSuper != null) {
                    return targetSuper;
                }
            }
        }

        return null;
    }

    /**
     * Walks up this class's hierarchy to find the first class targetted by the
     * specified mixin. This is used during mixin application to translate a
     * mixin reference to a "real class" reference <em>in the context of <b>this
     * </b> class</em>.
     * 
     * @param mixin Mixin class to search for
     * @return corresponding (target) class for the specified mixin or null if
     *      no corresponding mixin was found
     */
    ClassInfo findCorrespondingType(ClassInfo mixin) {
        if (mixin == null || !mixin.isMixin || this.isMixin) {
            return null;
        }

        ClassInfo correspondingType = this.correspondingTypes.get(mixin);
        if (correspondingType == null) {
            correspondingType = this.findSuperTypeForMixin(mixin);
            this.correspondingTypes.put(mixin, correspondingType);
        }
        return correspondingType;
    }

    /* (non-Javadoc)
     * Only used by findCorrespondingType(), used as a convenience so that
     * sanity checks and caching can be handled more elegantly
     */
    private ClassInfo findSuperTypeForMixin(ClassInfo mixin) {
        ClassInfo superClass = this;

        while (superClass != null && superClass != ClassInfo.OBJECT) {
            for (MixinInfo minion : superClass.mixins) {
                if (minion.getClassInfo().equals(mixin)) {
                    return superClass;
                }
            }

            superClass = superClass.getSuperClass();
        }

        return null;
    }

    /**
     * Find out whether this (mixin) class has another mixin in its superclass
     * hierarchy. This method always returns false for non-mixin classes.
     * 
     * @return true if and only if one or more mixins are found in the hierarchy
     *      of this mixin
     */
    public boolean hasMixinInHierarchy() {
        if (!this.isMixin) {
            return false;
        }

        ClassInfo superClass = this.getSuperClass();

        while (superClass != null && superClass != ClassInfo.OBJECT) {
            if (superClass.isMixin) {
                return true;
            }
            superClass = superClass.getSuperClass();
        }

        return false;
    }

    /**
     * Find out whether this (non-mixin) class has a mixin targetting
     * <em>any</em> of its superclasses. This method always returns false for
     * mixin classes.
     * 
     * @return true if and only if one or more classes in this class's hierarchy
     *      are targetted by a mixin
     */
    public boolean hasMixinTargetInHierarchy() {
        if (this.isMixin) {
            return false;
        }

        ClassInfo superClass = this.getSuperClass();

        while (superClass != null && superClass != ClassInfo.OBJECT) {
            if (superClass.mixins.size() > 0) {
                return true;
            }
            superClass = superClass.getSuperClass();
        }

        return false;
    }

    /**
     * Finds the specified private or protected method in this class's hierarchy
     * 
     * @param method Method to search for
     * @param includeThisClass True to return this class if the method exists
     *      here, or false to search only superclasses
     * @return the method object or null if the method could not be resolved
     */
    public Method findMethodInHierarchy(MethodNode method, boolean includeThisClass) {
        return this.findMethodInHierarchy(method.name, method.desc, includeThisClass);
    }

    /**
     * Finds the specified public or protected method in this class's hierarchy
     * 
     * @param method Method to search for
     * @param includeThisClass True to return this class if the method exists
     *      here, or false to search only superclasses
     * @return the method object or null if the method could not be resolved
     */
    public Method findMethodInHierarchy(MethodInsnNode method, boolean includeThisClass) {
        return this.findMethodInHierarchy(method.name, method.desc, includeThisClass);
    }

    /**
     * Finds the specified public or protected method in this class's hierarchy
     * 
     * @param name Method name to search for
     * @param desc Method descriptor
     * @param includeThisClass True to return this class if the method exists
     *      here, or false to search only superclasses
     * @return the method object or null if the method could not be resolved
     */
    public Method findMethodInHierarchy(String name, String desc, boolean includeThisClass) {
        return this.findMethodInHierarchy(name, desc, includeThisClass, Traversal.NONE);
    }

    /**
     * Finds the specified public or protected method in this class's hierarchy
     * 
     * @param name Method name to search for
     * @param desc Method descriptor
     * @param includeThisClass True to return this class if the method exists
     *      here, or false to search only superclasses
     * @param traversal Traversal type to allow during this lookup
     * @return the method object or null if the method could not be resolved
     */
    public Method findMethodInHierarchy(String name, String desc, boolean includeThisClass, Traversal traversal) {
        if (includeThisClass) {
            Method method = this.findMethod(name, desc);
            if (method != null) {
                return method;
            }

            if (traversal.canTraverse()) {
                for (MixinInfo mixin : this.mixins) {
                    Method mixinMethod = mixin.getClassInfo().findMethod(name, desc);
                    if (mixinMethod != null) {
                        return new Method(mixinMethod);
                    }
                }
            }
        }

        ClassInfo superClassInfo = this.getSuperClass();
        if (superClassInfo != null) {
            for (ClassInfo superTarget : superClassInfo.getTargets()) {
                Method method = superTarget.findMethodInHierarchy(name, desc, true, traversal.next());
                if (method != null) {
                    return method;
                }
            }
        }

        return null;
    }

    /**
     * Finds the specified public or protected method in this class
     * 
     * @param method Method to search for
     * @return the method object or null if the method could not be resolved
     */
    public Method findMethod(MethodNode method) {
        return this.findMethod(method.name, method.desc);
    }

    /**
     * Finds the specified public or protected method in this class
     * 
     * @param method Method to search for
     * @return the method object or null if the method could not be resolved
     */
    public Method findMethod(MethodInsnNode method) {
        return this.findMethod(method.name, method.desc);
    }

    /**
     * Finds the specified public or protected method in this class
     * 
     * @param method Method name to search for
     * @param desc Method signature to search for
     * @return the method object or null if the method could not be resolved
     */
    public Method findMethod(String name, String desc) {
        for (Method method : this.methods) {
            if (method.equals(name, desc)) {
                return method;
            }
        }

        return null;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object other) {
        if (!(other instanceof ClassInfo)) {
            return false;
        }
        return ((ClassInfo) other).name.equals(this.name);
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }

    /**
     * Return a ClassInfo for the supplied {@link ClassNode}. If a ClassInfo for
     * the class was already defined, then the original ClassInfo is returned
     * from the internal cache. Otherwise a new ClassInfo is created and
     * returned.
     * 
     * @param classNode
     * @return
     */
    public static ClassInfo fromClassNode(ClassNode classNode) {
        ClassInfo info = ClassInfo.cache.get(classNode.name);
        if (info == null) {
            info = new ClassInfo(classNode);
            ClassInfo.cache.put(classNode.name, info);
        }

        return info;
    }

    /**
     * Return a ClassInfo for the specified class name, fetches the ClassInfo
     * from the cache where possible
     * 
     * @param className Binary name of the class to look up
     * @return ClassInfo for the specified class name or null if the specified
     *      name cannot be resolved for some reason
     */
    public static ClassInfo forName(String className) {
        className = className.replace('.', '/');

        ClassInfo info = ClassInfo.cache.get(className);
        if (info == null) {
            try {
                ClassNode classNode = TreeInfo.getClassNode(className);
                info = new ClassInfo(classNode);
            } catch (Exception ex) {
                ex.printStackTrace();
            }

            // Put null in the cache if load failed
            ClassInfo.cache.put(className, info);
        }

        return info;
    }
}