/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered <https://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.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.spongepowered.asm.lib.Opcodes; import org.spongepowered.asm.lib.tree.AbstractInsnNode; import org.spongepowered.asm.lib.tree.ClassNode; import org.spongepowered.asm.lib.tree.FieldInsnNode; import org.spongepowered.asm.lib.tree.FieldNode; import org.spongepowered.asm.lib.tree.FrameNode; import org.spongepowered.asm.lib.tree.MethodInsnNode; import org.spongepowered.asm.lib.tree.MethodNode; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Invoker; import org.spongepowered.asm.mixin.transformer.ClassInfo.Member.Type; import org.spongepowered.asm.mixin.transformer.MixinInfo.MixinClassNode; import org.spongepowered.asm.util.Annotations; import org.spongepowered.asm.util.ClassSignature; 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. */ public final class ClassInfo extends TreeInfo { public static final int INCLUDE_PRIVATE = Opcodes.ACC_PRIVATE; public static final int INCLUDE_STATIC = Opcodes.ACC_STATIC; public static final int INCLUDE_ALL = ClassInfo.INCLUDE_PRIVATE | ClassInfo.INCLUDE_STATIC; /** * Search type for the findInHierarchy methods, replaces a boolean flag * which made calling code difficult to read */ public static enum SearchType { /** * Include this class when searching in the hierarchy */ ALL_CLASSES, /** * Only walk the superclasses when searching the hierarchy */ SUPER_CLASSES_ONLY } /** * <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> */ public static enum Traversal { /** * No traversals are allowed. */ NONE(null, false, SearchType.SUPER_CLASSES_ONLY), /** * Traversal is allowed at all stages. */ ALL(null, true, SearchType.ALL_CLASSES), /** * Traversal is allowed at the bottom of the hierarchy but no further. */ IMMEDIATE(Traversal.NONE, true, SearchType.SUPER_CLASSES_ONLY), /** * Traversal is allowed only on superclasses and not at the bottom of * the hierarchy. */ SUPER(Traversal.ALL, false, SearchType.SUPER_CLASSES_ONLY); private final Traversal next; private final boolean traverse; private final SearchType searchType; private Traversal(Traversal next, boolean traverse, SearchType searchType) { this.next = next != null ? next : this; this.traverse = traverse; this.searchType = searchType; } /** * Return the next traversal type for this traversal type */ public Traversal next() { return this.next; } /** * Return whether this traversal type allows traversal */ public boolean canTraverse() { return this.traverse; } public SearchType getSearchType() { return this.searchType; } } /** * Information about frames in a method */ public static class FrameData { private static final String[] FRAMETYPES = { "NEW", "FULL", "APPEND", "CHOP", "SAME", "SAME1" }; /** * Frame index */ public final int index; /** * Frame type */ public final int type; /** * Frame local count */ public final int locals; FrameData(int index, int type, int locals) { this.index = index; this.type = type; this.locals = locals; } FrameData(int index, FrameNode frameNode) { this.index = index; this.type = frameNode.type; this.locals = frameNode.local != null ? frameNode.local.size() : 0; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("FrameData[index=%d, type=%s, locals=%d]", this.index, FrameData.FRAMETYPES[this.type + 1], this.locals); } } /** * Information about a member in this class */ abstract static class Member { /** * Member type */ static enum Type { METHOD, FIELD } /** * Member type */ private final Type type; /** * The original name of the member */ private final String memberName; /** * The member's signature */ private final String memberDesc; /** * True if this member was injected by a mixin, false if it was * originally part of the class */ private final boolean isInjected; /** * Access modifiers */ private final int modifiers; /** * Current name of the member, may be different from {@link #memberName} * if the member has been renamed */ private String currentName; /** * True if this member is decorated with {@link Final} */ private boolean decoratedFinal; /** * True if this member is decorated with {@link Mutable} */ private boolean decoratedMutable; /** * True if this member is decorated with {@link Unique} */ private boolean unique; protected Member(Member member) { this(member.type, member.memberName, member.memberDesc, member.modifiers, member.isInjected); this.currentName = member.currentName; this.unique = member.unique; } protected Member(Type type, String name, String desc, int access) { this(type, name, desc, access, false); } protected Member(Type type, String name, String desc, int access, boolean injected) { this.type = type; this.memberName = name; this.memberDesc = desc; this.isInjected = injected; this.currentName = name; this.modifiers = access; } public String getOriginalName() { return this.memberName; } public String getName() { return this.currentName; } public String getDesc() { return this.memberDesc; } public boolean isInjected() { return this.isInjected; } public boolean isRenamed() { return this.currentName != this.memberName; } public boolean isPrivate() { return (this.modifiers & Opcodes.ACC_PRIVATE) != 0; } public boolean isStatic() { return (this.modifiers & Opcodes.ACC_STATIC) != 0; } public boolean isAbstract() { return (this.modifiers & Opcodes.ACC_ABSTRACT) != 0; } public boolean isFinal() { return (this.modifiers & Opcodes.ACC_FINAL) != 0; } public boolean isUnique() { return this.unique; } public void setUnique(boolean unique) { this.unique = unique; } public boolean isDecoratedFinal() { return this.decoratedFinal; } public boolean isDecoratedMutable() { return this.decoratedMutable; } public void setDecoratedFinal(boolean decoratedFinal, boolean decoratedMutable) { this.decoratedFinal = decoratedFinal; this.decoratedMutable = decoratedMutable; } public boolean matchesFlags(int flags) { return (((~this.modifiers | (flags & ClassInfo.INCLUDE_PRIVATE)) & ClassInfo.INCLUDE_PRIVATE) != 0 && ((~this.modifiers | (flags & ClassInfo.INCLUDE_STATIC)) & ClassInfo.INCLUDE_STATIC) != 0); } // Abstract because this has to be static in order to contain the enum public abstract ClassInfo getOwner(); public ClassInfo getImplementor() { return this.getOwner(); } public int getAccess() { return this.modifiers; } /** * @param name new name * @return the passed-in argument, for fluency */ public String renameTo(String name) { this.currentName = name; return name; } public boolean equals(String name, String desc) { return (this.memberName.equals(name) || this.currentName.equals(name)) && this.memberDesc.equals(desc); } @Override public boolean equals(Object obj) { if (!(obj instanceof Member)) { return false; } Member other = (Member)obj; return (other.memberName.equals(this.memberName) || other.currentName.equals(this.currentName)) && other.memberDesc.equals(this.memberDesc); } @Override public int hashCode() { return this.toString().hashCode(); } @Override public String toString() { return String.format(this.getDisplayFormat(), this.memberName, this.memberDesc); } protected String getDisplayFormat() { return "%s%s"; } } /** * A method */ public class Method extends Member { private final List<FrameData> frames; private boolean isAccessor; public Method(Member member) { super(member); this.frames = member instanceof Method ? ((Method)member).frames : null; } @SuppressWarnings("unchecked") public Method(MethodNode method) { this(method, false); this.setUnique(Annotations.getVisible(method, Unique.class) != null); this.isAccessor = Annotations.getSingleVisible(method, Accessor.class, Invoker.class) != null; } @SuppressWarnings("unchecked") public Method(MethodNode method, boolean injected) { super(Type.METHOD, method.name, method.desc, method.access, injected); this.frames = this.gatherFrames(method); this.setUnique(Annotations.getVisible(method, Unique.class) != null); this.isAccessor = Annotations.getSingleVisible(method, Accessor.class, Invoker.class) != null; } public Method(String name, String desc) { super(Type.METHOD, name, desc, Opcodes.ACC_PUBLIC, false); this.frames = null; } public Method(String name, String desc, int access) { super(Type.METHOD, name, desc, access, false); this.frames = null; } public Method(String name, String desc, int access, boolean injected) { super(Type.METHOD, name, desc, access, injected); this.frames = null; } private List<FrameData> gatherFrames(MethodNode method) { List<FrameData> frames = new ArrayList<FrameData>(); for (Iterator<AbstractInsnNode> iter = method.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn instanceof FrameNode) { frames.add(new FrameData(method.instructions.indexOf(insn), (FrameNode)insn)); } } return frames; } public List<FrameData> getFrames() { return this.frames; } @Override public ClassInfo getOwner() { return ClassInfo.this; } public boolean isAccessor() { return this.isAccessor; } @Override public boolean equals(Object obj) { if (!(obj instanceof Method)) { return false; } return super.equals(obj); } } /** * A method resolved in an interface <em>via</em> a class, return the member * wrapped so that the implementing class can be retrieved. */ public class InterfaceMethod extends Method { private final ClassInfo owner; public InterfaceMethod(Member member) { super(member); this.owner = member.getOwner(); } @Override public ClassInfo getOwner() { return this.owner; } @Override public ClassInfo getImplementor() { return ClassInfo.this; } } /** * A field */ class Field extends Member { public Field(Member member) { super(member); } public Field(FieldNode field) { this(field, false); } public Field(FieldNode field, boolean injected) { super(Type.FIELD, field.name, field.desc, field.access, injected); this.setUnique(Annotations.getVisible(field, Unique.class) != null); if (Annotations.getVisible(field, Shadow.class) != null) { boolean decoratedFinal = Annotations.getVisible(field, Final.class) != null; boolean decoratedMutable = Annotations.getVisible(field, Mutable.class) != null; this.setDecoratedFinal(decoratedFinal, decoratedMutable); } } public Field(String name, String desc, int access) { super(Type.FIELD, name, desc, access, false); } public Field(String name, String desc, int access, boolean injected) { super(Type.FIELD, name, desc, access, injected); } @Override public ClassInfo getOwner() { return ClassInfo.this; } @Override public boolean equals(Object obj) { if (!(obj instanceof Field)) { return false; } return super.equals(obj); } @Override protected String getDisplayFormat() { return "%s:%s"; } } private static final Logger logger = LogManager.getLogger("mixin"); 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; /** * True either if this is not an inner class or if it is an inner class but * does not contain a reference to its outer class. */ private final boolean isProbablyStatic; /** * Interfaces */ private final Set<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 Set<Field> 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; private final MethodMapper methodMapper; /** * 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; /** * Class signature, lazy-loaded where possible */ private ClassSignature signature; /** * 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.isProbablyStatic = true; 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.<Field>emptySet(); this.isInterface = false; this.interfaces = Collections.<String>emptySet(); this.access = Opcodes.ACC_PUBLIC; this.isMixin = false; this.mixin = null; this.methodMapper = 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 HashSet<Field>(); this.isInterface = ((classNode.access & Opcodes.ACC_INTERFACE) != 0); this.interfaces = new HashSet<String>(); this.access = classNode.access; this.isMixin = classNode instanceof MixinClassNode; this.mixin = this.isMixin ? ((MixinClassNode)classNode).getMixin() : null; this.interfaces.addAll(classNode.interfaces); for (MethodNode method : classNode.methods) { this.addMethod(method, this.isMixin); } boolean isProbablyStatic = true; String outerName = classNode.outerClass; if (outerName == null) { for (FieldNode field : classNode.fields) { if ((field.access & Opcodes.ACC_SYNTHETIC) != 0) { if (field.name.startsWith("this$")) { isProbablyStatic = false; outerName = field.desc; if (outerName.startsWith("L")) { outerName = outerName.substring(1, outerName.length() - 1); } } } this.fields.add(new Field(field, this.isMixin)); } } this.isProbablyStatic = isProbablyStatic; this.outerName = outerName; this.methodMapper = new MethodMapper(MixinEnvironment.getCurrentEnvironment(), this); this.signature = ClassSignature.ofLazy(classNode); } void addInterface(String iface) { this.interfaces.add(iface); this.getSignature().addInterface(iface); } void addMethod(MethodNode method) { this.addMethod(method, true); } private void addMethod(MethodNode method, boolean injected) { if (!method.name.startsWith("<")) { 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.<MixinInfo>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 has ACC_ABSTRACT */ public boolean isAbstract() { return (this.access & Opcodes.ACC_ABSTRACT) != 0; } /** * Get whether this class has ACC_SYNTHETIC */ public boolean isSynthetic() { return (this.access & Opcodes.ACC_SYNTHETIC) != 0; } /** * Get whether this class is probably static (or is not an inner class) */ public boolean isProbablyStatic() { return this.isProbablyStatic; } /** * 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 Set<String> getInterfaces() { return Collections.<String>unmodifiableSet(this.interfaces); } @Override public String toString() { return this.name; } public MethodMapper getMethodMapper() { return this.methodMapper; } public int getAccess() { return this.access; } /** * Get the class name (binary name) */ public String getName() { return this.name; } /** * Get the class name (java format) */ public String getClassName() { return this.name.replace('/', '.'); } /** * 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; } /** * Return the class signature * * @return signature as a {@link ClassSignature} instance */ public ClassSignature getSignature() { return this.signature.wake(); } /** * Class targets */ 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.<Method>unmodifiableSet(this.methods); } /** * If this is an interface, returns a set containing all methods in this * interface and all super interfaces. If this is a class, returns a set * containing all methods for all interfaces implemented by this class and * all super interfaces of those interfaces. * * @param includeMixins Whether to include methods from mixins targeting * this class info * @return read-only view of class methods */ public Set<Method> getInterfaceMethods(boolean includeMixins) { Set<Method> methods = new HashSet<Method>(); ClassInfo supClass = this.addMethodsRecursive(methods, includeMixins); if (!this.isInterface) { while (supClass != null && supClass != ClassInfo.OBJECT) { supClass = supClass.addMethodsRecursive(methods, includeMixins); } } // Remove default methods. for (Iterator<Method> it = methods.iterator(); it.hasNext();) { if (!it.next().isAbstract()) { it.remove(); } } return Collections.<Method>unmodifiableSet(methods); } /** * Recursive function used by {@link #getInterfaceMethods} to add all * interface methods to the supplied set * * @param methods Method set to add to * @param includeMixins Whether to include methods from mixins targeting * this class info * @return superclass reference, used to make the code above more fluent */ private ClassInfo addMethodsRecursive(Set<Method> methods, boolean includeMixins) { if (this.isInterface) { for (Method method : this.methods) { // Default methods take priority. They are removed later. if (!method.isAbstract()) { // Remove the old method so the new one is added. methods.remove(method); } methods.add(method); } } else if (!this.isMixin && includeMixins) { for (MixinInfo mixin : this.mixins) { mixin.getClassInfo().addMethodsRecursive(methods, includeMixins); } } for (String iface : this.interfaces) { ClassInfo.forName(iface).addMethodsRecursive(methods, includeMixins); } return this.getSuperClass(); } /** * 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) { if (ClassInfo.OBJECT.name == superClass) { return null; } return this.findSuperClass(superClass, traversal, new HashSet<String>()); } private ClassInfo findSuperClass(String superClass, Traversal traversal, Set<String> traversed) { 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(), traversed); if (found != null) { return found; } } } if (traversal.canTraverse()) { for (MixinInfo mixin : this.mixins) { String mixinClassName = mixin.getClassName(); if (traversed.contains(mixinClassName)) { continue; } traversed.add(mixinClassName); ClassInfo mixinClass = mixin.getClassInfo(); if (superClass.equals(mixinClass.getName())) { return mixinClass; } ClassInfo targetSuper = mixinClass.findSuperClass(superClass, Traversal.ALL, traversed); 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 supClass = this.getSuperClass(); while (supClass != null && supClass != ClassInfo.OBJECT) { if (supClass.isMixin) { return true; } supClass = supClass.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 supClass = this.getSuperClass(); while (supClass != null && supClass != ClassInfo.OBJECT) { if (supClass.mixins.size() > 0) { return true; } supClass = supClass.getSuperClass(); } return false; } /** * Finds the specified private or protected method in this class's hierarchy * * @param method Method to search for * @param searchType Search strategy to use * @return the method object or null if the method could not be resolved */ public Method findMethodInHierarchy(MethodNode method, SearchType searchType) { return this.findMethodInHierarchy(method.name, method.desc, searchType, Traversal.NONE); } /** * Finds the specified private or protected method in this class's hierarchy * * @param method Method to search for * @param searchType Search strategy to use * @param flags search flags * @return the method object or null if the method could not be resolved */ public Method findMethodInHierarchy(MethodNode method, SearchType searchType, int flags) { return this.findMethodInHierarchy(method.name, method.desc, searchType, Traversal.NONE, flags); } /** * Finds the specified public or protected method in this class's hierarchy * * @param method Method to search for * @param searchType Search strategy to use * @return the method object or null if the method could not be resolved */ public Method findMethodInHierarchy(MethodInsnNode method, SearchType searchType) { return this.findMethodInHierarchy(method.name, method.desc, searchType, Traversal.NONE); } /** * Finds the specified public or protected method in this class's hierarchy * * @param method Method to search for * @param searchType Search strategy to use * @param flags search flags * @return the method object or null if the method could not be resolved */ public Method findMethodInHierarchy(MethodInsnNode method, SearchType searchType, int flags) { return this.findMethodInHierarchy(method.name, method.desc, searchType, Traversal.NONE, flags); } /** * Finds the specified public or protected method in this class's hierarchy * * @param name Method name to search for * @param desc Method descriptor * @param searchType Search strategy to use * @return the method object or null if the method could not be resolved */ public Method findMethodInHierarchy(String name, String desc, SearchType searchType) { return this.findMethodInHierarchy(name, desc, searchType, 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 searchType Search strategy to use * @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, SearchType searchType, Traversal traversal) { return this.findMethodInHierarchy(name, desc, searchType, traversal, 0); } /** * Finds the specified public or protected method in this class's hierarchy * * @param name Method name to search for * @param desc Method descriptor * @param searchType Search strategy to use * @param traversal Traversal type to allow during this lookup * @param flags search flags * @return the method object or null if the method could not be resolved */ public Method findMethodInHierarchy(String name, String desc, SearchType searchType, Traversal traversal, int flags) { return this.findInHierarchy(name, desc, searchType, traversal, flags, Type.METHOD); } /** * Finds the specified private or protected field in this class's hierarchy * * @param field Field to search for * @param searchType Search strategy to use * @return the field object or null if the field could not be resolved */ public Field findFieldInHierarchy(FieldNode field, SearchType searchType) { return this.findFieldInHierarchy(field.name, field.desc, searchType, Traversal.NONE); } /** * Finds the specified private or protected field in this class's hierarchy * * @param field Field to search for * @param searchType Search strategy to use * @param flags search flags * @return the field object or null if the field could not be resolved */ public Field findFieldInHierarchy(FieldNode field, SearchType searchType, int flags) { return this.findFieldInHierarchy(field.name, field.desc, searchType, Traversal.NONE, flags); } /** * Finds the specified public or protected field in this class's hierarchy * * @param field Field to search for * @param searchType Search strategy to use * @return the field object or null if the field could not be resolved */ public Field findFieldInHierarchy(FieldInsnNode field, SearchType searchType) { return this.findFieldInHierarchy(field.name, field.desc, searchType, Traversal.NONE); } /** * Finds the specified public or protected field in this class's hierarchy * * @param field Field to search for * @param searchType Search strategy to use * @param flags search flags * @return the field object or null if the field could not be resolved */ public Field findFieldInHierarchy(FieldInsnNode field, SearchType searchType, int flags) { return this.findFieldInHierarchy(field.name, field.desc, searchType, Traversal.NONE, flags); } /** * Finds the specified public or protected field in this class's hierarchy * * @param name Field name to search for * @param desc Field descriptor * @param searchType Search strategy to use * @return the field object or null if the field could not be resolved */ public Field findFieldInHierarchy(String name, String desc, SearchType searchType) { return this.findFieldInHierarchy(name, desc, searchType, Traversal.NONE); } /** * Finds the specified public or protected field in this class's hierarchy * * @param name Field name to search for * @param desc Field descriptor * @param searchType Search strategy to use * @param traversal Traversal type to allow during this lookup * @return the field object or null if the field could not be resolved */ public Field findFieldInHierarchy(String name, String desc, SearchType searchType, Traversal traversal) { return this.findFieldInHierarchy(name, desc, searchType, traversal, 0); } /** * Finds the specified public or protected field in this class's hierarchy * * @param name Field name to search for * @param desc Field descriptor * @param searchType Search strategy to use * @param traversal Traversal type to allow during this lookup * @param flags search flags * @return the field object or null if the field could not be resolved */ public Field findFieldInHierarchy(String name, String desc, SearchType searchType, Traversal traversal, int flags) { return this.findInHierarchy(name, desc, searchType, traversal, flags, Type.FIELD); } /** * Finds a public or protected member in the hierarchy of this class which * matches the supplied details * * @param name Member name to search * @param desc Member descriptor * @param searchType Search strategy to use * @param traversal Traversal type to allow during this lookup * @param flags Inclusion flags * @param type Type of member to search for (field or method) * @return the discovered member or null if the member could not be resolved */ @SuppressWarnings("unchecked") private <M extends Member> M findInHierarchy(String name, String desc, SearchType searchType, Traversal traversal, int flags, Type type) { if (searchType == SearchType.ALL_CLASSES) { M member = this.findMember(name, desc, flags, type); if (member != null) { return member; } if (traversal.canTraverse()) { for (MixinInfo mixin : this.mixins) { M mixinMember = mixin.getClassInfo().findMember(name, desc, flags, type); if (mixinMember != null) { return this.cloneMember(mixinMember); } } } } ClassInfo superClassInfo = this.getSuperClass(); if (superClassInfo != null) { for (ClassInfo superTarget : superClassInfo.getTargets()) { M member = superTarget.findInHierarchy(name, desc, SearchType.ALL_CLASSES, traversal.next(), flags & ~ClassInfo.INCLUDE_PRIVATE, type); if (member != null) { return member; } } } if (type == Type.METHOD && (this.isInterface || MixinEnvironment.getCompatibilityLevel().supportsMethodsInInterfaces())) { for (String implemented : this.interfaces) { ClassInfo iface = ClassInfo.forName(implemented); if (iface == null) { ClassInfo.logger.debug("Failed to resolve declared interface {} on {}", implemented, this.name); continue; // throw new RuntimeException(new ClassNotFoundException(implemented)); } M member = iface.findInHierarchy(name, desc, SearchType.ALL_CLASSES, traversal.next(), flags & ~ClassInfo.INCLUDE_PRIVATE, type); if (member != null) { return this.isInterface ? member : (M)new InterfaceMethod(member); } } } return null; } /** * Effectively a clone method for member, placed here so that the enclosing * instance for the inner class is this class and not the enclosing instance * of the existing class. Basically creates a cloned member with this * ClassInfo as its parent. * * @param member member to clone * @return wrapper member */ @SuppressWarnings("unchecked") private <M extends Member> M cloneMember(M member) { if (member instanceof Method) { return (M)new Method(member); } return (M)new Field(member); } /** * 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, method.access); } /** * Finds the specified public or protected method in this class * * @param method Method to search for * @param flags search flags * @return the method object or null if the method could not be resolved */ public Method findMethod(MethodNode method, int flags) { return this.findMethod(method.name, method.desc, flags); } /** * 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, 0); } /** * Finds the specified public or protected method in this class * * @param method Method to search for * @param flags search flags * @return the method object or null if the method could not be resolved */ public Method findMethod(MethodInsnNode method, int flags) { return this.findMethod(method.name, method.desc, flags); } /** * Finds the specified public or protected method in this class * * @param name Method name to search for * @param desc Method signature to search for * @param flags search flags * @return the method object or null if the method could not be resolved */ public Method findMethod(String name, String desc, int flags) { return this.findMember(name, desc, flags, Type.METHOD); } /** * Finds the specified field in this class * * @param field Field to search for * @return the field object or null if the field could not be resolved */ public Field findField(FieldNode field) { return this.findField(field.name, field.desc, field.access); } /** * Finds the specified public or protected method in this class * * @param field Field to search for * @param flags search flags * @return the field object or null if the field could not be resolved */ public Field findField(FieldInsnNode field, int flags) { return this.findField(field.name, field.desc, flags); } /** * Finds the specified field in this class * * @param name Field name to search for * @param desc Field signature to search for * @param flags search flags * @return the field object or null if the field could not be resolved */ public Field findField(String name, String desc, int flags) { return this.findMember(name, desc, flags, Type.FIELD); } /** * Finds the specified member in this class * * @param name Field name to search for * @param desc Field signature to search for * @param flags search flags * @param memberType Type of member list to search * @return the field object or null if the field could not be resolved */ private <M extends Member> M findMember(String name, String desc, int flags, Type memberType) { @SuppressWarnings("unchecked") Set<M> members = (Set<M>)(memberType == Type.METHOD ? this.methods : this.fields); for (M member : members) { if (member.equals(name, desc) && member.matchesFlags(flags)) { return member; } } 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 classNode to get info for * @return ClassInfo instance for the supplied classNode */ 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) { ClassInfo.logger.warn("Error loading class: {}", className); // ex.printStackTrace(); } // Put null in the cache if load failed ClassInfo.cache.put(className, info); ClassInfo.logger.trace("Added class metadata for {} to metadata cache", className); } return info; } /** * Return a ClassInfo for the specified class type, fetches the ClassInfo * from the cache where possible and generates the class meta if not. * * @param type Type to look up * @return ClassInfo for the supplied type or null if the supplied type * cannot be found or is a primitive type */ public static ClassInfo forType(org.spongepowered.asm.lib.Type type) { if (type.getSort() == org.spongepowered.asm.lib.Type.ARRAY) { return ClassInfo.forType(type.getElementType()); } else if (type.getSort() < org.spongepowered.asm.lib.Type.ARRAY) { return null; } return ClassInfo.forName(type.getClassName().replace('.', '/')); } }