Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@

package net.fabricmc.tinyremapper.extension.mixin.soft.annotation.injection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.objectweb.asm.AnnotationVisitor;
Expand Down Expand Up @@ -85,11 +92,23 @@ public AnnotationVisitor visitArray(String name) {
return new AnnotationVisitor(Constant.ASM_VERSION, av) {
@Override
public void visit(String name, Object value) {
Optional<MemberInfo> info = Optional.ofNullable(MemberInfo.parse(Objects.requireNonNull((String) value).replaceAll("\\s", "")));
String string = Objects.requireNonNull((String) value);

value = info.map(i -> new InjectMethodMappable(data, i, targets).result().toString()).orElse((String) value);
MemberInfo info = MemberInfo.parse(string.replaceAll("\\s", ""));

super.visit(name, value);
if (info == null) {
super.visit(name, value);
return;
}

MemberInfo[] resolved = new InjectMethodMappable(data, info, targets).result();
if (resolved.length == 0) {
throw new RuntimeException("InjectMethodMappable should never resolve to zero entries");
}

for (MemberInfo memberInfos : resolved) {
super.visit(name, memberInfos.toString());
Comment thread
Moulberry marked this conversation as resolved.
Outdated
}
}
};
} else if (name.equals(AnnotationElement.TARGET)) { // All
Expand Down Expand Up @@ -133,7 +152,7 @@ public AnnotationVisitor visitAnnotation(String name, String descriptor) {
return av;
}

private static class InjectMethodMappable implements IMappable<MemberInfo> {
private static class InjectMethodMappable implements IMappable<MemberInfo[]> {
Comment thread
Moulberry marked this conversation as resolved.
Outdated
private final CommonData data;
private final MemberInfo info;
private final List<TrClass> targets;
Expand Down Expand Up @@ -161,52 +180,144 @@ private Optional<TrMember> resolvePartial(TrClass owner, String name, String des
name = name.isEmpty() ? null : name;
desc = desc.isEmpty() ? null : desc;

return data.resolver.resolveMethod(owner, name, desc, ResolveUtility.FLAG_FIRST | ResolveUtility.FLAG_NON_SYN).map(m -> m);
return data.resolver.resolveMethod(owner, name, desc, ResolveUtility.FLAG_FIRST).map(m -> m);
}

@Override
public MemberInfo result() {
private Collection<TrMethod> resolvePartials(TrClass owner, String name, String desc) {
Objects.requireNonNull(owner);

name = name.isEmpty() ? null : name;
desc = desc.isEmpty() ? null : desc;

return owner.resolveMethods(name, desc, false, null, null);
}

private MemberInfo[] wildcardResult() {
// Special case to remap the desc of wildcards without a name, such as `*()Lcom/example/ClassName;`
if (info.getOwner().isEmpty()
&& info.getName().isEmpty()
&& info.getQuantifier().equals("*")
&& !info.getDesc().isEmpty()) {
return new MemberInfo(info.getOwner(), info.getName(), info.getQuantifier(), data.mapper.asTrRemapper().mapDesc(info.getDesc()));
if (info.getName().isEmpty() && !info.getDesc().isEmpty()) {
Comment thread
Moulberry marked this conversation as resolved.
Outdated
return new MemberInfo[] {
new MemberInfo(data.mapper.asTrRemapper().map(info.getOwner()), info.getName(), "*", data.mapper.asTrRemapper().mapDesc(info.getDesc()))
};
}

if (targets.isEmpty() || info.getName().isEmpty()) {
return info;
return new MemberInfo[] { info };
}

List<Pair<String, String>> collection = targets.stream()
.map(target -> resolvePartial(target, info.getName(), info.getDesc()))
.filter(Optional::isPresent)
.map(Optional::get)
.map(m -> {
String mappedName = data.mapper.mapName(m);
boolean shouldPassDesc = false;

for (TrMethod other : m.getOwner().getMethods()) { // look for ambiguous targets
if (other == m) continue;

if (data.mapper.mapName(other).equals(mappedName)) {
shouldPassDesc = true;
.flatMap(target -> resolvePartials(target, info.getName(), info.getDesc()).stream())
.map(m -> Pair.of(data.mapper.mapName(m), data.mapper.mapDesc(m)))
.distinct()
.collect(Collectors.toList());

if (collection.isEmpty()) {
data.getLogger().warn(Message.NO_MAPPING_NON_RECURSIVE, info.getName(), targets);
return new MemberInfo[] { info };
}

Map<String, Set<String>> descriptorsForName = new TreeMap<>();
for (Pair<String, String> pair : collection) {
descriptorsForName.computeIfAbsent(pair.first(), k -> new TreeSet<>()).add(pair.second());
}
Comment thread
Moulberry marked this conversation as resolved.
Outdated

List<MemberInfo> finalMembers = new ArrayList<>();

if (info.getDesc().isEmpty()) {
// If the descriptor was omitted in the input, we want to omit the descriptor in the output as well
// However, we can only do this if all the methods in the source namespace
// are exactly matched in the target namespace

for (Map.Entry<String, Set<String>> entry : descriptorsForName.entrySet()) {
String mappedName = entry.getKey();
Set<String> mappedDescriptors = entry.getValue();

Set<String> allDescriptorsInTargets = new HashSet<>();
Comment thread
Moulberry marked this conversation as resolved.
Outdated

for (TrClass target : targets) {
for (TrMethod method : target.getMethods()) {
String otherName = data.mapper.mapName(method);
if (otherName.equals(mappedName)) {
allDescriptorsInTargets.add(data.mapper.mapDesc(method));
}
}
}

return Pair.of(mappedName, shouldPassDesc ? data.mapper.mapDesc(m) : "");
})
.distinct().collect(Collectors.toList());
if (allDescriptorsInTargets.equals(mappedDescriptors)) {
finalMembers.add(new MemberInfo(data.mapper.asTrRemapper().map(info.getOwner()), mappedName, "*", ""));
} else {
for (String mappedDesc : mappedDescriptors) {
finalMembers.add(new MemberInfo(data.mapper.asTrRemapper().map(info.getOwner()), mappedName, "*", mappedDesc));
Comment thread
Moulberry marked this conversation as resolved.
Outdated
}
}
}
} else {
for (Map.Entry<String, Set<String>> entry : descriptorsForName.entrySet()) {
String mappedName = entry.getKey();
Set<String> mappedDescriptors = entry.getValue();

for (String mappedDesc : mappedDescriptors) {
finalMembers.add(new MemberInfo(data.mapper.asTrRemapper().map(info.getOwner()), mappedName, "*", mappedDesc));
Comment thread
Moulberry marked this conversation as resolved.
Outdated
}
}
}

return finalMembers.toArray(new MemberInfo[0]);
}

private MemberInfo singleResult() {
if (targets.isEmpty() || info.getName().isEmpty()) {
return info;
Comment thread
Moulberry marked this conversation as resolved.
Outdated
}

List<Pair<String, String>> collection = targets.stream()
.map(target -> resolvePartial(target, info.getName(), info.getDesc()))
.filter(Optional::isPresent)
.map(Optional::get)
.map(m -> Pair.of(data.mapper.mapName(m), data.mapper.mapDesc(m)))
.distinct()
.collect(Collectors.toList());

if (collection.size() > 1) {
data.getLogger().error(Message.CONFLICT_MAPPING, info.getName(), collection);
} else if (collection.isEmpty()) {
data.getLogger().warn(Message.NO_MAPPING_NON_RECURSIVE, info.getName(), targets);
return info;
}

Pair<String, String> pair = collection.get(0);
String mappedName = pair.first();

boolean useDescriptor = !info.getDesc().isEmpty() || isNameAmbiguous(mappedName, pair.second());
String desc = useDescriptor ? pair.second() : "";

return new MemberInfo(data.mapper.asTrRemapper().map(info.getOwner()), mappedName, info.getQuantifier(), desc);
}

private boolean isNameAmbiguous(String mappedName, String mappedDesc) {
// Try to find a method with the same name, but a different descriptor

for (TrClass target : targets) {
for (TrMethod method : target.getMethods()) {
String otherName = data.mapper.mapName(method);
if (otherName.equals(mappedName)) { // Same name
String otherDesc = data.mapper.mapDesc(method);
if (!otherDesc.equals(mappedDesc)) { // Different descriptor
return true;
}
}
}
}

return collection.stream().findFirst()
.map(pair -> new MemberInfo(data.mapper.asTrRemapper().map(info.getOwner()), pair.first(), info.getQuantifier(), info.getQuantifier().equals("*") ? "" : pair.second()))
.orElse(info);
return false;
}

@Override
public MemberInfo[] result() {
if (info.getQuantifier().equals("*")) {
return this.wildcardResult();
} else {
return new MemberInfo[] { singleResult() };
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
import net.fabricmc.tinyremapper.extension.mixin.integration.mixins.DescAtMixin;
import net.fabricmc.tinyremapper.extension.mixin.integration.mixins.LvtRemapTargetMixin;
import net.fabricmc.tinyremapper.extension.mixin.integration.mixins.NonObfuscatedOverrideMixin;
import net.fabricmc.tinyremapper.extension.mixin.integration.mixins.SeparateRemappedNameMixin;
import net.fabricmc.tinyremapper.extension.mixin.integration.mixins.WildcardTargetMixin;
import net.fabricmc.tinyremapper.extension.mixin.integration.targets.AmbiguousRemappedNameTarget;
import net.fabricmc.tinyremapper.extension.mixin.integration.targets.DescAtTarget;
import net.fabricmc.tinyremapper.extension.mixin.integration.targets.LvtRemapTarget;
import net.fabricmc.tinyremapper.extension.mixin.integration.targets.NonObfuscatedOverrideTarget;
import net.fabricmc.tinyremapper.extension.mixin.integration.targets.SeparateRemappedNameTarget;
import net.fabricmc.tinyremapper.extension.mixin.integration.targets.WildcardTarget;

public class MixinIntegrationTest {
Expand All @@ -58,13 +60,23 @@ public class MixinIntegrationTest {

@Test
public void remapWildcardName() throws IOException {
String remapped = remap(WildcardTarget.class, WildcardTargetMixin.class, out ->
out.acceptClass("java/lang/String", "com/example/NotString"));
String remapped = remap(WildcardTarget.class, WildcardTargetMixin.class, out -> {
String fqn = "net/fabricmc/tinyremapper/extension/mixin/integration/targets/WildcardTarget";
out.acceptClass("java/lang/String", "com/example/NotString");
out.acceptMethod(new IMappingProvider.Member(fqn, "targetA", "(Ljava/lang/Object;)V"), "sameName");
out.acceptMethod(new IMappingProvider.Member(fqn, "targetA", "()Ljava/lang/String;"), "sameName");
out.acceptMethod(new IMappingProvider.Member(fqn, "targetB", "()Ljava/lang/Object;"), "sameName");
});

// Check constructor inject did not gain a desc
// <init>* -> <init>*
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"<init>*\"}"));
// Check that wildcard desc is remapped without a name
// *()Ljava/lang/String; -> *()Lcom/example/NotString;
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"*()Lcom/example/NotString;\"}"));
// Check that wildcards are expanded with descriptor to avoid incorrect targets (targetB)
// targetA* -> {"sameName*()Lcom/example/NotString;", "sameName*(Ljava/lang/Object;)V"}
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"sameName*()Lcom/example/NotString;\", \"sameName*(Ljava/lang/Object;)V\"}"));
}

@Test
Expand All @@ -83,7 +95,7 @@ public void remapInvokeNonObfuscatedOverride() throws IOException {
}

@Test
public void remapAmbiuousRemappedName() throws IOException {
public void remapAmbiguousRemappedName() throws IOException {
String remapped = remap(AmbiguousRemappedNameTarget.class, AmbiguousRemappedNameMixin.class, out -> {
String fqn = "net/fabricmc/tinyremapper/extension/mixin/integration/targets/AmbiguousRemappedNameTarget";
out.acceptClass(fqn, "com/example/Remapped");
Expand All @@ -92,7 +104,30 @@ public void remapAmbiuousRemappedName() throws IOException {
});

// full signature is used to disambiguate names
// addString -> add(Ljava/lang/String;)V
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"add(Ljava/lang/String;)V\""));
// ensure full signature is used for wildcard as well
// addString* -> add*(Ljava/lang/String;)V
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"add*(Ljava/lang/String;)V\"}"));
}

@Test
public void remapSeparateRemappedName() throws IOException {
String remapped = remap(SeparateRemappedNameTarget.class, SeparateRemappedNameMixin.class, out -> {
String fqn = "net/fabricmc/tinyremapper/extension/mixin/integration/targets/SeparateRemappedNameTarget";
out.acceptMethod(new IMappingProvider.Member(fqn, "addString", "(Ljava/lang/String;)V"), "add1");
out.acceptMethod(new IMappingProvider.Member(fqn, "addString", "(Ljava/lang/String;I)V"), "add2");
});

// Ensure that descriptor isn't added and first method is targeted
// addString -> add1
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"add1\"}"));
// Ensure that descriptor is kept and second method is targeted
// addString(Ljava/lang/String;I)V -> add2(Ljava/lang/String;I)V
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"add2(Ljava/lang/String;I)V\"}"));
// Ensure that both methods are targeted by wildcard
// addString* -> {"add1*", "add2*"}
assertTrue(remapped.contains("@Lorg/spongepowered/asm/mixin/injection/Inject;(method={\"add1*\", \"add2*\"}"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ public class AmbiguousRemappedNameMixin {
@Inject(method = "addString", at = @At("HEAD"))
private void injectAddString(String string, CallbackInfo ci) {
}

@Inject(method = "addString*", at = @At("HEAD"))
private void injectAddStringWildcard(String string, CallbackInfo ci) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2016, 2018, Player, asie
* Copyright (c) 2026, FabricMC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package net.fabricmc.tinyremapper.extension.mixin.integration.mixins;

import net.fabricmc.tinyremapper.extension.mixin.integration.targets.SeparateRemappedNameTarget;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(SeparateRemappedNameTarget.class)
public class SeparateRemappedNameMixin {
@Inject(method = "addString", at = @At("HEAD"))
private void injectAddStringFirst(String string, CallbackInfo ci) {
}

@Inject(method = "addString(Ljava/lang/String;I)V", at = @At("HEAD"))
private void injectAddStringSecond(String string, int value, CallbackInfo ci) {
}

@Inject(method = "addString*", at = @At("HEAD"))
private void injectAddStringBoth(CallbackInfo ci) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ private void constructorHook(final CallbackInfo ci) {
@Inject(method = "*()Ljava/lang/String;", at = @At("HEAD"), cancellable = true)
private void injectName(CallbackInfoReturnable<String> ci) {
}

@Inject(method = "targetA*", at = @At("HEAD"))
private void injectTargetA(CallbackInfo ci) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2016, 2018, Player, asie
* Copyright (c) 2026, FabricMC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package net.fabricmc.tinyremapper.extension.mixin.integration.targets;

public class SeparateRemappedNameTarget {
public void addString(String string) {
}
public void addString(String string, int value) {
}
}
Loading