/* * 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.lang.annotation.Annotation; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.spongepowered.asm.lib.Label; import org.spongepowered.asm.lib.Opcodes; import org.spongepowered.asm.lib.Type; import org.spongepowered.asm.lib.signature.SignatureReader; import org.spongepowered.asm.lib.signature.SignatureVisitor; import org.spongepowered.asm.lib.tree.*; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Intrinsic; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.transformer.ClassInfo.Field; import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; import org.spongepowered.asm.mixin.transformer.meta.MixinRenamed; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.util.Bytecode; import org.spongepowered.asm.util.Annotations; import org.spongepowered.asm.util.Constants; import org.spongepowered.asm.util.ConstraintParser; import org.spongepowered.asm.util.ConstraintParser.Constraint; import org.spongepowered.asm.util.throwables.ConstraintViolationException; import org.spongepowered.asm.util.throwables.InvalidConstraintException; import com.google.common.collect.ImmutableList; /** * Applies mixins to a target class */ class MixinApplicatorStandard { /** * Annotations which can have constraints */ protected static final List<Class<? extends Annotation>> CONSTRAINED_ANNOTATIONS = ImmutableList.<Class<? extends Annotation>>of( Overwrite.class, Inject.class, ModifyArg.class, Redirect.class, ModifyVariable.class, ModifyConstant.class ); /** * Passes the mixin applicator applies to each mixin */ enum ApplicatorPass { /** * Main pass, mix in methods, fields, interfaces etc */ MAIN, /** * Enumerate injectors and scan for injection points */ PREINJECT, /** * Apply injectors from previous pass */ INJECT } /** * Strategy for injecting initialiser insns */ enum InitialiserInjectionMode { /** * Default mode, attempts to place initialisers after all other * competing initialisers in the target ctor */ DEFAULT, /** * Safe mode, only injects initialiser directly after the super-ctor * invocation */ SAFE } /** * Internal struct for representing a range */ class Range { /** * Start of the range */ final int start; /** * End of the range */ final int end; /** * Range marker */ final int marker; /** * Create a range with the specified values. * * @param start Start of the range * @param end End of the range * @param marker Arbitrary marker value */ Range(int start, int end, int marker) { this.start = start; this.end = end; this.marker = marker; } /** * Range is valid if both start and end are nonzero and end is after or * at start * * @return true if valid */ boolean isValid() { return (this.start != 0 && this.end != 0 && this.end >= this.start); } /** * Returns true if the supplied value is between or equal to start and * end * * @param value true if the range contains value */ boolean contains(int value) { return value >= this.start && value <= this.end; } /** * Returns true if the supplied value is outside the range * * @param value true if the range does not contain value */ boolean excludes(int value) { return value < this.start || value > this.end; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("Range[%d-%d,%d,valid=%s)", this.start, this.end, this.marker, this.isValid()); } } /** * List of opcodes which must not appear in a class initialiser, mainly a * sanity check so that if any of the specified opcodes are found, we can * log it as an error condition and then people can bitch at me to fix it. * Essentially if it turns out that field initialisers can somehow make use * of local variables, then I need to write some code to ensure that said * locals are shifted so that they don't interfere with locals in the * receiving constructor. */ protected static final int[] INITIALISER_OPCODE_BLACKLIST = { Opcodes.RETURN, Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, Opcodes.IALOAD, Opcodes.LALOAD, Opcodes.FALOAD, Opcodes.DALOAD, Opcodes.AALOAD, Opcodes.BALOAD, Opcodes.CALOAD, Opcodes.SALOAD, Opcodes.ISTORE, Opcodes.LSTORE, Opcodes.FSTORE, Opcodes.DSTORE, Opcodes.ASTORE, Opcodes.IASTORE, Opcodes.LASTORE, Opcodes.FASTORE, Opcodes.DASTORE, Opcodes.AASTORE, Opcodes.BASTORE, Opcodes.CASTORE, Opcodes.SASTORE }; /** * Log more things */ protected final Logger logger = LogManager.getLogger("mixin"); /** * Target class context */ protected final TargetClassContext context; /** * Target class name */ protected final String targetName; /** * Target class tree */ protected final ClassNode targetClass; MixinApplicatorStandard(TargetClassContext context) { this.context = context; this.targetName = context.getClassName(); this.targetClass = context.getClassNode(); } /** * Apply supplied mixins to the target class */ void apply(SortedSet<MixinInfo> mixins) { List<MixinTargetContext> mixinContexts = new ArrayList<MixinTargetContext>(); for (MixinInfo mixin : mixins) { this.logger.log(mixin.getLoggingLevel(), "Mixing {} from {} into {}", mixin.getName(), mixin.getParent(), this.targetName); mixinContexts.add(mixin.createContextFor(this.context)); } MixinTargetContext current = null; try { for (MixinTargetContext context : mixinContexts) { (current = context).preApply(this.targetName, this.targetClass); } for (ApplicatorPass pass : ApplicatorPass.values()) { for (MixinTargetContext context : mixinContexts) { this.applyMixin(current = context, pass); } } for (MixinTargetContext context : mixinContexts) { (current = context).postApply(this.targetName, this.targetClass); } } catch (InvalidMixinException ex) { throw ex; } catch (Exception ex) { throw new InvalidMixinException(current, "Unexpecteded " + ex.getClass().getSimpleName() + " whilst applying the mixin class: " + ex.getMessage(), ex); } this.applySourceMap(this.context); this.context.processDebugTasks(); } /** * Apply the mixin described by mixin to the supplied classNode * * @param mixin Mixin to apply */ protected final void applyMixin(MixinTargetContext mixin, ApplicatorPass pass) { switch (pass) { case MAIN: this.applySignature(mixin); this.applyInterfaces(mixin); this.applyAttributes(mixin); this.applyAnnotations(mixin); this.applyFields(mixin); this.applyMethods(mixin); this.applyInitialisers(mixin); break; case PREINJECT: this.prepareInjections(mixin); break; case INJECT: this.applyAccessors(mixin); this.applyInjections(mixin); break; default: // wat? throw new IllegalStateException("Invalid pass specified " + pass); } } protected void applySignature(MixinTargetContext mixin) { this.context.mergeSignature(mixin.getSignature()); } /** * Mixin interfaces implemented by the mixin class onto the target class * * @param mixin mixin target context */ protected void applyInterfaces(MixinTargetContext mixin) { for (String interfaceName : mixin.getInterfaces()) { if (!this.targetClass.interfaces.contains(interfaceName)) { this.targetClass.interfaces.add(interfaceName); mixin.getTargetClassInfo().addInterface(interfaceName); } } } /** * Mixin misc attributes from mixin class onto the target class * * @param mixin mixin target context */ protected void applyAttributes(MixinTargetContext mixin) { if (mixin.shouldSetSourceFile()) { this.targetClass.sourceFile = mixin.getSourceFile(); } this.targetClass.version = Math.max(this.targetClass.version, mixin.getMinRequiredClassVersion()); } /** * Mixin class-level annotations on the mixin into the target class * * @param mixin mixin target context */ protected void applyAnnotations(MixinTargetContext mixin) { ClassNode sourceClass = mixin.getClassNode(); this.mergeAnnotations(sourceClass, this.targetClass); } /** * Mixin fields from mixin class into the target class. It is vital that * this is done before mixinMethods because we need to compute renamed * fields so that transformMethod can rename field references in the method * body. * * @param mixin mixin target context */ protected void applyFields(MixinTargetContext mixin) { this.mergeShadowFields(mixin); this.mergeNewFields(mixin); } protected void mergeShadowFields(MixinTargetContext mixin) { for (Entry<FieldNode, Field> entry : mixin.getShadowFields()) { FieldNode shadow = entry.getKey(); FieldNode target = this.findTargetField(shadow); if (target != null) { this.mergeAnnotations(shadow, target); // Strip the FINAL flag from @Mutable non-private fields if (entry.getValue().isDecoratedMutable() && !Bytecode.hasFlag(target, Opcodes.ACC_PRIVATE)) { target.access &= ~Opcodes.ACC_FINAL; } } } } protected void mergeNewFields(MixinTargetContext mixin) { for (FieldNode field : mixin.getFields()) { FieldNode target = this.findTargetField(field); if (target == null) { // This is just a local field, so add it this.targetClass.fields.add(field); } } } /** * Mixin methods from the mixin class into the target class * * @param mixin mixin target context */ protected void applyMethods(MixinTargetContext mixin) { for (MethodNode shadow : mixin.getShadowMethods()) { this.applyShadowMethod(mixin, shadow); } for (MethodNode mixinMethod : mixin.getMethods()) { this.applyNormalMethod(mixin, mixinMethod); } } protected void applyShadowMethod(MixinTargetContext mixin, MethodNode shadow) { MethodNode target = this.findTargetMethod(shadow); if (target != null) { this.mergeAnnotations(shadow, target); } } protected void applyNormalMethod(MixinTargetContext mixin, MethodNode mixinMethod) { // Reparent all mixin methods into the target class mixin.transformMethod(mixinMethod); if (!mixinMethod.name.startsWith("<")) { this.checkMethodVisibility(mixin, mixinMethod); this.checkMethodConstraints(mixin, mixinMethod); this.mergeMethod(mixin, mixinMethod); } else if (Constants.CLINIT.equals(mixinMethod.name)) { // Class initialiser insns get appended this.appendInsns(mixin, mixinMethod); } } /** * Attempts to merge the supplied method into the target class * * @param mixin Mixin being applied * @param method Method to merge */ protected void mergeMethod(MixinTargetContext mixin, MethodNode method) { boolean isOverwrite = Annotations.getVisible(method, Overwrite.class) != null; MethodNode target = this.findTargetMethod(method); if (target != null) { if (this.isAlreadyMerged(mixin, method, isOverwrite, target)) { return; } AnnotationNode intrinsic = Annotations.getInvisible(method, Intrinsic.class); if (intrinsic != null) { if (this.mergeIntrinsic(mixin, method, isOverwrite, target, intrinsic)) { return; } } else { this.targetClass.methods.remove(target); } } else if (isOverwrite) { throw new InvalidMixinException(mixin, String.format("Overwrite target \"%s\" was not located in target class %s", method.name, mixin.getTargetClassRef())); } this.targetClass.methods.add(method); mixin.addMergedMethod(method); if (method.signature != null) { SignatureVisitor sv = mixin.getSignature().getRemapper(); new SignatureReader(method.signature).accept(sv); method.signature = sv.toString(); } } /** * Check whether this method was already merged into the target, returns * false if the method was <b>not</b> already merged or if the incoming * method has a higher priority than the already merged method. * * @param mixin Mixin context * @param method Method being merged * @param isOverwrite True if the incoming method is tagged with Override * @param target target method being checked * @return true if the target was already merged and should be skipped */ protected boolean isAlreadyMerged(MixinTargetContext mixin, MethodNode method, boolean isOverwrite, MethodNode target) { AnnotationNode merged = Annotations.getVisible(target, MixinMerged.class); if (merged == null) { if (Annotations.getVisible(target, Final.class) != null) { this.logger.warn("Overwrite prohibited for @Final method {} in {}. Skipping method.", method.name, mixin); return true; } return false; } String sessionId = Annotations.<String>getValue(merged, "sessionId"); if (!this.context.getSessionId().equals(sessionId)) { throw new ClassFormatError("Invalid @MixinMerged annotation found in" + mixin + " at " + method.name + " in " + this.targetClass.name); } if (Bytecode.hasFlag(target, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE) && Bytecode.hasFlag(method, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE)) { if (mixin.getEnvironment().getOption(Option.DEBUG_VERBOSE)) { this.logger.warn("Synthetic bridge method clash for {} in {}", method.name, mixin); } return true; } String owner = Annotations.<String>getValue(merged, "mixin"); int priority = Annotations.<Integer>getValue(merged, "priority"); if (priority >= mixin.getPriority() && !owner.equals(mixin.getClassName())) { this.logger.warn("Method overwrite conflict for {} in {}, previously written by {}. Skipping method.", method.name, mixin, owner); return true; } if (Annotations.getVisible(target, Final.class) != null) { this.logger.warn("Method overwrite conflict for @Final method {} in {} declared by {}. Skipping method.", method.name, mixin, owner); return true; } return false; } /** * Validates and prepares an intrinsic merge, returns true if the intrinsic * check results in a "skip" action, indicating that no further merge action * should be undertaken * * @param mixin Mixin context * @param method Method being merged * @param isOverwrite True if the incoming method is tagged with Override * @param target target method being checked * @param intrinsic {@link Intrinsic} annotation * @return true if the intrinsic method was skipped (short-circuit further * merge operations) */ protected boolean mergeIntrinsic(MixinTargetContext mixin, MethodNode method, boolean isOverwrite, MethodNode target, AnnotationNode intrinsic) { if (isOverwrite) { throw new InvalidMixinException(mixin, "@Intrinsic is not compatible with @Overwrite, remove one of these annotations on " + method.name + " in " + mixin); } String methodName = method.name + method.desc; if (Bytecode.hasFlag(method, Opcodes.ACC_STATIC)) { throw new InvalidMixinException(mixin, "@Intrinsic method cannot be static, found " + methodName + " in " + mixin); } AnnotationNode renamed = Annotations.getVisible(method, MixinRenamed.class); if (renamed == null || !Annotations.getValue(renamed, "isInterfaceMember", Boolean.FALSE)) { throw new InvalidMixinException(mixin, "@Intrinsic method must be prefixed interface method, no rename encountered on " + methodName + " in " + mixin); } if (!Annotations.getValue(intrinsic, "displace", Boolean.FALSE)) { this.logger.log(mixin.getLoggingLevel(), "Skipping Intrinsic mixin method {} for {}", methodName, mixin.getTargetClassRef()); return true; } this.displaceIntrinsic(mixin, method, target); return false; } /** * Handles intrinsic displacement * * @param mixin Mixin context * @param method Method being merged * @param target target method being checked */ protected void displaceIntrinsic(MixinTargetContext mixin, MethodNode method, MethodNode target) { // Deliberately include invalid character in the method name so that // we guarantee no hackiness String proxyName = "proxy+" + target.name; for (Iterator<AbstractInsnNode> iter = method.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn instanceof MethodInsnNode && insn.getOpcode() != Opcodes.INVOKESTATIC) { MethodInsnNode methodNode = (MethodInsnNode)insn; if (methodNode.owner.equals(this.targetClass.name) && methodNode.name.equals(target.name) && methodNode.desc.equals(target.desc)) { methodNode.name = proxyName; } } } target.name = proxyName; } /** * Handles appending instructions from the source method to the target * method. Both methods must return void * * @param mixin mixin target context * @param method source method */ protected final void appendInsns(MixinTargetContext mixin, MethodNode method) { if (Type.getReturnType(method.desc) != Type.VOID_TYPE) { throw new IllegalArgumentException("Attempted to merge insns from a method which does not return void"); } MethodNode target = this.findTargetMethod(method); if (target != null) { AbstractInsnNode returnNode = MixinApplicatorStandard.findInsn(target, Opcodes.RETURN); if (returnNode != null) { Iterator<AbstractInsnNode> injectIter = method.instructions.iterator(); while (injectIter.hasNext()) { AbstractInsnNode insn = injectIter.next(); if (!(insn instanceof LineNumberNode) && insn.getOpcode() != Opcodes.RETURN) { target.instructions.insertBefore(returnNode, insn); } } target.maxLocals = Math.max(target.maxLocals, method.maxLocals); target.maxStack = Math.max(target.maxStack, method.maxStack); } return; } this.targetClass.methods.add(method); } /** * (Attempts to) find and patch field initialisers from the mixin into the * target class * * @param mixin mixin target context */ protected void applyInitialisers(MixinTargetContext mixin) { // Try to find a suitable constructor, we need a constructor with line numbers in order to extract the initialiser MethodNode ctor = this.getConstructor(mixin); if (ctor == null) { return; } // Find the initialiser instructions in the candidate ctor Deque<AbstractInsnNode> initialiser = this.getInitialiser(mixin, ctor); if (initialiser == null || initialiser.size() == 0) { return; } // Patch the initialiser into the target class ctors for (MethodNode method : this.targetClass.methods) { if (Constants.CTOR.equals(method.name)) { method.maxStack = Math.max(method.maxStack, ctor.maxStack); this.injectInitialiser(mixin, method, initialiser); } } } /** * Finds a suitable ctor for reading the instance initialiser bytecode * * @param mixin mixin to search * @return appropriate ctor or null if none found */ protected MethodNode getConstructor(MixinTargetContext mixin) { MethodNode ctor = null; for (MethodNode mixinMethod : mixin.getMethods()) { if (Constants.CTOR.equals(mixinMethod.name) && MixinApplicatorStandard.hasLineNumbers(mixinMethod)) { if (ctor == null) { ctor = mixinMethod; } else { // Not an error condition, just weird this.logger.warn(String.format("Mixin %s has multiple constructors, %s was selected\n", mixin, ctor.desc)); } } } return ctor; } /** * Identifies line numbers in the supplied ctor which correspond to the * start and end of the method body. * * @param ctor constructor to scan * @return range indicating the line numbers of the specified constructor * and the position of the superclass ctor invocation */ private Range getConstructorRange(MethodNode ctor) { boolean lineNumberIsValid = false; AbstractInsnNode endReturn = null; int line = 0, start = 0, end = 0, superIndex = -1; for (Iterator<AbstractInsnNode> iter = ctor.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn instanceof LineNumberNode) { line = ((LineNumberNode)insn).line; lineNumberIsValid = true; } else if (insn instanceof MethodInsnNode) { if (insn.getOpcode() == Opcodes.INVOKESPECIAL && Constants.CTOR.equals(((MethodInsnNode)insn).name) && superIndex == -1) { superIndex = ctor.instructions.indexOf(insn); start = line; } } else if (insn.getOpcode() == Opcodes.PUTFIELD) { lineNumberIsValid = false; } else if (insn.getOpcode() == Opcodes.RETURN) { if (lineNumberIsValid) { end = line; } else { end = start; endReturn = insn; } } } if (endReturn != null) { LabelNode label = new LabelNode(new Label()); ctor.instructions.insertBefore(endReturn, label); ctor.instructions.insertBefore(endReturn, new LineNumberNode(start, label)); } return new Range(start, end, superIndex); } /** * Get insns corresponding to the instance initialiser (hopefully) from the * supplied constructor. * * @param mixin mixin target context * @param ctor constructor to inspect * @return initialiser bytecode extracted from the supplied constructor, or * null if the constructor range could not be parsed */ protected final Deque<AbstractInsnNode> getInitialiser(MixinTargetContext mixin, MethodNode ctor) { // // TODO Potentially rewrite this to be less horrible. // // Find the range of line numbers which corresponds to the constructor body Range init = this.getConstructorRange(ctor); if (!init.isValid()) { return null; } // Now we know where the constructor is, look for insns which lie OUTSIDE the method body int line = 0; Deque<AbstractInsnNode> initialiser = new ArrayDeque<AbstractInsnNode>(); boolean gatherNodes = false; int trimAtOpcode = -1; LabelNode optionalInsn = null; for (Iterator<AbstractInsnNode> iter = ctor.instructions.iterator(init.marker); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn instanceof LineNumberNode) { line = ((LineNumberNode)insn).line; AbstractInsnNode next = ctor.instructions.get(ctor.instructions.indexOf(insn) + 1); if (line == init.end && next.getOpcode() != Opcodes.RETURN) { gatherNodes = true; trimAtOpcode = Opcodes.RETURN; } else { gatherNodes = init.excludes(line); trimAtOpcode = -1; } } else if (gatherNodes) { if (optionalInsn != null) { initialiser.add(optionalInsn); optionalInsn = null; } if (insn instanceof LabelNode) { optionalInsn = (LabelNode)insn; } else { int opcode = insn.getOpcode(); if (opcode == trimAtOpcode) { trimAtOpcode = -1; continue; } for (int ivalidOp : MixinApplicatorStandard.INITIALISER_OPCODE_BLACKLIST) { if (opcode == ivalidOp) { // At the moment I don't handle any transient locals because I haven't seen any in the wild, but let's avoid writing // code which will likely break things and fix it if a real test case ever appears throw new InvalidMixinException(mixin, "Cannot handle " + Bytecode.getOpcodeName(opcode) + " opcode (0x" + Integer.toHexString(opcode).toUpperCase() + ") in class initialiser"); } } initialiser.add(insn); } } } // Check that the last insn is a PUTFIELD, if it's not then AbstractInsnNode last = initialiser.peekLast(); if (last != null) { if (last.getOpcode() != Opcodes.PUTFIELD) { throw new InvalidMixinException(mixin, "Could not parse initialiser, expected 0xB5, found 0x" + Integer.toHexString(last.getOpcode()) + " in " + mixin); } } return initialiser; } /** * Inject initialiser code into the target constructor * * @param mixin mixin target context * @param ctor target constructor * @param initialiser initialiser instructions */ protected final void injectInitialiser(MixinTargetContext mixin, MethodNode ctor, Deque<AbstractInsnNode> initialiser) { Map<LabelNode, LabelNode> labels = Bytecode.cloneLabels(ctor.instructions); AbstractInsnNode insn = this.findInitialiserInjectionPoint(mixin, ctor, initialiser); if (insn == null) { this.logger.warn("Failed to locate initialiser injection point in <init>{}, initialiser was not mixed in.", ctor.desc); return; } for (AbstractInsnNode node : initialiser) { if (node instanceof LabelNode) { continue; } if (node instanceof JumpInsnNode) { throw new InvalidMixinException(mixin, "Unsupported JUMP opcode in initialiser in " + mixin); } AbstractInsnNode imACloneNow = node.clone(labels); ctor.instructions.insert(insn, imACloneNow); insn = imACloneNow; } } /** * Find the injection point for injected initialiser insns in the target * ctor * * @param mixin target context for mixin being applied * @param ctor target ctor * @param initialiser source initialiser insns * @return target node */ protected AbstractInsnNode findInitialiserInjectionPoint(MixinTargetContext mixin, MethodNode ctor, Deque<AbstractInsnNode> initialiser) { Set<String> initialisedFields = new HashSet<String>(); for (AbstractInsnNode initialiserInsn : initialiser) { if (initialiserInsn.getOpcode() == Opcodes.PUTFIELD) { initialisedFields.add(MixinApplicatorStandard.fieldKey((FieldInsnNode)initialiserInsn)); } } InitialiserInjectionMode mode = this.getInitialiserInjectionMode(mixin.getEnvironment()); String targetName = mixin.getTargetClassInfo().getName(); String targetSuperName = mixin.getTargetClassInfo().getSuperName(); AbstractInsnNode targetInsn = null; for (Iterator<AbstractInsnNode> iter = ctor.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn.getOpcode() == Opcodes.INVOKESPECIAL && Constants.CTOR.equals(((MethodInsnNode)insn).name)) { String owner = ((MethodInsnNode)insn).owner; if (owner.equals(targetName) || owner.equals(targetSuperName)) { targetInsn = insn; if (mode == InitialiserInjectionMode.SAFE) { break; } } } else if (insn.getOpcode() == Opcodes.PUTFIELD && mode == InitialiserInjectionMode.DEFAULT) { String key = MixinApplicatorStandard.fieldKey((FieldInsnNode)insn); if (initialisedFields.contains(key)) { targetInsn = insn; } } } return targetInsn; } private InitialiserInjectionMode getInitialiserInjectionMode(MixinEnvironment environment) { String strMode = environment.getOptionValue(Option.INITIALISER_INJECTION_MODE); if (strMode == null) { return InitialiserInjectionMode.DEFAULT; } try { return InitialiserInjectionMode.valueOf(strMode.toUpperCase()); } catch (Exception ex) { this.logger.warn("Could not parse unexpected value \"{}\" for mixin.initialiserInjectionMode, reverting to DEFAULT", strMode); return InitialiserInjectionMode.DEFAULT; } } private static String fieldKey(FieldInsnNode fieldNode) { return String.format("%s:%s", fieldNode.desc, fieldNode.name); } /** * Scan for injector methods and injection points * * @param mixin Mixin being scanned */ protected void prepareInjections(MixinTargetContext mixin) { mixin.prepareInjections(); } /** * Apply all injectors discovered in the previous pass * * @param mixin Mixin being applied */ protected void applyInjections(MixinTargetContext mixin) { mixin.applyInjections(); } /** * Apply all accessors discovered during preprocessing * * @param mixin Mixin being applied */ protected void applyAccessors(MixinTargetContext mixin) { List<MethodNode> accessorMethods = mixin.generateAccessors(); for (MethodNode method : accessorMethods) { if (!method.name.startsWith("<")) { this.mergeMethod(mixin, method); } } } /** * Check visibility before merging a mixin method * * @param mixin mixin target context * @param mixinMethod method to check */ protected void checkMethodVisibility(MixinTargetContext mixin, MethodNode mixinMethod) { if (Bytecode.hasFlag(mixinMethod, Opcodes.ACC_STATIC) && !Bytecode.hasFlag(mixinMethod, Opcodes.ACC_PRIVATE) && !Bytecode.hasFlag(mixinMethod, Opcodes.ACC_SYNTHETIC) && !(Annotations.getVisible(mixinMethod, Overwrite.class) != null)) { throw new InvalidMixinException(mixin, String.format("Mixin %s contains non-private static method %s", mixin, mixinMethod)); } } protected void applySourceMap(TargetClassContext context) { this.targetClass.sourceDebug = context.getSourceMap().toString(); } /** * Check constraints in annotations on the specified mixin method * * @param mixin Target context * @param method Mixin method */ protected void checkMethodConstraints(MixinTargetContext mixin, MethodNode method) { for (Class<? extends Annotation> annotationType : MixinApplicatorStandard.CONSTRAINED_ANNOTATIONS) { AnnotationNode annotation = Annotations.getVisible(method, annotationType); if (annotation != null) { this.checkConstraints(mixin, method, annotation); } } } /** * Check constraints for the specified annotation based on token values in * the current environment * * @param mixin Mixin being applied * @param method annotated method * @param annotation Annotation node to check constraints */ protected final void checkConstraints(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { try { Constraint constraint = ConstraintParser.parse(annotation); MixinEnvironment environment = MixinEnvironment.getCurrentEnvironment(); try { constraint.check(environment); } catch (ConstraintViolationException ex) { String message = String.format("Constraint violation: %s on %s in %s", ex.getMessage(), method, mixin); this.logger.warn(message); if (!environment.getOption(Option.IGNORE_CONSTRAINTS)) { throw new InvalidMixinException(mixin, message, ex); } } } catch (InvalidConstraintException ex) { throw new InvalidMixinException(mixin, ex.getMessage()); } } /** * Merge annotations from the specified source ClassNode to the destination * ClassNode, replaces annotations of the equivalent type on the target with * annotations from the source. If the source node has no annotations then * no action will take place, if the target node has no annotations then a * new annotation list will be created. Annotations from the mixin package * are not merged. * * @param from ClassNode to merge annotations from * @param to ClassNode to merge annotations to */ protected final void mergeAnnotations(ClassNode from, ClassNode to) { to.visibleAnnotations = this.mergeAnnotations(from.visibleAnnotations, to.visibleAnnotations, from.name); to.invisibleAnnotations = this.mergeAnnotations(from.invisibleAnnotations, to.invisibleAnnotations, from.name); } /** * Merge annotations from the specified source MethodNode to the destination * MethodNode, replaces annotations of the equivalent type on the target * with annotations from the source. If the source node has no annotations * then no action will take place, if the target node has no annotations * then a new annotation list will be created. Annotations from the mixin * package are not merged. * * @param from MethodNode to merge annotations from * @param to MethodNode to merge annotations to */ protected final void mergeAnnotations(MethodNode from, MethodNode to) { to.visibleAnnotations = this.mergeAnnotations(from.visibleAnnotations, to.visibleAnnotations, from.name); to.invisibleAnnotations = this.mergeAnnotations(from.invisibleAnnotations, to.invisibleAnnotations, from.name); } /** * Merge annotations from the specified source FieldNode to the destination * FieldNode, replaces annotations of the equivalent type on the target with * annotations from the source. If the source node has no annotations then * no action will take place, if the target node has no annotations then a * new annotation list will be created. Annotations from the mixin package * are not merged. * * @param from FieldNode to merge annotations from * @param to FieldNode to merge annotations to */ protected final void mergeAnnotations(FieldNode from, FieldNode to) { to.visibleAnnotations = this.mergeAnnotations(from.visibleAnnotations, to.visibleAnnotations, from.name); to.invisibleAnnotations = this.mergeAnnotations(from.invisibleAnnotations, to.invisibleAnnotations, from.name); } /** * Merge annotations from the source list to the target list. Returns the * target list or a new list if the target list was null. * * @param from Annotations to merge * @param to Annotation list to merge into * @param name Name of the item being merged, for debugging purposes * @return The merged list (or a new list if the target list was null) */ private List<AnnotationNode> mergeAnnotations(List<AnnotationNode> from, List<AnnotationNode> to, String name) { try { if (from == null) { return to; } if (to == null) { to = new ArrayList<AnnotationNode>(); } for (AnnotationNode annotation : from) { if (!this.isMergeableAnnotation(annotation)) { continue; } for (Iterator<AnnotationNode> iter = to.iterator(); iter.hasNext();) { if (iter.next().desc.equals(annotation.desc)) { iter.remove(); break; } } to.add(annotation); } } catch (Exception ex) { this.logger.warn("Exception encountered whilst merging annotations for {}", name); } return to; } private boolean isMergeableAnnotation(AnnotationNode annotation) { if (annotation.desc.startsWith("L" + Constants.MIXIN_PACKAGE_REF)) { return annotation.desc.endsWith("Debug;"); } return true; } /** * Find the first insn node with a matching opcode in the specified method * * @param method method to search * @param opcode opcode to search for * @return found node or null if not found */ protected static AbstractInsnNode findInsn(MethodNode method, int opcode) { Iterator<AbstractInsnNode> findReturnIter = method.instructions.iterator(); while (findReturnIter.hasNext()) { AbstractInsnNode insn = findReturnIter.next(); if (insn.getOpcode() == opcode) { return insn; } } return null; } /** * Returns true if the supplied method contains any line number information * * @param method Method to scan * @return true if a line number node is located */ private static boolean hasLineNumbers(MethodNode method) { for (Iterator<AbstractInsnNode> iter = method.instructions.iterator(); iter.hasNext();) { if (iter.next() instanceof LineNumberNode) { return true; } } return false; } /** * Finds a method in the target class * @param searchFor * * @return Target method matching searchFor, or null if not found */ protected final MethodNode findTargetMethod(MethodNode searchFor) { for (MethodNode target : this.targetClass.methods) { if (target.name.equals(searchFor.name) && target.desc.equals(searchFor.desc)) { return target; } } return null; } /** * Finds a field in the target class * @param searchFor * * @return Target field matching searchFor, or null if not found */ protected final FieldNode findTargetField(FieldNode searchFor) { for (FieldNode target : this.targetClass.fields) { if (target.name.equals(searchFor.name)) { return target; } } return null; } }