package org.stianloader.micromixin.remapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import org.jetbrains.annotations.ApiStatus.Internal;
import org.jetbrains.annotations.ApiStatus.OverrideOnly;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.stianloader.micromixin.remapper.selectors.AtSelector;
import org.stianloader.micromixin.remapper.selectors.ConstantSelector;
import org.stianloader.micromixin.remapper.selectors.FieldSelector;
import org.stianloader.micromixin.remapper.selectors.HeadSelector;
import org.stianloader.micromixin.remapper.selectors.InvokeSelector;
import org.stianloader.micromixin.remapper.selectors.NewSelector;
import org.stianloader.micromixin.remapper.selectors.ReturnSelector;
import org.stianloader.micromixin.remapper.selectors.TailSelector;
import org.stianloader.remapper.MappingLookup;
import org.stianloader.remapper.MappingSink;
import org.stianloader.remapper.MemberRef;
import org.stianloader.remapper.Remapper;
public class MicromixinRemapper {
@NotNull
@Internal
public static final String CALLBACK_INFO_CLASS = "org/spongepowered/asm/mixin/injection/callback/CallbackInfo";
@NotNull
@Internal
public static final String CALLBACK_INFO_RETURNABLE_CLASS = "org/spongepowered/asm/mixin/injection/callback/CallbackInfoReturnable";
@NotNull
private final MemberLister lister;
@NotNull
private final MappingLookup lookup;
@NotNull
private final MappingSink sink;
public MicromixinRemapper(@NotNull MappingLookup lookup, @NotNull MappingSink sink, @NotNull MemberLister lister) {
this.lookup = lookup;
this.sink = sink;
this.lister = lister;
}
/**
* Queries whether interface members may be renamed as a result of a {@link MicromixinRemapper#remapClass(ClassNode)}
* pass. This method mainly exist as a way to prevent collateral damage when inappropriately implementing interfaces
* when using annotations such as @Shadow or @Overwrite alongside the
* {@link Override}.
*
*
The default implementation always returns true as an overly cautious way of weeding out potentially
* inappropriate usages of mixins.
*
* @param name The internal name of the interface whose member may be modified
* @param targets A collection of internal names of the targeted classes by the mixin (in case one wishes to perform hierarchy validation)
* @return True if renaming it's members is forbidden, false if allowed.
*/
protected boolean forbidRemappingInterfaceMembers(@NotNull String name, @NotNull Collection<@NotNull String> targets) {
return true;
}
private void handleOverwrite(@Nullable AnnotationNode annot, @NotNull Collection<@NotNull String> targets, ClassNode node, MethodNode method) throws IllegalMixinException, MissingFeatureException {
if (annot != null && annot.values != null) {
for (int i = 0; i < annot.values.size(); i += 2) {
String name = (String) annot.values.get(i);
Object value = annot.values.get(i + 1);
if (name.equals("aliases")) {
@SuppressWarnings("unchecked")
List aliases = (List) (List>) value;
for (int j = 0; j < aliases.size(); j++) {
String alias = aliases.get(j);
assert alias != null;
String remappedAlias = null;
for (String target : targets) {
if (this.lister.hasMemberInHierarchy(target, alias, method.desc)) {
String remappedName = this.lookup.getRemappedMethodName(target, alias, method.desc);
if (remappedAlias != null && !remappedAlias.equals(remappedName)) {
throw new IllegalMixinException("Disjoint mapping names while trying to remap alias for @Overwrite-annotated method: " + node.name + "." + method.name + method.desc
+ ". This is likely caused by different target classes having different names for the overwritten member. Potential ways of resolving this issue include:\n"
+ "\t1. Splitting the mixin class so that each target class has it's own mixin.\n"
+ "\t2. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change as well as what the new behaviour should be)");
}
remappedAlias = remappedName;
}
}
if (remappedAlias != null) {
aliases.set(j, remappedAlias);
}
}
} else {
this.logUnimplementedFeature("Unimplemented key in @Overwrite: " + name + " within node " + node.name);
}
}
}
String remappedMemberName = null;
for (String target : targets) {
String targetRemapped = this.lookup.getRemappedMethodName(target, method.name, method.desc);
if (remappedMemberName != null && !remappedMemberName.equals(targetRemapped)) {
throw new IllegalMixinException("Disjoint mapping names while trying to remap name of (implicitly) @Overwrite-annotated method: " + node.name + "." + method.name + method.desc
+ ". This is likely caused by different target classes having different names for the shadowed member. Potential ways of resolving this issue include:\n"
+ "\t1. Splitting the mixin class so that each target class has it's own mixin.\n"
+ "\t2. Use an @Invoker (not supported by micromixin as of April 2024)\n"
+ "\t3. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change)");
}
remappedMemberName = targetRemapped;
}
if (remappedMemberName != null && !method.name.equals(remappedMemberName)) {
for (String itf : node.interfaces) {
assert itf != null;
if (!this.forbidRemappingInterfaceMembers(itf, targets)) {
continue;
}
if (this.lister.hasMemberInHierarchy(itf, method.name, method.desc)) {
throw new IllegalMixinException("Attempt to (implicitly) @Overwrite method " + node.name + "." + method.name + method.desc + " which is provided by the interface " + itf
+ ". The interface does not allow remapping it's members (see MicromixinRemapper#forbidRemappingInterfaceMembers). Potential ways of resolving this issue include:\n"
+ "\t1. Rename the method in the interface or alter it's descriptor.\n"
+ "\t2. Do not implement the interface in the mixin.\n"
+ "\t3. Use @CanonicalOverwrite (micromixin-transformer and micromixin-backports exclusive feature).\n"
+ "\t4. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change)");
}
}
}
if (remappedMemberName != null) {
this.sink.remapMember(new MemberRef(node.name, method.name, method.desc), remappedMemberName);
}
}
/**
* The error handler that is invoked whenever an unimplemented or unknown feature is encountered.
*
*
Being able to track down these cases may help narrow down why the remapper failed remapping something
* or remapped something in an unexpected way.
*
*
By default it'll cause an {@link MissingFeatureException} to be thrown, but it is possible to overwrite this
* behaviour with a logging call. In that case, the remapper will try to continue on a best-effort basis.
*
* @param featureDescription The description of the feature that is not implemented and what caused the issue to occur.
* @throws MissingFeatureException Thrown if the error handler is configured to stop execution of the remapper in a
* fail-fast manner.
*/
@OverrideOnly
protected void logUnimplementedFeature(@NotNull String featureDescription) throws MissingFeatureException {
throw new MissingFeatureException(featureDescription);
}
@Nullable
@MustBeInvokedByOverriders
@Contract(pure = true)
protected AtSelector lookupSelector(@NotNull String atValue) {
switch (atValue) {
case "org.spongepowered.asm.mixin.injection.points.MethodHead":
case "HEAD":
return HeadSelector.INSTANCE;
case "org.spongepowered.asm.mixin.injection.points.BeforeInvoke":
case "INVOKE":
return InvokeSelector.INSTANCE;
case "org.spongepowered.asm.mixin.injection.points.BeforeReturn":
case "RETURN":
return ReturnSelector.INSTANCE;
case "org.spongepowered.asm.mixin.injection.points.BeforeFinalReturn":
case "TAIL":
return TailSelector.INSTANCE;
case "org.spongepowered.asm.mixin.injection.points.BeforeConstant":
case "CONSTANT":
return ConstantSelector.INSTANCE;
case "org.spongepowered.asm.mixin.injection.points.BeforeFieldAccess":
case "FIELD":
return FieldSelector.INSTANCE;
case "org.spongepowered.asm.mixin.injection.points.BeforeNew":
case "NEW":
return NewSelector.INSTANCE;
default:
return null;
}
}
@Internal
public void remapAt(@NotNull String owner, @NotNull String member, int ordinal, @NotNull Collection targets, AnnotationNode annot) throws IllegalMixinException, MissingFeatureException {
int idxValue = 0;
int idxArgs = 0;
int idxTarget = 0;
int idxDesc = 0;
for (int i = 0; i < annot.values.size(); i++) {
String name = (String) annot.values.get(i++);
if (name.equals("args")) {
idxArgs = i;
} else if (name.equals("value")) {
idxValue = i;
} else if (name.equals("desc")) {
idxDesc = i;
} else if (name.equals("slice")) {
// Slices don't need to be remapped to my knowledge, nor are they relevant to the remapping process.
} else if (name.equals("shift") || name.equals("by") || name.equals("opcode")) {
// Shifts and opcode can stay as-is
} else if (name.equals("target")) {
idxTarget = i;
} else {
String error = "An unexpected error occured while remapping @At annotation in " + owner + "." + member;
error += ordinal < 0 ? ("[" + ordinal + "]: ") : ": ";
this.logUnimplementedFeature(error + "Unimplemented key in @At: " + name);
}
}
if (idxValue == 0) {
String error = "An unexpected error occured while remapping @At annotation in " + owner + "." + member;
error += ordinal < 0 ? ("[" + ordinal + "]: ") : ": ";
throw new IllegalMixinException(error + "The annotation is missing the required element 'value'. This error is usually caused by improperly written ASM transformers generating the mixin improperly. Tip: Use tools such as Krakatau, javap and Recaf for troubleshooting faulty transformers!");
}
@SuppressWarnings("unchecked")
List args = idxArgs != 0 ? (List) annot.values.get(idxArgs) : null;
String value = (String) annot.values.get(idxValue);
AtSelector selector = this.lookupSelector(Objects.requireNonNull(value));
if (selector == null) {
String error = "An unexpected error occured while remapping @At annotation in " + owner + "." + member;
error += ordinal < 0 ? ("[" + ordinal + "]: ") : ": ";
this.logUnimplementedFeature(error + "Unknown @At injection point selector value: " + value);
} else {
String errorPrefix = "An unexpected error occured while remapping @At annotation in " + owner + "." + member;
errorPrefix += ordinal < 0 ? ("[" + ordinal + "]: ") : ": ";
selector.remapArgs(errorPrefix, args, this.lookup);
}
if (idxTarget != 0) {
String errorPrefix = "An unexpected error occured while remapping @At.target in " + owner + "." + member;
errorPrefix += ordinal < 0 ? ("[" + ordinal + "]: ") : ": ";
annot.values.set(idxTarget, this.remapTargetSelector(errorPrefix, (String) annot.values.get(idxTarget), null, null));
}
if (idxDesc != 0) {
String errorPrefix = "An unexpected error occured while remapping @At.desc in " + owner + "." + member;
errorPrefix += ordinal < 0 ? ("[" + ordinal + "]: ") : ": ";
boolean matchFields = selector != null && selector.isMatchingFields();
this.remapDescAnnotation(errorPrefix, targets, (AnnotationNode) annot.values.get(idxDesc), matchFields);
}
}
@Internal
public void remapAtArray(@NotNull String owner, @NotNull String method, @NotNull Collection targets, Object nodes) throws IllegalMixinException, MissingFeatureException {
int ordinal = 0;
for (Object node : (Iterable>) nodes) {
this.remapAt(owner, method, ordinal++, targets, (AnnotationNode) node);
}
}
/**
* Remap a {@link ClassNode} and all the member {@link MethodNode MethodNodes}
* and {@link FieldNode FieldNodes} within the class. Note that in order for
* the micromixin remapping process to work correctly and account for inter-class
* relationships (mixin inheritance), this method needs to be executed before the
* actual remapping process through {@link Remapper#remapNode(ClassNode, StringBuilder)}.
* More concisely, this method expect that {@link Remapper#remapNode(ClassNode, StringBuilder)}
* will be run after {@link MicromixinRemapper#remapClass(ClassNode)}.
*
*
Modifications to this remapper's underlying {@link MappingSink} instance (set through
* the constructor) are expected to be applied to the {@link Remapper} instance.
* It is highly recommended that the {@link MappingSink} is aware of class hierarchy remapping.
* Additionally, it may be useful to be able to verify that the {@link MappingSink} instance
* does not accidentally or not remap non-mixin classes. If a mapping request emitted
* by this method (or any of the method is is delegating to) ends up remapping an unrelated class,
* then the mixin should be considered illegal.
*
*
It is permissible for the provided mixin class to not be an @Mixin-annotated
* class, in which case the class is skipped.
*
* @param node The {@link ClassNode} to remap
* @throws IllegalMixinException Thrown if the mixin contains illegal code (e.g. invalid targets in
* the @Mixin or @Shadow collisions while implementing an interface).
* @throws MissingFeatureException Thrown due to {@link #logUnimplementedFeature(String)}, this exception is
* reserved for purposes where the remapper encounters mixin features it is not aware of at the current
* point in time.
*/
public void remapClass(@NotNull ClassNode node) throws IllegalMixinException, MissingFeatureException {
Set<@NotNull String> targets = new LinkedHashSet<>();
boolean mixinClass = false;
if (node.invisibleAnnotations == null) {
return;
}
for (AnnotationNode annot : node.invisibleAnnotations) {
if (!annot.desc.startsWith("Lorg/spongepowered/asm/mixin/")) {
continue;
}
if (annot.desc.equals("Lorg/spongepowered/asm/mixin/Mixin;")) {
mixinClass = true;
for (int i = 0; i < annot.values.size(); i += 2) {
String name = (String) annot.values.get(i);
Object value = annot.values.get(i + 1);
if (name.equals("value")) {
@SuppressWarnings("unchecked")
List aev = (List) value;
int j = aev.size();
while (j-- != 0) {
Type target = aev.get(j);
String targetDesc = target.getDescriptor();
assert targetDesc != null;
if (targetDesc.codePointAt(0) != 'L') {
throw new IllegalMixinException("Mixin class " + node.name + " targets type " + targetDesc + ", which is not an L-type reference (arrays and primitives cannot be transformed and are illegal targets for mixins!)");
}
String originTarget = targetDesc.substring(1, targetDesc.length() - 1);
String remappedTarget = this.lookup.getRemappedClassNameFast(originTarget);
targets.add(originTarget);
if (remappedTarget != null) {
aev.set(j, Type.getType('L' + remappedTarget + ';'));
}
}
} else if (name.equals("targets")) {
@SuppressWarnings("unchecked")
List aev = (List) value;
int j = aev.size();
while (j-- != 0) {
String target = aev.get(j);
assert target != null;
targets.add(target.replace('.', '/'));
aev.set(j, this.lookup.getRemappedClassName(target));
}
} else if (!name.equals("priority")) {
this.logUnimplementedFeature("Unimplemented key in @Mixin: " + name + " within node " + node.name);
}
}
} else {
this.logUnimplementedFeature("Unknown annotation at class level for node " + node.name + ": " + annot.desc);
}
}
if (!mixinClass) {
return;
}
for (MethodNode method : node.methods) {
this.remapMethod(node, method, targets);
}
for (FieldNode field : node.fields) {
this.remapField(node, field, targets);
}
}
@NotNull
private void remapDescAnnotation(@NotNull String errorPrefix, @NotNull Collection targets, AnnotationNode descAnnot, boolean matchField) throws MissingFeatureException, IllegalMixinException {
if (!descAnnot.desc.equals("Lorg/spongepowered/asm/mixin/injection/Desc;")) {
throw new IllegalMixinException(errorPrefix + "Invalid annotation descriptor: " + descAnnot.desc);
}
int idxValue = 0;
int idxArgs = 0;
int idxRet = 0;
int idxOwner = 0;
for (int i = 0; i < descAnnot.values.size(); i++) {
String name = (String) descAnnot.values.get(i++);
if (name.equals("args")) {
idxArgs = i;
} else if (name.equals("value")) {
idxValue = i;
} else if (name.equals("ret")) {
idxRet = i;
} else if (name.equals("owner")) {
idxOwner = i;
} else {
this.logUnimplementedFeature(errorPrefix + "Unimplemented key in @Desc: " + name);
}
}
if (idxValue == 0) {
throw new IllegalMixinException(errorPrefix + "The @Desc annotation is missing the required element 'value'. This error is usually caused by improperly written ASM transformers generating the mixin improperly. Tip: Use tools such as Krakatau, javap and Recaf for troubleshooting faulty transformers!");
}
Collection owners = targets;
if (idxOwner != 0) {
owners = Collections.singleton(((Type) descAnnot.values.get(idxOwner)).getInternalName());
}
String name = (String) descAnnot.values.get(idxValue);
assert name != null;
String desc;
if (!matchField) {
if (idxArgs == 0) {
desc = "()";
} else {
desc = "(";
for (Object arg : (Iterable>) descAnnot.values.get(idxArgs)) {
desc += ((Type) Objects.requireNonNull(arg)).getDescriptor();
}
desc += ")";
}
if (idxRet == 0) {
desc += "V";
} else {
desc += ((Type) descAnnot.values.get(idxRet)).getDescriptor();
}
} else {
if (idxRet == 0) {
desc = "V";
this.logUnimplementedFeature(errorPrefix + "The @Desc annotation is expected to match a field, but has not explicitly set the field descriptor using ret.");
} else {
desc = ((Type) descAnnot.values.get(idxRet)).getDescriptor();
}
}
if (owners.size() == 1) {
String owner = owners.iterator().next();
assert owner != null;
StringBuilder builder = new StringBuilder();
Remapper.remapSignature(this.lookup, desc, builder);
if (matchField) {
descAnnot.values.set(idxValue, this.lookup.getRemappedFieldName(owner, name, desc));
if (idxRet != 0) {
descAnnot.values.set(idxRet, Type.getType(builder.toString()));
}
} else {
descAnnot.values.set(idxValue, this.lookup.getRemappedMethodName(owner, name, desc));
desc = builder.toString();
if (idxRet != 0) {
descAnnot.values.set(idxRet, Type.getType(desc.substring(desc.lastIndexOf(')') + 1)));
}
if (idxArgs != 0) {
descAnnot.values.set(idxArgs, new ArrayList<>(Arrays.asList(Type.getMethodType(desc).getArgumentTypes())));
}
}
if (idxOwner != 0) {
descAnnot.values.set(idxOwner, Type.getObjectType(this.lookup.getRemappedClassName(owner)));
}
} else {
String mappedName = null;
for (String owner : owners) {
assert owner != null;
String newName;
if (matchField) {
newName = this.lookup.getRemappedFieldName(owner, name, desc);
} else {
newName = this.lookup.getRemappedMethodName(owner, name, desc);
}
if (mappedName == null) {
mappedName = newName;
} else if (!mappedName.equals(newName)) {
throw new IllegalMixinException(errorPrefix + "Torn @Desc: Multiple potential owners define multiple potential names. Following steps can be taken to mitigate this issue:\n"
+ "\t1.: Only define a single @Mixin.target/@Mixin.value per Mixin class.\n"
+ "\t2.: Explicitly define @Desc.owner for this @Desc annotation (and if necessary seperate a single @Desc into multiple @Desc annotations).\n"
+ "\t3.: Validate the name hierarchy used to remap the @Desc; ensuring that no two classes define different names for the same method.\n"
+ "\t4.: Ask for guidance in the relevant support channels (though I am afraid we wouldn't be able to help you much - there is only a limited pool of options that are available in this scenario)");
}
}
if (mappedName == null) {
throw new IllegalMixinException(errorPrefix + "No owners exist that would influence this @Desc (did you forget specifying a target in the @Mixin annotation?).");
}
StringBuilder builder = new StringBuilder();
Remapper.remapSignature(this.lookup, desc, builder);
descAnnot.values.set(idxValue, mappedName);
if (matchField) {
if (idxRet != 0) {
descAnnot.values.set(idxRet, Type.getType(builder.toString()));
}
} else {
desc = builder.toString();
if (idxRet != 0) {
descAnnot.values.set(idxRet, Type.getType(desc.substring(desc.lastIndexOf(')') + 1)));
}
if (idxArgs != 0) {
descAnnot.values.set(idxArgs, new ArrayList<>(Arrays.asList(Type.getMethodType(desc).getArgumentTypes())));
}
}
}
}
private void remapField(@NotNull ClassNode node, FieldNode field, @NotNull Collection<@NotNull String> targets) throws MissingFeatureException, IllegalMixinException {
String mainAnnotation = null;
if (field.visibleAnnotations != null) {
for (AnnotationNode annot : field.visibleAnnotations) {
if (!annot.desc.startsWith("Lorg/spongepowered/asm/mixin/")
&& !annot.desc.startsWith("Lcom/llamalad7/mixinextras/injector/")) {
continue;
}
if (annot.desc.equals("Lorg/spongepowered/asm/mixin/Shadow;")) {
if (mainAnnotation != null) {
throw new IllegalMixinException("Illegal mixin field " + node.name + "." + field.name + ":" + field.desc + ": The mixin field is annotated with two or more incompatible annotations: " + mainAnnotation + " and " + annot.desc);
}
mainAnnotation = annot.desc;
String remapPrefix = "shadow$";
for (int i = 0; annot.values != null && i < annot.values.size(); i += 2) {
String name = (String) annot.values.get(i);
Object value = annot.values.get(i + 1);
if (name.equals("prefix")) {
remapPrefix = (String) value;
} else if (name.equals("aliases")) {
@SuppressWarnings("unchecked")
List aliases = (List) (List>) value;
for (int j = 0; j < aliases.size(); j++) {
String alias = aliases.get(j);
assert alias != null;
String remappedAlias = null;
for (String target : targets) {
if (this.lister.hasMemberInHierarchy(target, alias, field.desc)) {
String remappedName = this.lookup.getRemappedFieldName(target, alias, field.desc);
if (remappedAlias != null && !remappedAlias.equals(remappedName)) {
throw new IllegalMixinException("Disjoint mapping names while trying to remap alias for @Shadow-annotated field: " + node.name + "." + field.name + ":" + field.desc
+ ". This is likely caused by different target classes having different names for the shadowed member. Potential ways of resolving this issue include:\n"
+ "\t1. Splitting the mixin class so that each target class has it's own mixin.\n"
+ "\t2. Use an @Accessor (not supported by micromixin as of April 2024)\n"
+ "\t3. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change)");
}
remappedAlias = remappedName;
}
}
if (remappedAlias != null) {
aliases.set(j, remappedAlias);
}
}
} else {
this.logUnimplementedFeature("Unimplemented key in @Shadow: " + name + " within node " + node.name);
}
}
String shadowName = field.name;
boolean prefixed = field.name.startsWith(remapPrefix);
if (prefixed) {
shadowName = field.name.substring(remapPrefix.length());
}
String remappedShadowName = null;
for (String target : targets) {
String targetRemapped = this.lookup.getRemappedFieldName(target, shadowName, field.desc);
if (remappedShadowName != null && !remappedShadowName.equals(targetRemapped)) {
throw new IllegalMixinException("Disjoint mapping names while trying to remap name of @Shadow-annotated field: " + node.name + "." + field.name + ":" + field.desc
+ ". This is likely caused by different target classes having different names for the shadowed member. Potential ways of resolving this issue include:\n"
+ "\t1. Splitting the mixin class so that each target class has it's own mixin.\n"
+ "\t2. Use an @Accessor (not supported by micromixin as of April 2024)\n"
+ "\t3. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change)");
}
remappedShadowName = targetRemapped;
}
if (remappedShadowName != null) {
if (prefixed) {
remappedShadowName = remapPrefix + remappedShadowName;
}
this.sink.remapMember(new MemberRef(node.name, field.name, field.desc), remappedShadowName);
}
} else if (annot.desc.equals("Lorg/spongepowered/asm/mixin/Unique;")) {
if (mainAnnotation != null) {
throw new IllegalMixinException("Illegal mixin field " + node.name + "." + field.name + ":" + field.desc + ": The mixin field is annotated with two or more incompatible annotations: " + mainAnnotation + " and " + annot.desc);
}
mainAnnotation = annot.desc;
for (int i = 0; annot.values != null && i < annot.values.size(); i += 2) {
String name = (String) annot.values.get(i);
if (!name.equals("silent")) {
this.logUnimplementedFeature("Unimplemented key in @Unique: " + name + " within node " + node.name);
}
}
// @Unique requires no further changes
}
}
}
// TODO implement implicit field overlay/shadow/overwrite
}
private void remapMethod(@NotNull ClassNode node, MethodNode method, @NotNull Collection<@NotNull String> targets) throws MissingFeatureException, IllegalMixinException {
String mainAnnotation = null;
if (method.visibleAnnotations != null) {
for (AnnotationNode annot : method.visibleAnnotations) {
if (!annot.desc.startsWith("Lorg/spongepowered/asm/mixin/")
&& !annot.desc.startsWith("Lcom/llamalad7/mixinextras/injector/")
&& !annot.desc.startsWith("Lorg/stianloader/micromixin/annotations/")) {
continue;
}
if (annot.desc.equals("Lorg/spongepowered/asm/mixin/Shadow;")) {
if (mainAnnotation != null) {
throw new IllegalMixinException("Illegal mixin method " + node.name + "." + method.name + method.desc + ": The mixin handler is annotated with two or more incompatible annotations: " + mainAnnotation + " and " + annot.desc);
}
mainAnnotation = annot.desc;
String remapPrefix = (method.access & Opcodes.ACC_STATIC) == 0 ? "shadow$" : null;
for (int i = 0; annot.values != null && i < annot.values.size(); i += 2) {
String name = (String) annot.values.get(i);
Object value = annot.values.get(i + 1);
if (name.equals("prefix")) {
if ((method.access & Opcodes.ACC_STATIC) != 0) {
this.logUnimplementedFeature("The static @Shadow-annotated mixin method " + node.name + "." + method.name + method.desc + " defines a prefix. However, due to a bug in the spongeian mixin implementation INVOKESTATIC calls will not be redirected to the non-prefixed member you are targetting, effectively causing a crash at runtime. At this point in time micromixin-transformer replicates this issue, but this behaviour is subject to change.");
}
remapPrefix = (String) value;
} else if (name.equals("aliases")) {
@SuppressWarnings("unchecked")
List aliases = (List) (List>) value;
for (int j = 0; j < aliases.size(); j++) {
String alias = aliases.get(j);
assert alias != null;
String remappedAlias = null;
for (String target : targets) {
if (this.lister.hasMemberInHierarchy(target, alias, method.desc)) {
String remappedName = this.lookup.getRemappedMethodName(target, alias, method.desc);
if (remappedAlias != null && !remappedAlias.equals(remappedName)) {
throw new IllegalMixinException("Disjoint mapping names while trying to remap alias for @Shadow-annotated method: " + node.name + "." + method.name + method.desc
+ ". This is likely caused by different target classes having different names for the shadowed member. Potential ways of resolving this issue include:\n"
+ "\t1. Splitting the mixin class so that each target class has it's own mixin.\n"
+ "\t2. Use an @Invoker (not supported by micromixin as of April 2024)\n"
+ "\t3. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change)");
}
remappedAlias = remappedName;
}
}
if (remappedAlias != null) {
aliases.set(j, remappedAlias);
}
}
} else {
this.logUnimplementedFeature("Unimplemented key in @Shadow: " + name + " within node " + node.name);
}
}
String shadowName = method.name;
if (remapPrefix != null && method.name.startsWith(remapPrefix)) {
shadowName = method.name.substring(remapPrefix.length());
}
String remappedShadowName = null;
for (String target : targets) {
String targetRemapped = this.lookup.getRemappedMethodName(target, shadowName, method.desc);
if (remappedShadowName != null && !remappedShadowName.equals(targetRemapped)) {
throw new IllegalMixinException("Disjoint mapping names while trying to remap name of @Shadow-annotated method: " + node.name + "." + method.name + method.desc
+ ". This is likely caused by different target classes having different names for the shadowed member. Potential ways of resolving this issue include:\n"
+ "\t1. Splitting the mixin class so that each target class has it's own mixin.\n"
+ "\t2. Use an @Invoker (not supported by micromixin as of April 2024)\n"
+ "\t3. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change)");
}
remappedShadowName = targetRemapped;
}
if (remappedShadowName != null && !shadowName.equals(remappedShadowName) && method.name.equals(shadowName)) {
for (String itf : node.interfaces) {
assert itf != null;
if (!this.forbidRemappingInterfaceMembers(itf, targets)) {
continue;
}
if (this.lister.hasMemberInHierarchy(itf, method.name, method.desc)) {
throw new IllegalMixinException("Attempt to @Shadow method " + node.name + "." + method.name + method.desc + " which is provided by the interface " + itf
+ ". The interface does not allow remapping it's members (see MicromixinRemapper#forbidRemappingInterfaceMembers). Potential ways of resolving this issue include:\n"
+ "\t1. Rename the method in the interface or alter it's descriptor.\n"
+ "\t2. Do not implement the interface in the mixin.\n"
+ "\t3. Use an @Invoker (not supported by micromixin as of April 2024)\n"
+ "\t4. Use @Intrinsic (not supported by micromixin as of April 2024)\n"
+ "\t5. Use @Unique with silent = true\n"
+ "\t6. Report this behaviour as unintended to the micromixin-remapper developers (please also include the mixin itself and a short statement on why the behaviour should change)");
}
}
}
if (remappedShadowName != null) {
String remappedName = (method.access & Opcodes.ACC_STATIC) == 0 ? remapPrefix + remappedShadowName : remappedShadowName;
this.sink.remapMember(new MemberRef(node.name, method.name, method.desc), remappedName);
}
} else if (annot.desc.equals("Lorg/spongepowered/asm/mixin/Unique;")) {
if (mainAnnotation != null) {
throw new IllegalMixinException("Illegal mixin method " + node.name + "." + method.name + method.desc + ": The mixin handler is annotated with two or more incompatible annotations: " + mainAnnotation + " and " + annot.desc);
}
mainAnnotation = annot.desc;
for (int i = 0; annot.values != null && i < annot.values.size(); i += 2) {
String name = (String) annot.values.get(i);
if (!name.equals("silent")) {
this.logUnimplementedFeature("Unimplemented key in @Unique: " + name + " within node " + node.name);
}
}
// @Unique requires no further changes
} else if (annot.desc.equals("Lorg/spongepowered/asm/mixin/Overwrite;")) {
if (mainAnnotation != null) {
throw new IllegalMixinException("Illegal mixin method " + node.name + "." + method.name + method.desc + ": The mixin handler is annotated with two or more incompatible annotations: " + mainAnnotation + " and " + annot.desc);
}
mainAnnotation = annot.desc;
this.handleOverwrite(annot, targets, node, method);
} else {
AnnotationRemapper remapper = AnnotationRemapper.ANNOTATION_REMAPPERS.get(annot.desc);
if (remapper != null) {
if (mainAnnotation != null) {
throw new IllegalMixinException("Illegal mixin method " + node.name + "." + method.name + method.desc + ": The mixin handler is annotated with two or more incompatible annotations: " + mainAnnotation + " and " + annot.desc);
}
mainAnnotation = annot.desc;
remapper.remapAnnotation(new RemapContext(this, node.name, method, targets), annot);
} else {
this.logUnimplementedFeature("Unknown mixin annotation on method " + node.name + "." + method.name + method.desc + ": " + annot.desc);
}
}
}
}
if (mainAnnotation == null) {
this.handleOverwrite(null, targets, node, method);
}
}
@Internal
public void remapMethodSelectorList(List> selectors, @NotNull String originName, MethodNode originMethod, @NotNull Collection targets, @Nullable Predicate<@NotNull String> inferredDescriptorPredicate) throws IllegalMixinException, MissingFeatureException {
@SuppressWarnings("unchecked")
ListIterator