/* * 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.Iterator; 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.AnnotationNode; import org.spongepowered.asm.lib.tree.FieldInsnNode; import org.spongepowered.asm.lib.tree.FieldNode; import org.spongepowered.asm.lib.tree.MethodInsnNode; import org.spongepowered.asm.lib.tree.MethodNode; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Invoker; import org.spongepowered.asm.mixin.gen.throwables.InvalidAccessorException; import org.spongepowered.asm.mixin.transformer.ClassInfo.Field; import org.spongepowered.asm.mixin.transformer.ClassInfo.Method; import org.spongepowered.asm.mixin.transformer.ClassInfo.SearchType; import org.spongepowered.asm.mixin.transformer.MixinInfo.MixinClassNode; import org.spongepowered.asm.mixin.transformer.MixinInfo.MixinMethodNode; import org.spongepowered.asm.mixin.transformer.meta.MixinRenamed; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.util.Annotations; import org.spongepowered.asm.util.Bytecode; import org.spongepowered.asm.util.Bytecode.Visibility; import org.spongepowered.asm.util.Constants; /** * <p>Mixin bytecode pre-processor. This class is responsible for bytecode pre- * processing tasks required to be performed on mixin bytecode before the mixin * can be applied. In previous versions the duties performed by this class were * performed by {@link MixinInfo}.</p> * * <p>Before a mixin can be applied to the target class, it is necessary to * convert certain aspects of the mixin bytecode into the intended final form of * the mixin, this involves for example stripping the prefix from shadow and * soft-implemented methods. This preparation is done in two stages: first the * target-context-insensitive transformations are applied (this also acts as a * validation pass when the mixin is first loaded) and then transformations * which depend on the target class are applied in a second stage.</p> * * <p>The validation pass propagates method renames into the metadata tree and * thus changes made during this phase are visible to all other mixins. The * target-context-sensitive pass on the other hand can only operate on private * class members for obvious reasons.</p> */ class MixinPreProcessorStandard { /** * Logger */ private static final Logger logger = LogManager.getLogger("mixin"); /** * The mixin */ protected final MixinInfo mixin; /** * Mixin class node */ protected final MixinClassNode classNode; protected final MixinEnvironment env; private final boolean verboseLogging, strictUnique; private boolean prepared, attached; MixinPreProcessorStandard(MixinInfo mixin, MixinClassNode classNode) { this.mixin = mixin; this.classNode = classNode; this.env = mixin.getParent().getEnvironment(); this.verboseLogging = this.env.getOption(Option.DEBUG_VERBOSE); this.strictUnique = this.env.getOption(Option.DEBUG_UNIQUE); } /** * Run the first pass. Propagates changes into the metadata tree. * * @return Prepared classnode */ MixinPreProcessorStandard prepare() { if (this.prepared) { return this; } this.prepared = true; for (MixinMethodNode mixinMethod : this.classNode.mixinMethods) { Method method = this.mixin.getClassInfo().findMethod(mixinMethod); this.prepareMethod(mixinMethod, method); } for (FieldNode mixinField : this.classNode.fields) { this.prepareField(mixinField); } return this; } protected void prepareMethod(MixinMethodNode mixinMethod, Method method) { this.prepareShadow(mixinMethod, method); this.prepareSoftImplements(mixinMethod, method); } protected void prepareShadow(MixinMethodNode mixinMethod, Method method) { AnnotationNode shadowAnnotation = Annotations.getVisible(mixinMethod, Shadow.class); if (shadowAnnotation == null) { return; } String prefix = Annotations.<String>getValue(shadowAnnotation, "prefix", Shadow.class); if (mixinMethod.name.startsWith(prefix)) { Annotations.setVisible(mixinMethod, MixinRenamed.class, "originalName", mixinMethod.name); String newName = mixinMethod.name.substring(prefix.length()); mixinMethod.name = method.renameTo(newName); } } protected void prepareSoftImplements(MixinMethodNode mixinMethod, Method method) { for (InterfaceInfo iface : this.mixin.getSoftImplements()) { if (iface.renameMethod(mixinMethod)) { method.renameTo(mixinMethod.name); } } } protected void prepareField(FieldNode mixinField) { // stub } MixinPreProcessorStandard conform(TargetClassContext target) { return this.conform(target.getClassInfo()); } MixinPreProcessorStandard conform(ClassInfo target) { for (MixinMethodNode mixinMethod : this.classNode.mixinMethods) { if (mixinMethod.isInjector()) { Method method = this.mixin.getClassInfo().findMethod(mixinMethod, ClassInfo.INCLUDE_ALL); this.conformInjector(target, mixinMethod, method); } } return this; } private void conformInjector(ClassInfo targetClass, MixinMethodNode mixinMethod, Method method) { MethodMapper methodMapper = targetClass.getMethodMapper(); methodMapper.remapHandlerMethod(this.mixin, mixinMethod, method); } MixinTargetContext createContextFor(TargetClassContext target) { MixinTargetContext context = new MixinTargetContext(this.mixin, this.classNode, target); this.conform(target); this.attach(context); return context; } /** * Run the second pass, attach to the specified context * * @param context mixin target context */ MixinPreProcessorStandard attach(MixinTargetContext context) { if (this.attached) { throw new IllegalStateException("Preprocessor was already attached"); } this.attached = true; // Perform context-sensitive attachment phase this.attachMethods(context); this.attachFields(context); // Apply transformations to the mixin bytecode this.transform(context); return this; } protected void attachMethods(MixinTargetContext context) { for (Iterator<MixinMethodNode> iter = this.classNode.mixinMethods.iterator(); iter.hasNext();) { MixinMethodNode mixinMethod = iter.next(); if (!this.validateMethod(context, mixinMethod)) { iter.remove(); continue; } if (this.attachInjectorMethod(context, mixinMethod)) { continue; } if (this.attachAccessorMethod(context, mixinMethod)) { iter.remove(); continue; } if (this.attachShadowMethod(context, mixinMethod)) { iter.remove(); continue; } if (this.attachOverwriteMethod(context, mixinMethod)) { continue; } if (this.attachUniqueMethod(context, mixinMethod)) { iter.remove(); continue; } this.attachMethod(mixinMethod); } } protected boolean validateMethod(MixinTargetContext context, MixinMethodNode mixinMethod) { return true; } protected boolean attachInjectorMethod(MixinTargetContext context, MixinMethodNode mixinMethod) { return mixinMethod.isInjector(); } protected boolean attachAccessorMethod(MixinTargetContext context, MixinMethodNode mixinMethod) { return this.attachAccessorMethod(context, mixinMethod, Accessor.class) || this.attachAccessorMethod(context, mixinMethod, Invoker.class); } protected boolean attachAccessorMethod(MixinTargetContext context, MixinMethodNode mixinMethod, Class<? extends Annotation> type) { AnnotationNode annotation = mixinMethod.getVisibleAnnotation(type); if (annotation == null) { return false; } String description = "@" + Bytecode.getSimpleName(type) + " method " + mixinMethod.name; Method method = this.getSpecialMethod(mixinMethod, type); if (MixinEnvironment.getCompatibilityLevel().isAtLeast(CompatibilityLevel.JAVA_8) && method.isStatic()) { if (this.mixin.getTargets().size() > 1) { throw new InvalidAccessorException(context, description + " in multi-target mixin is invalid. Mixin must have exactly 1 target."); } String uniqueName = context.getUniqueName(mixinMethod, true); MixinPreProcessorStandard.logger.log(this.mixin.getLoggingLevel(), "Renaming @Unique method {}{} to {} in {}", mixinMethod.name, mixinMethod.desc, uniqueName, this.mixin); mixinMethod.name = method.renameTo(uniqueName); } else { if (!method.isAbstract()) { throw new InvalidAccessorException(context, description + " is not abstract"); } if (method.isStatic()) { throw new InvalidAccessorException(context, description + " cannot be static"); } } context.addAccessorMethod(mixinMethod, type); return true; } protected boolean attachShadowMethod(MixinTargetContext context, MixinMethodNode mixinMethod) { if (this.attachSpecialMethod(context, mixinMethod, Shadow.class, false)) { context.addShadowMethod(mixinMethod); return true; } return false; } protected boolean attachOverwriteMethod(MixinTargetContext context, MixinMethodNode mixinMethod) { return this.attachSpecialMethod(context, mixinMethod, Overwrite.class, true); } protected boolean attachSpecialMethod(MixinTargetContext context, MixinMethodNode mixinMethod, Class<? extends Annotation> type, boolean overwrite) { AnnotationNode annotation = mixinMethod.getVisibleAnnotation(type); if (annotation == null) { return false; } if (overwrite) { this.checkMixinNotUnique(mixinMethod, type); } String description = "@" + Bytecode.getSimpleName(type); Method method = this.getSpecialMethod(mixinMethod, type); MethodNode target = context.findMethod(mixinMethod, annotation); if (target == null) { if (overwrite) { return false; } target = context.findRemappedMethod(mixinMethod); if (target == null) { throw new InvalidMixinException(this.mixin, description + " method " + mixinMethod.name + " in " + this.mixin + " was not located in the target class"); } mixinMethod.name = method.renameTo(target.name); } if (Constants.CTOR.equals(target.name)) { throw new InvalidMixinException(this.mixin, "Nice try! " + mixinMethod.name + " in " + this.mixin + " cannot alias a constructor!"); } if (!Bytecode.compareFlags(mixinMethod, target, Opcodes.ACC_STATIC)) { throw new InvalidMixinException(this.mixin, "STATIC modifier of " + description + " method " + mixinMethod.name + " in " + this.mixin + " does not match the target"); } if (overwrite) { Visibility visTarget = Bytecode.getVisibility(target); Visibility visMethod = Bytecode.getVisibility(mixinMethod); if (visMethod.ordinal() < visTarget.ordinal()) { throw new InvalidMixinException(this.mixin, visMethod + " " + description + " method " + mixinMethod.name + " in " + this.mixin + " cannot reduce visibiliy of " + visTarget + " target method"); } } if (!target.name.equals(mixinMethod.name)) { if (!overwrite && (target.access & Opcodes.ACC_PRIVATE) == 0) { throw new InvalidMixinException(this.mixin, "Non-private method cannot be aliased. Found " + target.name); } mixinMethod.name = method.renameTo(target.name); } return true; } protected Method getSpecialMethod(MixinMethodNode mixinMethod, Class<? extends Annotation> type) { Method method = this.mixin.getClassInfo().findMethod(mixinMethod, ClassInfo.INCLUDE_ALL); this.checkMethodNotUnique(method, type); return method; } protected void checkMethodNotUnique(Method method, Class<? extends Annotation> type) { if (method.isUnique()) { String description = "@" + Bytecode.getSimpleName(type); throw new InvalidMixinException(this.mixin, description + " method " + method.getName() + " cannot be @Unique"); } } protected void checkMixinNotUnique(MixinMethodNode mixinMethod, Class<? extends Annotation> type) { if (this.mixin.isUnique()) { String description = "@" + Bytecode.getSimpleName(type); throw new InvalidMixinException(this.mixin, description + " method " + mixinMethod.name + " found in a @Unique mixin"); } } protected boolean attachUniqueMethod(MixinTargetContext context, MixinMethodNode mixinMethod) { Method method = this.mixin.getClassInfo().findMethod(mixinMethod, ClassInfo.INCLUDE_ALL); if (method == null || (!method.isUnique() && !this.mixin.isUnique())) { return false; } MethodNode target = context.findMethod(mixinMethod, null); if (target == null) { return false; } if ((mixinMethod.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) != 0) { String uniqueName = context.getUniqueName(mixinMethod, false); MixinPreProcessorStandard.logger.log(this.mixin.getLoggingLevel(), "Renaming @Unique method {}{} to {} in {}", mixinMethod.name, mixinMethod.desc, uniqueName, this.mixin); mixinMethod.name = method.renameTo(uniqueName); return false; } if (this.strictUnique) { throw new InvalidMixinException(this.mixin, "Method conflict, @Unique method " + mixinMethod.name + " in " + this.mixin + " cannot overwrite " + target.name + target.desc + " in " + context.getTarget()); } MixinPreProcessorStandard.logger.warn("Discarding @Unique public method {} in {} because it already exists in {}", mixinMethod.name, this.mixin, context.getTarget()); return true; } protected void attachMethod(MixinMethodNode mixinMethod) { Method method = this.mixin.getClassInfo().findMethod(mixinMethod); if (method == null) { return; } Method parentMethod = this.mixin.getClassInfo().findMethodInHierarchy(mixinMethod, SearchType.SUPER_CLASSES_ONLY); if (parentMethod != null && parentMethod.isRenamed()) { mixinMethod.name = method.renameTo(parentMethod.getName()); } } protected void attachFields(MixinTargetContext context) { for (Iterator<FieldNode> iter = this.classNode.fields.iterator(); iter.hasNext();) { FieldNode mixinField = iter.next(); AnnotationNode shadow = Annotations.getVisible(mixinField, Shadow.class); boolean isShadow = shadow != null; if (!this.validateField(context, mixinField, shadow)) { iter.remove(); continue; } context.transformDescriptor(mixinField); Field field = this.mixin.getClassInfo().findField(mixinField); if (field.isUnique() && isShadow) { throw new InvalidMixinException(this.mixin, "@Shadow field " + mixinField.name + " cannot be @Unique"); } FieldNode target = context.findField(mixinField, shadow); if (target == null) { if (shadow == null) { continue; } target = context.findRemappedField(mixinField); if (target == null) { // If this field is a shadow field but is NOT found in the target class, that's bad, mmkay throw new InvalidMixinException(this.mixin, "Shadow field " + mixinField.name + " was not located in the target class"); } mixinField.name = field.renameTo(target.name); } if (!Bytecode.compareFlags(mixinField, target, Opcodes.ACC_STATIC)) { throw new InvalidMixinException(this.mixin, "STATIC modifier of @Shadow field " + mixinField.name + " in " + this.mixin + " does not match the target"); } if (field.isUnique()) { if ((mixinField.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) != 0) { String uniqueName = context.getUniqueName(mixinField); MixinPreProcessorStandard.logger.log(this.mixin.getLoggingLevel(), "Renaming @Unique field {}{} to {} in {}", mixinField.name, mixinField.desc, uniqueName, this.mixin); mixinField.name = field.renameTo(uniqueName); continue; } if (this.strictUnique) { throw new InvalidMixinException(this.mixin, "Field conflict, @Unique field " + mixinField.name + " in " + this.mixin + " cannot overwrite " + target.name + target.desc + " in " + context.getTarget()); } MixinPreProcessorStandard.logger.warn("Discarding @Unique public field {} in {} because it already exists in {}. " + "Note that declared FIELD INITIALISERS will NOT be removed!", mixinField.name, this.mixin, context.getTarget()); iter.remove(); continue; } // Check that the shadow field has a matching descriptor if (!target.desc.equals(mixinField.desc)) { throw new InvalidMixinException(this.mixin, "The field " + mixinField.name + " in the target class has a conflicting signature"); } if (!target.name.equals(mixinField.name)) { if ((target.access & Opcodes.ACC_PRIVATE) == 0 && (target.access & Opcodes.ACC_SYNTHETIC) == 0) { throw new InvalidMixinException(this.mixin, "Non-private field cannot be aliased. Found " + target.name); } mixinField.name = field.renameTo(target.name); } // Shadow fields get stripped from the mixin class iter.remove(); if (isShadow) { boolean isFinal = field.isDecoratedFinal(); if (this.verboseLogging && Bytecode.hasFlag(target, Opcodes.ACC_FINAL) != isFinal) { String message = isFinal ? "@Shadow field {}::{} is decorated with @Final but target is not final" : "@Shadow target {}::{} is final but shadow is not decorated with @Final"; MixinPreProcessorStandard.logger.warn(message, this.mixin, mixinField.name); } context.addShadowField(mixinField, field); } } } protected boolean validateField(MixinTargetContext context, FieldNode field, AnnotationNode shadow) { // Public static fields will fall foul of early static binding in java, including them in a mixin is an error condition if (Bytecode.hasFlag(field, Opcodes.ACC_STATIC) && !Bytecode.hasFlag(field, Opcodes.ACC_PRIVATE) && !Bytecode.hasFlag(field, Opcodes.ACC_SYNTHETIC) && shadow == null) { throw new InvalidMixinException(context, String.format("Mixin %s contains non-private static field %s:%s", context, field.name, field.desc)); } // Shadow fields can't have prefixes, it's meaningless for them anyway String prefix = Annotations.<String>getValue(shadow, "prefix", Shadow.class); if (field.name.startsWith(prefix)) { throw new InvalidMixinException(context, String.format("@Shadow field %s.%s has a shadow prefix. This is not allowed.", context, field.name)); } // Imaginary super fields get stripped from the class, but first we validate them if (Constants.IMAGINARY_SUPER.equals(field.name)) { if (field.access != Opcodes.ACC_PRIVATE) { throw new InvalidMixinException(this.mixin, "Imaginary super field " + context + "." + field.name + " must be private and non-final"); } if (!field.desc.equals("L" + this.mixin.getClassRef() + ";")) { throw new InvalidMixinException(this.mixin, "Imaginary super field " + context + "." + field.name + " must have the same type as the parent mixin"); } return false; } return true; } /** * Apply discovered method and field renames to method invocations and field * accesses in the mixin */ protected void transform(MixinTargetContext context) { for (MethodNode mixinMethod : this.classNode.methods) { for (Iterator<AbstractInsnNode> iter = mixinMethod.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn instanceof MethodInsnNode) { MethodInsnNode methodNode = (MethodInsnNode)insn; Method method = ClassInfo.forName(methodNode.owner).findMethodInHierarchy(methodNode, SearchType.ALL_CLASSES, ClassInfo.INCLUDE_PRIVATE); if (method != null && method.isRenamed()) { methodNode.name = method.getName(); } } else if (insn instanceof FieldInsnNode) { FieldInsnNode fieldNode = (FieldInsnNode)insn; Field field = ClassInfo.forName(fieldNode.owner).findField(fieldNode, ClassInfo.INCLUDE_PRIVATE); if (field != null && field.isRenamed()) { fieldNode.name = field.getName(); } } } } } }