/*
* 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.gen;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.tree.FieldNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.mixin.MixinEnvironment.Option;
import org.spongepowered.asm.mixin.gen.throwables.InvalidAccessorException;
import org.spongepowered.asm.mixin.injection.struct.MemberInfo;
import org.spongepowered.asm.mixin.refmap.IMixinContext;
import org.spongepowered.asm.mixin.struct.SpecialMethodInfo;
import org.spongepowered.asm.mixin.transformer.MixinTargetContext;
import org.spongepowered.asm.util.Bytecode;
import org.spongepowered.asm.util.Annotations;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
/**
* Information about an accessor
*/
public class AccessorInfo extends SpecialMethodInfo {
/**
* Accessor types
*/
public enum AccessorType {
/**
* A field getter, accessor must accept no args and return field type
*/
FIELD_GETTER(ImmutableSet.<String>of("get", "is")) {
@Override
AccessorGenerator getGenerator(AccessorInfo info) {
return new AccessorGeneratorFieldGetter(info);
}
},
/**
* A field setter, accessor must accept single arg of the field type and
* return void
*/
FIELD_SETTER(ImmutableSet.<String>of("set")) {
@Override
AccessorGenerator getGenerator(AccessorInfo info) {
return new AccessorGeneratorFieldSetter(info);
}
},
/**
* An invoker (proxy) method
*/
METHOD_PROXY(ImmutableSet.<String>of("call", "invoke")) {
@Override
AccessorGenerator getGenerator(AccessorInfo info) {
return new AccessorGeneratorMethodProxy(info);
}
};
private final Set<String> expectedPrefixes;
private AccessorType(Set<String> expectedPrefixes) {
this.expectedPrefixes = expectedPrefixes;
}
/**
* Returns true if the supplied prefix string is an allowed prefix for
* this accessor type
*
* @param prefix prefix to check
* @return true if the expected prefix set contains the supplied value
*/
public boolean isExpectedPrefix(String prefix) {
return this.expectedPrefixes.contains(prefix);
}
/**
* Returns all the expected prefixes for this accessor type as a string
* for debugging/error message purposes
*
* @return string representation of expected prefixes for this accessor
* type
*/
public String getExpectedPrefixes() {
return this.expectedPrefixes.toString();
}
abstract AccessorGenerator getGenerator(AccessorInfo info);
}
/**
* Pattern for matching accessor names (for inflector)
*/
protected static final Pattern PATTERN_ACCESSOR = Pattern.compile("^(get|set|is|invoke|call)(([A-Z])(.*?))(_\\$md.*)?$");
/**
* Accessor method argument types (raw, from method)
*/
protected final Type[] argTypes;
/**
* Accessor method return type (raw, from method)
*/
protected final Type returnType;
/**
* Type of accessor to generate, computed based on the signature of the
* target method.
*/
protected final AccessorType type;
/**
* For field accessors, the expected type of the target field
*/
private final Type targetFieldType;
/**
* Computed information about the target field or method, name and
* descriptor
*/
protected final MemberInfo target;
/**
* For accessors, stores the discovered target field
*/
protected FieldNode targetField;
/**
* For invokers, stores the discovered target method
*/
protected MethodNode targetMethod;
public AccessorInfo(MixinTargetContext mixin, MethodNode method) {
this(mixin, method, Accessor.class);
}
protected AccessorInfo(MixinTargetContext mixin, MethodNode method, Class<? extends Annotation> annotationClass) {
super(mixin, method, Annotations.getVisible(method, annotationClass));
this.argTypes = Type.getArgumentTypes(method.desc);
this.returnType = Type.getReturnType(method.desc);
this.type = this.initType();
this.targetFieldType = this.initTargetFieldType();
this.target = this.initTarget();
}
protected AccessorType initType() {
if (this.returnType.equals(Type.VOID_TYPE)) {
return AccessorType.FIELD_SETTER;
}
return AccessorType.FIELD_GETTER;
}
protected Type initTargetFieldType() {
switch (this.type) {
case FIELD_GETTER:
if (this.argTypes.length > 0) {
throw new InvalidAccessorException(this.mixin, this + " must take exactly 0 arguments, found " + this.argTypes.length);
}
return this.returnType;
case FIELD_SETTER:
if (this.argTypes.length != 1) {
throw new InvalidAccessorException(this.mixin, this + " must take exactly 1 argument, found " + this.argTypes.length);
}
return this.argTypes[0];
default:
throw new InvalidAccessorException(this.mixin, "Computed unsupported accessor type " + this.type + " for " + this);
}
}
protected MemberInfo initTarget() {
MemberInfo target = new MemberInfo(this.getTargetName(), null, this.targetFieldType.getDescriptor());
this.annotation.visit("target", target.toString());
return target;
}
protected String getTargetName() {
String name = Annotations.<String>getValue(this.annotation);
if (Strings.isNullOrEmpty(name)) {
String inflectedTarget = this.inflectTarget();
if (inflectedTarget == null) {
throw new InvalidAccessorException(this.mixin, "Failed to inflect target name for " + this + ", supported prefixes: [get, set, is]");
}
return inflectedTarget;
}
return MemberInfo.parse(name, this.mixin).name;
}
/**
* Uses the name of this accessor method and the calculated accessor type to
* try and inflect the name of the target field or method. This allows a
* method named <tt>getFoo</tt> to be inflected to a target named
* <tt>foo</tt> for example.
*/
protected String inflectTarget() {
return AccessorInfo.inflectTarget(this.method.name, this.type, this.toString(), this.mixin,
this.mixin.getEnvironment().getOption(Option.DEBUG_VERBOSE));
}
/**
* Uses the name of an accessor method and the accessor type to try and
* inflect the name of the target field or method. This allows a method
* named <tt>getFoo</tt> to be inflected to a target named <tt>foo</tt> for
* example.
*
* @param accessorName Name of the accessor method
* @param accessorType Type of accessor being processed, this is calculated
* from the method signature (<tt>void</tt> methods being setters,
* methods with return types being getters)
* @param accessorDescription description of the accessor to include in
* error messages
* @param context Mixin context
* @param verbose Emit warnings when accessor prefix doesn't match type
* @return inflected target member name or <tt>null</tt> if name cannot be
* inflected
*/
public static String inflectTarget(String accessorName, AccessorType accessorType, String accessorDescription,
IMixinContext context, boolean verbose) {
Matcher nameMatcher = AccessorInfo.PATTERN_ACCESSOR.matcher(accessorName);
if (nameMatcher.matches()) {
String prefix = nameMatcher.group(1);
String firstChar = nameMatcher.group(3);
String remainder = nameMatcher.group(4);
// If the entire name is upper case, do not lowercase the first char
String name = String.format("%s%s", AccessorInfo.toLowerCase(firstChar, !AccessorInfo.isUpperCase(remainder)), remainder);
if (!accessorType.isExpectedPrefix(prefix) && verbose) {
LogManager.getLogger("mixin").warn("Unexpected prefix for {}, found [{}] expecting {}", accessorDescription, prefix,
accessorType.getExpectedPrefixes());
}
return MemberInfo.parse(name, context).name;
}
return null;
}
/**
* Get the inflected/specified target member for this accessor
*/
public final MemberInfo getTarget() {
return this.target;
}
/**
* For field accessors, returns the field type, returns null for invokers
*/
public final Type getTargetFieldType() {
return this.targetFieldType;
}
/**
* For field accessors, returns the target field, returns null for invokers
*/
public final FieldNode getTargetField() {
return this.targetField;
}
/**
* For invokers, returns the target method, returns null for field accessors
*/
public final MethodNode getTargetMethod() {
return this.targetMethod;
}
/**
* Get the return type of the annotated method
*/
public final Type getReturnType() {
return this.returnType;
}
/**
* Get the argument types of the annotated method
*/
public final Type[] getArgTypes() {
return this.argTypes;
}
@Override
public String toString() {
return String.format("%s->@%s[%s]::%s%s", this.mixin.toString(), Bytecode.getSimpleName(this.annotation), this.type.toString(),
this.method.name, this.method.desc);
}
/**
* First pass, locate the target field in the class. This is done after all
* other mixins are applied so that mixin-added fields and methods can be
* targetted.
*/
public void locate() {
this.targetField = this.findTargetField();
}
/**
* Second pass, generate the actual accessor method for this accessor. The
* method still respects intrinsic/mixinmerged rules so is not guaranteed to
* be added to the target class
*
* @return generated accessor method
*/
public MethodNode generate() {
return this.type.getGenerator(this).generate();
}
private FieldNode findTargetField() {
return this.<FieldNode>findTarget(this.classNode.fields);
}
/**
* Generified candidate search, since the search logic is the same for both
* fields and methods.
*
* @param nodes Node list to search (method/field list)
* @param <TNode> node type
* @return best match
*/
protected <TNode> TNode findTarget(List<TNode> nodes) {
TNode exactMatch = null;
List<TNode> candidates = new ArrayList<TNode>();
for (TNode node : nodes) {
String desc = AccessorInfo.<TNode>getNodeDesc(node);
if (desc == null || !desc.equals(this.target.desc)) {
continue;
}
String name = AccessorInfo.<TNode>getNodeName(node);
if (name != null) {
if (name.equals(this.target.name)) {
exactMatch = node;
}
if (name.equalsIgnoreCase(this.target.name)) {
candidates.add(node);
}
}
}
if (exactMatch != null) {
if (candidates.size() > 1) {
LogManager.getLogger("mixin").debug("{} found an exact match for {} but other candidates were found!", this, this.target);
}
return exactMatch;
}
if (candidates.size() == 1) {
return candidates.get(0);
}
String number = candidates.size() == 0 ? "No" : "Multiple";
throw new InvalidAccessorException(this, number + " candidates were found matching " + this.target + " in " + this.classNode.name
+ " for " + this);
}
private static <TNode> String getNodeDesc(TNode node) {
return (node instanceof MethodNode) ? ((MethodNode)node).desc : ((node instanceof FieldNode) ? ((FieldNode)node).desc : null);
}
private static <TNode> String getNodeName(TNode node) {
return (node instanceof MethodNode) ? ((MethodNode)node).name : ((node instanceof FieldNode) ? ((FieldNode)node).name : null);
}
/**
* Return a wrapper AccessorInfo of the correct type based on the method
* passed in.
*
* @param mixin mixin context which owns this accessor
* @param method annotated method
* @param type annotation type to process
* @return parsed AccessorInfo
*/
public static AccessorInfo of(MixinTargetContext mixin, MethodNode method, Class<? extends Annotation> type) {
if (type == Accessor.class) {
return new AccessorInfo(mixin, method);
} else if (type == Invoker.class) {
return new InvokerInfo(mixin, method);
}
throw new InvalidAccessorException(mixin, "Could not parse accessor for unknown type " + type.getName());
}
private static String toLowerCase(String string, boolean condition) {
return condition ? string.toLowerCase() : string;
}
private static boolean isUpperCase(String string) {
return string.toUpperCase().equals(string);
}
}