diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 20b1a2d2..b6e62b59 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { implementation("com.google.guava:guava:32.1.2-jre") implementation("org.slf4j:slf4j-api:2.0.0") implementation("net.fabricmc:sponge-mixin:0.14.0+mixin.0.8.6") - implementation("io.github.llamalad7:mixinextras-common:0.3.1") compileOnly("org.jetbrains:annotations:24.0.1") api(platform("org.ow2.asm:asm-bom:9.8")) diff --git a/core/src/main/java/org/sinytra/adapter/analysis/MethodLabelComparator.java b/core/src/main/java/org/sinytra/adapter/analysis/MethodLabelComparator.java deleted file mode 100644 index 946f21ec..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/MethodLabelComparator.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.sinytra.adapter.analysis; - -import com.mojang.datafixers.util.Pair; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.FrameNode; -import org.objectweb.asm.tree.LabelNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.env.ctx.Targets; - -import java.util.*; -import java.util.stream.Stream; - -public class MethodLabelComparator { - public record ComparisonResult(List> patchedLabels, List cleanLabel) { - } - - @Nullable - public static ComparisonResult findPatchedLabels(AbstractInsnNode cleanInjectionInsn, Recipe recipe) { - Targets targets = recipe.targets(); - if (targets == null) return null; - - List> cleanLabels = getLabelsInMethod(targets.clean().methodNode()); - List> cleanLabelsOriginal = List.copyOf(cleanLabels); - - List> cleanMatchedLabels = cleanLabels.stream() - .filter(insns -> insns.contains(cleanInjectionInsn)) - .toList(); - if (cleanMatchedLabels.size() != 1) { - return null; - } - List cleanLabel = cleanMatchedLabels.getFirst(); - - List> dirtyLabels = getLabelsInMethod(targets.dirty().methodNode()); - List> dirtyLabelsOriginal = List.copyOf(dirtyLabels); - - Map, List> matchedLabels = new LinkedHashMap<>(); - for (List cleanInsns : cleanLabelsOriginal) { - List> candidates = new ArrayList<>(); - - for (List dirtyInsns : dirtyLabels) { - if (InstructionMatcher.test(cleanInsns, dirtyInsns, InsnComparator.IGNORE_VAR_INDEX | InsnComparator.IGNORE_LINE_NUMBERS)) { - candidates.add(dirtyInsns); - } - } - // TODO Try and come up with something better - // This prevents messing up the order of labels - // Without this countermeasure, it might happen that a label that was deleted will match a seemingly identical label somewhere else in the method, which is wrong - // We disable any duplicated until we can properly handle such cases - if (candidates.size() == 1) { - List dirtyInsns = candidates.getFirst(); - matchedLabels.put(cleanInsns, dirtyInsns); - cleanLabels.remove(cleanInsns); - dirtyLabels.remove(dirtyInsns); - } - } - - Pair, List> patchRange = findPatchHunkRange(cleanLabel, cleanLabelsOriginal, matchedLabels); - if (patchRange == null) { - return null; - } - - List> patchedLabels; - int to = dirtyLabelsOriginal.indexOf(patchRange.getSecond()); - if (patchRange.getFirst() == null) { - patchedLabels = dirtyLabelsOriginal.subList(0, to); - } else { - int from = dirtyLabelsOriginal.indexOf(patchRange.getFirst()) + 1; - if (from < to) { - patchedLabels = dirtyLabelsOriginal.subList(dirtyLabelsOriginal.indexOf(patchRange.getFirst()) + 1, to); - } else { - return null; - } - } - return new ComparisonResult(patchedLabels, cleanLabel); - } - - @Nullable - private static Pair<@Nullable List, List> findPatchHunkRange(List cleanLabel, List> cleanLabels, Map, List> matchedLabels) { - // Find last matched dirty label BEFORE the injection point - List dirtyLabelBefore; - int cleanLabelOrdinal = cleanLabels.indexOf(cleanLabel); - if (cleanLabelOrdinal == 0) { - dirtyLabelBefore = null; - } else { - dirtyLabelBefore = Stream.iterate(cleanLabelOrdinal, i -> i >= 0, i -> i - 1) - .map(i -> matchedLabels.get(cleanLabels.get(i))) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - if (dirtyLabelBefore == null) { - return null; - } - } - - // Find first matched dirty label AFTER the injection point - List dirtyLabelAfter = Stream.iterate(cleanLabels.indexOf(cleanLabel), i -> i < cleanLabels.size(), i -> i + 1) - .map(i -> matchedLabels.get(cleanLabels.get(i))) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - if (dirtyLabelAfter == null) { - return null; - } - - return Pair.of(dirtyLabelBefore, dirtyLabelAfter); - } - - private static List> getLabelsInMethod(MethodNode methodNode) { - List> list = new ArrayList<>(); - List workingList = null; - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof FrameNode) { - continue; - } - if (insn instanceof LabelNode || workingList == null) { - if (workingList != null) { - list.add(workingList); - } - workingList = new ArrayList<>(); - } - workingList.add(insn); - } - if (workingList != null) { - list.add(workingList); - } - return list; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/MixinGroups.java b/core/src/main/java/org/sinytra/adapter/analysis/MixinGroups.java deleted file mode 100644 index 7c51d748..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/MixinGroups.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.sinytra.adapter.analysis; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.AnnotationNode; -import org.objectweb.asm.tree.MethodNode; -import org.spongepowered.asm.mixin.injection.Group; -import org.spongepowered.asm.mixin.injection.struct.InjectorGroupInfo; -import org.spongepowered.asm.util.Annotations; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class MixinGroups { - private final Map groups; - - private MixinGroups(Map groups) { - this.groups = Map.copyOf(groups); - } - - @Nullable - public GroupInfo getGroupInfo(MethodNode methodNode) { - AnnotationNode node = Annotations.getInvisible(methodNode, Group.class); - if (node == null) return null; - - String name = Annotations.getValue(node, "name"); - if (name == null || name.isEmpty()) return null; - - return this.groups.get(name); - } - - public static MixinGroups create(List methods) { - InjectorGroupInfo.Map infos = new InjectorGroupInfo.Map(); - Multimap members = HashMultimap.create(); - - for (MethodNode method : methods) { - InjectorGroupInfo info = infos.parseGroup(method, "default"); - if (info.isDefault() || info.getName().equals("default")) continue; - - members.put(info.getName(), method); - } - - Map groups = new HashMap<>(); - members.asMap().forEach((name, mems) -> { - InjectorGroupInfo info = infos.forName(name); - groups.put(name, new GroupInfo(mems, info.getMinRequired())); - }); - - return new MixinGroups(groups); - } - - public record GroupInfo(Collection members, int minRequired) {} -} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/InsnComparator.java b/core/src/main/java/org/sinytra/adapter/analysis/compare/InsnComparator.java similarity index 99% rename from core/src/main/java/org/sinytra/adapter/analysis/InsnComparator.java rename to core/src/main/java/org/sinytra/adapter/analysis/compare/InsnComparator.java index 9d0ad3ff..2e0040a7 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/InsnComparator.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/compare/InsnComparator.java @@ -23,7 +23,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package org.sinytra.adapter.analysis; +package org.sinytra.adapter.analysis.compare; import org.objectweb.asm.tree.*; diff --git a/core/src/main/java/org/sinytra/adapter/analysis/InstructionMatcher.java b/core/src/main/java/org/sinytra/adapter/analysis/compare/InstructionMatcher.java similarity index 91% rename from core/src/main/java/org/sinytra/adapter/analysis/InstructionMatcher.java rename to core/src/main/java/org/sinytra/adapter/analysis/compare/InstructionMatcher.java index 6837237c..f7c12033 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/InstructionMatcher.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/compare/InstructionMatcher.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.analysis; +package org.sinytra.adapter.analysis.compare; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; @@ -7,10 +7,6 @@ import java.util.List; public record InstructionMatcher(AbstractInsnNode insn, List before, List after) { - public InstructionMatcher inverse() { - return new InstructionMatcher(insn, after.reversed(), before.reversed()); - } - public boolean test(InstructionMatcher other) { return test(other, 0); } diff --git a/core/src/main/java/org/sinytra/adapter/analysis/compare/MethodAlignment.java b/core/src/main/java/org/sinytra/adapter/analysis/compare/MethodAlignment.java new file mode 100644 index 00000000..ce360373 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/compare/MethodAlignment.java @@ -0,0 +1,363 @@ +package org.sinytra.adapter.analysis.compare; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Handle; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.analysis.SourceValue; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.*; +import java.util.function.IntFunction; + +public final class MethodAlignment { + private static final int MAX_LCS_CELLS = 250000; + + public enum Confidence { + EXACT, + FINGERPRINT, + POSITIONAL + } + + public record Match(AbstractInsnNode dirty, Confidence confidence) { + } + + public record Hunk(List dirtyInsns, @Nullable AbstractInsnNode cleanBefore, @Nullable AbstractInsnNode cleanAfter) { + } + + private record Fingerprint(String insn, List producers) { + } + + private record Pairing(int dirty, Confidence confidence, boolean inOrder) { + } + + private final List clean; + private final List dirty; + private final Map cleanIndices = new HashMap<>(); + private final Fingerprint[] cleanPrints; + private final Fingerprint[] dirtyPrints; + private final Map pairings = new HashMap<>(); + private final Map reverse = new HashMap<>(); + + private MethodAlignment(MethodNode cleanMethod, MethodNode dirtyMethod) { + this.clean = realInsns(cleanMethod); + this.dirty = realInsns(dirtyMethod); + for (int i = 0; i < this.clean.size(); i++) { + this.cleanIndices.put(this.clean.get(i), i); + } + this.cleanPrints = fingerprints(cleanMethod, this.clean); + this.dirtyPrints = fingerprints(dirtyMethod, this.dirty); + } + + public static MethodAlignment align(MethodNode cleanMethod, MethodNode dirtyMethod) { + MethodAlignment alignment = new MethodAlignment(cleanMethod, dirtyMethod); + alignment.alignInOrder(); + alignment.alignMoved(); + return alignment; + } + + public double similarity() { + int total = this.clean.size() + this.dirty.size(); + return total == 0 ? 1 : 2.0 * this.pairings.size() / total; + } + + public double dirtyCoverage() { + return this.dirty.isEmpty() ? 0 : (double) this.pairings.size() / this.dirty.size(); + } + + public List find(AbstractInsnNode cleanInsn) { + Integer index = indexOf(cleanInsn); + if (index == null) { + return List.of(); + } + Pairing pairing = this.pairings.get(index); + if (pairing != null) { + return List.of(new Match(this.dirty.get(pairing.dirty()), pairing.confidence())); + } + + int[] range = dirtyRangeAround(index); + List matches = new ArrayList<>(); + for (int i = range[0]; i < range[1]; i++) { + if (!this.reverse.containsKey(i) && this.dirtyPrints[i].insn().equals(this.cleanPrints[index].insn())) { + matches.add(new Match(this.dirty.get(i), Confidence.POSITIONAL)); + } + } + return matches; + } + + public List findDirtyAround(AbstractInsnNode cleanInsn) { + Integer index = indexOf(cleanInsn); + if (index == null || this.pairings.containsKey(index)) { + return List.of(); + } + int[] range = dirtyRangeAround(index); + List insns = new ArrayList<>(); + for (int i = range[0]; i < range[1]; i++) { + if (!this.reverse.containsKey(i)) { + insns.add(this.dirty.get(i)); + } + } + return insns; + } + + private int[] dirtyRangeAround(int index) { + int from = 0; + for (int i = index - 1; i >= 0; i--) { + if (isAnchor(i)) { + from = this.pairings.get(i).dirty() + 1; + break; + } + } + int to = this.dirty.size(); + for (int i = index + 1; i < this.clean.size(); i++) { + if (isAnchor(i)) { + to = this.pairings.get(i).dirty(); + break; + } + } + return new int[]{from, to}; + } + + private boolean isAnchor(int cleanIndex) { + Pairing pairing = this.pairings.get(cleanIndex); + return pairing != null && pairing.inOrder() && !OpcodeUtil.isLoadOpcode(this.clean.get(cleanIndex).getOpcode()); + } + + @Nullable + public AbstractInsnNode resolve(AbstractInsnNode cleanInsn) { + List matches = find(cleanInsn); + return matches.size() == 1 && matches.getFirst().confidence() != Confidence.POSITIONAL ? matches.getFirst().dirty() : null; + } + + public List findHunks() { + List hunks = new ArrayList<>(); + int start = -1; + for (int i = 0; i <= this.dirty.size(); i++) { + boolean unmatched = i < this.dirty.size() && !this.reverse.containsKey(i); + if (unmatched && start == -1) { + start = i; + } else if (!unmatched && start != -1) { + hunks.add(new Hunk(List.copyOf(this.dirty.subList(start, i)), cleanPartner(start - 1), cleanPartner(i))); + start = -1; + } + } + return hunks; + } + + @Nullable + private AbstractInsnNode cleanPartner(int dirtyIndex) { + Integer cleanIndex = dirtyIndex >= 0 && dirtyIndex < this.dirty.size() ? this.reverse.get(dirtyIndex) : null; + return cleanIndex != null ? this.clean.get(cleanIndex) : null; + } + + @Nullable + private Integer indexOf(AbstractInsnNode insn) { + AbstractInsnNode current = insn; + while (current != null && isPseudo(current)) { + current = current.getNext(); + } + return current != null ? this.cleanIndices.get(current) : null; + } + + private void alignInOrder() { + diff(0, this.clean.size(), 0, this.dirty.size(), i -> this.cleanPrints[i], i -> this.dirtyPrints[i], Confidence.EXACT); + + List matched = new ArrayList<>(this.pairings.keySet()); + Collections.sort(matched); + int prevClean = -1; + int prevDirty = -1; + for (int cleanIndex : matched) { + int dirtyIndex = this.pairings.get(cleanIndex).dirty(); + diff(prevClean + 1, cleanIndex, prevDirty + 1, dirtyIndex, i -> this.cleanPrints[i].insn(), i -> this.dirtyPrints[i].insn(), Confidence.FINGERPRINT); + prevClean = cleanIndex; + prevDirty = dirtyIndex; + } + diff(prevClean + 1, this.clean.size(), prevDirty + 1, this.dirty.size(), i -> this.cleanPrints[i].insn(), i -> this.dirtyPrints[i].insn(), Confidence.FINGERPRINT); + } + + private void alignMoved() { + Map> cleanUnmatched = new HashMap<>(); + for (int i = 0; i < this.clean.size(); i++) { + if (!this.pairings.containsKey(i)) { + cleanUnmatched.computeIfAbsent(this.cleanPrints[i], k -> new ArrayList<>()).add(i); + } + } + Map> dirtyUnmatched = new HashMap<>(); + for (int i = 0; i < this.dirty.size(); i++) { + if (!this.reverse.containsKey(i)) { + dirtyUnmatched.computeIfAbsent(this.dirtyPrints[i], k -> new ArrayList<>()).add(i); + } + } + cleanUnmatched.forEach((print, cleanIndices) -> { + List dirtyIndices = dirtyUnmatched.get(print); + if (cleanIndices.size() == 1 && dirtyIndices != null && dirtyIndices.size() == 1) { + pair(cleanIndices.getFirst(), dirtyIndices.getFirst(), Confidence.FINGERPRINT, false); + } + }); + } + + private void diff(int cleanFrom, int cleanTo, int dirtyFrom, int dirtyTo, IntFunction cleanKey, IntFunction dirtyKey, Confidence confidence) { + while (cleanFrom < cleanTo && dirtyFrom < dirtyTo && cleanKey.apply(cleanFrom).equals(dirtyKey.apply(dirtyFrom))) { + pair(cleanFrom++, dirtyFrom++, confidence, true); + } + while (cleanFrom < cleanTo && dirtyFrom < dirtyTo && cleanKey.apply(cleanTo - 1).equals(dirtyKey.apply(dirtyTo - 1))) { + pair(--cleanTo, --dirtyTo, confidence, true); + } + if (cleanFrom >= cleanTo || dirtyFrom >= dirtyTo) { + return; + } + + Map counts = new HashMap<>(); + for (int i = cleanFrom; i < cleanTo; i++) { + int[] entry = counts.computeIfAbsent(cleanKey.apply(i), k -> new int[]{0, 0, -1, -1}); + entry[0]++; + entry[2] = i; + } + for (int i = dirtyFrom; i < dirtyTo; i++) { + int[] entry = counts.get(dirtyKey.apply(i)); + if (entry != null) { + entry[1]++; + entry[3] = i; + } + } + List unique = new ArrayList<>(); + for (int[] entry : counts.values()) { + if (entry[0] == 1 && entry[1] == 1) { + unique.add(new int[]{entry[2], entry[3]}); + } + } + if (unique.isEmpty()) { + lcs(cleanFrom, cleanTo, dirtyFrom, dirtyTo, cleanKey, dirtyKey, confidence); + return; + } + unique.sort(Comparator.comparingInt(pair -> pair[0])); + List anchors = longestIncreasing(unique); + + int prevClean = cleanFrom - 1; + int prevDirty = dirtyFrom - 1; + for (int[] anchor : anchors) { + diff(prevClean + 1, anchor[0], prevDirty + 1, anchor[1], cleanKey, dirtyKey, confidence); + pair(anchor[0], anchor[1], confidence, true); + prevClean = anchor[0]; + prevDirty = anchor[1]; + } + diff(prevClean + 1, cleanTo, prevDirty + 1, dirtyTo, cleanKey, dirtyKey, confidence); + } + + private void lcs(int cleanFrom, int cleanTo, int dirtyFrom, int dirtyTo, IntFunction cleanKey, IntFunction dirtyKey, Confidence confidence) { + int n = cleanTo - cleanFrom; + int m = dirtyTo - dirtyFrom; + if ((long) n * m > MAX_LCS_CELLS) { + return; + } + int[][] table = new int[n + 1][m + 1]; + for (int i = n - 1; i >= 0; i--) { + for (int j = m - 1; j >= 0; j--) { + table[i][j] = cleanKey.apply(cleanFrom + i).equals(dirtyKey.apply(dirtyFrom + j)) + ? table[i + 1][j + 1] + 1 + : Math.max(table[i + 1][j], table[i][j + 1]); + } + } + int i = 0; + int j = 0; + while (i < n && j < m) { + if (cleanKey.apply(cleanFrom + i).equals(dirtyKey.apply(dirtyFrom + j))) { + pair(cleanFrom + i++, dirtyFrom + j++, confidence, true); + } else if (table[i + 1][j] >= table[i][j + 1]) { + i++; + } else { + j++; + } + } + } + + private static List longestIncreasing(List pairs) { + int[] tails = new int[pairs.size()]; + int[] previous = new int[pairs.size()]; + int length = 0; + for (int i = 0; i < pairs.size(); i++) { + int value = pairs.get(i)[1]; + int lo = 0; + int hi = length; + while (lo < hi) { + int mid = (lo + hi) >>> 1; + if (pairs.get(tails[mid])[1] < value) lo = mid + 1; + else hi = mid; + } + previous[i] = lo > 0 ? tails[lo - 1] : -1; + tails[lo] = i; + if (lo == length) length++; + } + List result = new ArrayList<>(); + for (int i = length > 0 ? tails[length - 1] : -1; i != -1; i = previous[i]) { + result.add(pairs.get(i)); + } + return result.reversed(); + } + + private void pair(int cleanIndex, int dirtyIndex, Confidence confidence, boolean inOrder) { + if (this.pairings.containsKey(cleanIndex) || this.reverse.containsKey(dirtyIndex)) { + return; + } + Confidence actual = confidence == Confidence.FINGERPRINT && inOrder && this.cleanPrints[cleanIndex].equals(this.dirtyPrints[dirtyIndex]) + ? Confidence.EXACT : confidence; + this.pairings.put(cleanIndex, new Pairing(dirtyIndex, actual, inOrder)); + this.reverse.put(dirtyIndex, cleanIndex); + } + + private static List realInsns(MethodNode method) { + List insns = new ArrayList<>(); + for (AbstractInsnNode insn : method.instructions) { + if (!isPseudo(insn)) { + insns.add(insn); + } + } + return insns; + } + + private static boolean isPseudo(AbstractInsnNode insn) { + return insn instanceof LabelNode || insn instanceof LineNumberNode || insn instanceof FrameNode; + } + + private static Fingerprint[] fingerprints(MethodNode method, List insns) { + DataFlow flow = DataFlow.of(method); + Fingerprint[] prints = new Fingerprint[insns.size()]; + boolean analyzable = true; + for (int i = 0; i < insns.size(); i++) { + AbstractInsnNode insn = insns.get(i); + List producers = List.of(); + if (analyzable) { + try { + producers = flow.operands(insn).stream().map(MethodAlignment::describeProducers).toList(); + } catch (RuntimeException e) { + analyzable = false; + } + } + prints[i] = new Fingerprint(describe(insn), producers); + } + return prints; + } + + private static String describeProducers(SourceValue value) { + return value.insns.size() == 1 ? describe(value.insns.iterator().next()) : "*"; + } + + private static String describe(AbstractInsnNode insn) { + int op = insn.getOpcode(); + return switch (insn) { + case MethodInsnNode m -> op + " " + m.owner + "." + m.name + m.desc; + case FieldInsnNode f -> op + " " + f.owner + "." + f.name + ":" + f.desc; + case TypeInsnNode t -> op + " " + t.desc; + case LdcInsnNode l -> "LDC " + l.cst.getClass().getSimpleName() + " " + l.cst; + case IntInsnNode n -> op + " " + n.operand; + case IincInsnNode n -> "IINC " + n.incr; + case MultiANewArrayInsnNode n -> op + " " + n.desc + " " + n.dims; + case TableSwitchInsnNode n -> op + " " + n.min + ".." + n.max; + case LookupSwitchInsnNode n -> op + " " + n.keys; + case InvokeDynamicInsnNode n -> op + " " + n.name + n.desc + " " + n.bsm.getName() + " " + Arrays.stream(n.bsmArgs) + .map(arg -> arg instanceof Handle handle ? handle.getDesc() : String.valueOf(arg)) + .toList(); + default -> String.valueOf(op); + }; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/method/AdvancedSourceInterpreter.java b/core/src/main/java/org/sinytra/adapter/analysis/flow/AdvancedSourceInterpreter.java similarity index 98% rename from core/src/main/java/org/sinytra/adapter/analysis/method/AdvancedSourceInterpreter.java rename to core/src/main/java/org/sinytra/adapter/analysis/flow/AdvancedSourceInterpreter.java index 39ec07c4..7fa35cd9 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/method/AdvancedSourceInterpreter.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/flow/AdvancedSourceInterpreter.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.analysis.method; +package org.sinytra.adapter.analysis.flow; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; diff --git a/core/src/main/java/org/sinytra/adapter/analysis/flow/DataFlow.java b/core/src/main/java/org/sinytra/adapter/analysis/flow/DataFlow.java new file mode 100644 index 00000000..a0a1940e --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/flow/DataFlow.java @@ -0,0 +1,455 @@ +package org.sinytra.adapter.analysis.flow; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.analysis.*; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.*; +import java.util.function.BiConsumer; + +public final class DataFlow { + public enum Producers { + /// Instructions that loaded the value, including DUP and SWAP + LOADER, + /// Instructions that actually computed the value + COMPUTER + } + + public enum ImplicitCalls { + /// No changes + KEEP, + /// Unwrap the inner arguments of boxing and unboxing calls + UNWRAP + } + + private static final Set BOXED_TYPES = Set.of( + "java/lang/Boolean", "java/lang/Byte", "java/lang/Character", "java/lang/Short", + "java/lang/Integer", "java/lang/Float", "java/lang/Long", "java/lang/Double" + ); + private static final Set UNBOXING_METHODS = Set.of( + "booleanValue", "byteValue", "charValue", "shortValue", "intValue", "floatValue", "longValue", "doubleValue" + ); + + private final MethodNode method; + @Nullable + private final ClassNode owner; + private final Producers producers; + @Nullable + private Frame[] frames; + + private DataFlow(MethodNode method, @Nullable ClassNode owner, Producers producers) { + this.method = method; + this.owner = owner; + this.producers = producers; + } + + public static DataFlow of(MethodNode method) { + return of(method, Producers.LOADER); + } + + public static DataFlow of(MethodNode method, Producers producers) { + return new DataFlow(method, null, producers); + } + + public static DataFlow of(TargetPair target) { + return of(target, Producers.LOADER); + } + + public static DataFlow of(TargetPair target, Producers producers) { + return new DataFlow(target.methodNode(), target.classNode(), producers); + } + + public MethodNode method() { + return this.method; + } + + public Producers producers() { + return this.producers; + } + + /// {@return the frame before the instruction} + @Nullable + public Frame frame(AbstractInsnNode insn) { + Frame[] frames = frames(); + int index = this.method.instructions.indexOf(insn); + return index >= 0 && index < frames.length ? frames[index] : null; + } + + /// {@return the stack values the insn consumes or an empty list if unreachable} + public List operands(AbstractInsnNode insn) { + Frame frame = frame(insn); + return frame != null ? operands(frame, insn) : List.of(); + } + + public List findReturns() { + List returns = new ArrayList<>(); + for (AbstractInsnNode insn : this.method.instructions) { + if (OpcodeUtil.isReturnOpcode(insn.getOpcode())) { + returns.add(insn); + } + } + return returns; + } + + public List findReturnedValues() { + List producers = new ArrayList<>(); + Set seen = new HashSet<>(); + for (AbstractInsnNode ret : findReturns()) { + for (SourceValue value : operands(ret)) { + value.insns.stream().filter(seen::add).forEach(producers::add); + } + } + return producers; + } + + /// {@return a single producer of each argument of the call or null if an arg has more producers} + @Nullable + public List resolveCallOperandProducers(MethodInsnNode call) { + if (!this.method.instructions.contains(call)) { + List calls = MethodAnalyzer.findCalls(this.method, MethodQualifier.create(call)); + return calls.size() == 1 ? resolveCallOperandProducers(calls.getFirst()) : null; + } + + Frame frame = frame(call); + if (frame == null) { + return null; + } + + List insns = new ArrayList<>(); + for (SourceValue src : operands(frame, call)) { + AbstractInsnNode srcInsn = AdapterUtil.getSingleInsn(src); + if (srcInsn == null) { + return null; + } + insns.add(srcInsn); + } + + return insns; + } + + /// {@return instructions of operands excluding the call insn} + @Nullable + public List resolveOperandProducers(MethodInsnNode call) { + List insns = resolveCallOperandProducers(call); + return insns != null && !insns.isEmpty() ? AdapterUtil.subListInsnsExc(insns.getFirst(), call) : null; + } + + /// {@return operands for each call matching the qualifier including the call insn} + public List> findCallsWithProducers(MethodQualifier qualifier) { + List> list = new ArrayList<>(); + for (MethodInsnNode call : MethodAnalyzer.findCalls(this.method, qualifier)) { + List insns = resolveCallOperandProducers(call); + if (insns != null) { + insns.add(call); + list.add(insns); + } + } + return list; + } + + /// {@return for each operand of the call, all instructions computing it in order, or an empty list} + public List> findOperandExpressions(MethodInsnNode call) { + List> args = new ArrayList<>(); + if (!this.method.instructions.contains(call)) { + return args; + } + + Frame frame = frame(call); + if (frame == null) { + return args; + } + + for (SourceValue value : operands(frame, call)) { + List insns = new ArrayList<>(); + for (AbstractInsnNode insn : value.insns) { + expandExpression(insns, insn); + insns.add(insn); + } + args.add(insns); + } + return args; + } + + /// Call the action for each reachable call with its operands + public void forEachCall(BiConsumer> action) { + for (AbstractInsnNode insn : this.method.instructions) { + if (insn instanceof MethodInsnNode minsn) { + Frame frame = frame(minsn); + if (frame != null) { // Dead code otherwise + action.accept(minsn, operands(frame, minsn)); + } + } + } + } + + /// Call the action for each reachable instance call with the producers of its receiver + public void forEachReceiver(BiConsumer> action) { + forEachCall((call, operands) -> { + if (call.getOpcode() != Opcodes.INVOKESTATIC && operands.size() == Type.getArgumentCount(call.desc) + 1) { + action.accept(call, operands.getFirst().insns); + } + }); + } + + /// {@return calls whose result is not used as an operand of another call in order} + public List findTopLevelCalls(ImplicitCalls implicitCalls) { + InsnList instructions = this.method.instructions; + + Set consumedCalls = new HashSet<>(); + for (AbstractInsnNode insn : instructions) { + if (insn instanceof MethodInsnNode call) { + traceInputs(call, consumedCalls, new HashSet<>()); + } + } + + List topLevelCalls = new ArrayList<>(); + for (AbstractInsnNode insn : instructions) { + if (insn instanceof MethodInsnNode call && !consumedCalls.contains(call)) { + if (implicitCalls == ImplicitCalls.UNWRAP && isImplicitCall(call)) { + unwrapImplicitCall(call, topLevelCalls, new HashSet<>()); + } else if (!topLevelCalls.contains(call)) { + topLevelCalls.add(call); + } + } + } + topLevelCalls.sort(Comparator.comparingInt(instructions::indexOf)); + + return topLevelCalls; + } + + /// {@return methods of the owner class that are called at the top level} + public List findTopLevelOwnCalls() { + if (this.owner == null) { + throw new IllegalStateException("Missing owner for data flow of " + this.method.name + this.method.desc); + } + List ownCalls = new ArrayList<>(); + for (MethodInsnNode insn : findTopLevelCalls(ImplicitCalls.KEEP)) { + MethodNode method = AdapterUtil.findMethod(this.owner, insn.name, insn.desc); + if (method != null) { + ownCalls.add(method); + } + } + return ownCalls; + } + + /// {@return instructions initializing each element of the array, including the array store insn} + public List> findArrayElements(AbstractInsnNode newArrayInsn) { + Frame[] frames = frames(); + + Map indexToStoreNode = new HashMap<>(); + InsnList instructions = this.method.instructions; + for (int i = 0; i < instructions.size(); i++) { + AbstractInsnNode insn = instructions.get(i); + + if (insn.getOpcode() == Opcodes.AASTORE) { + Frame frame = frames[i]; + if (frame == null) continue; + + if (frame.getStackSize() < 3) continue; + SourceValue arrayRefSource = frame.getStack(frame.getStackSize() - 3); + + if (arrayRefSource.insns.contains(newArrayInsn)) { + SourceValue indexSource = frame.getStack(frame.getStackSize() - 2); + Integer constantIndex = resolveConstantIndex(indexSource); + + if (constantIndex != null) { + indexToStoreNode.put(constantIndex, insn); + } + } + } + } + + List> groups = new ArrayList<>(); + AbstractInsnNode currentStart = newArrayInsn.getNext(); + int maxIndex = indexToStoreNode.keySet().stream().max(Integer::compare).orElse(-1); + + for (int i = 0; i <= maxIndex; i++) { + AbstractInsnNode endNode = indexToStoreNode.get(i); + List currentGroup = new ArrayList<>(); + + if (endNode != null) { + AbstractInsnNode ptr = currentStart; + while (ptr != null) { + currentGroup.add(ptr); + if (ptr == endNode) break; + ptr = ptr.getNext(); + } + currentStart = endNode.getNext(); + } + + groups.add(currentGroup); + } + + return groups; + } + + private Frame[] frames() { + if (this.frames == null) { + Interpreter interpreter = this.producers == Producers.COMPUTER ? new AdvancedSourceInterpreter(true) : new SourceInterpreter(); + try { + this.frames = new Analyzer<>(interpreter).analyze(Object.class.getName(), this.method); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return this.frames; + } + + private void expandExpression(List out, AbstractInsnNode insn) { + Frame frame = frame(insn); + if (frame == null) return; + + for (SourceValue value : operands(frame, insn)) { + for (AbstractInsnNode sourceInsn : value.insns) { + if (!out.contains(sourceInsn)) { + expandExpression(out, sourceInsn); + out.add(sourceInsn); + } + } + } + } + + private void unwrapImplicitCall(MethodInsnNode call, List out, Set seen) { + if (!seen.add(call)) return; + + Set wrapped = new HashSet<>(); + traceInputs(call, wrapped, new HashSet<>()); + if (wrapped.isEmpty()) { + if (!out.contains(call)) { + out.add(call); + } + return; + } + for (MethodInsnNode inner : wrapped) { + if (isImplicitCall(inner)) { + unwrapImplicitCall(inner, out, seen); + } else if (!out.contains(inner)) { + out.add(inner); + } + } + } + + private void collectProducerCalls(SourceValue value, Set consumedCalls, Set visited) { + for (AbstractInsnNode producer : value.insns) { + if (!visited.add(producer)) continue; + + if (producer instanceof MethodInsnNode producerCall) { + consumedCalls.add(producerCall); + continue; + } + + traceInputs(producer, consumedCalls, visited); + } + } + + private void traceInputs(AbstractInsnNode insn, Set consumedCalls, Set visited) { + Frame frame = frame(insn); + if (frame == null) return; + + for (SourceValue input : operands(frame, insn)) { + collectProducerCalls(input, consumedCalls, visited); + } + } + + private static boolean isImplicitCall(MethodInsnNode minsn) { + if (!BOXED_TYPES.contains(minsn.owner)) { + return false; + } + if (minsn.getOpcode() == Opcodes.INVOKESTATIC && minsn.name.equals("valueOf")) { + Type[] args = Type.getArgumentTypes(minsn.desc); + return args.length == 1 && args[0].getSort() <= Type.DOUBLE; + } + return minsn.getOpcode() == Opcodes.INVOKEVIRTUAL && UNBOXING_METHODS.contains(minsn.name) && Type.getArgumentCount(minsn.desc) == 0; + } + + private static Integer resolveConstantIndex(SourceValue source) { + for (AbstractInsnNode insn : source.insns) { + int index = AdapterUtil.getIntConstValue(insn).orElse(-1); + if (index != -1) { + return index; + } + } + return null; + } + + private static List operands(Frame frame, AbstractInsnNode insn) { + int stackSize = frame.getStackSize(); + int start = Math.max(0, stackSize - getPopCount(insn, frame)); + List operands = new ArrayList<>(stackSize - start); + for (int i = start; i < stackSize; i++) { + operands.add(frame.getStack(i)); + } + return operands; + } + + private static int getPopCount(AbstractInsnNode insn, @Nullable Frame frame) { + int op = insn.getOpcode(); + + if (insn instanceof MethodInsnNode minsn) { + int args = Type.getArgumentCount(minsn.desc); + return op == Opcodes.INVOKESTATIC ? args : args + 1; + } + + if (insn instanceof InvokeDynamicInsnNode indy) { + return Type.getArgumentCount(indy.desc); + } + + if (insn instanceof MultiANewArrayInsnNode multiANewArray) { + return multiANewArray.dims; + } + + return switch (op) { + case Opcodes.POP2, Opcodes.DUP2 -> isWide(frame, 0) ? 1 : 2; + case Opcodes.DUP_X2 -> isWide(frame, 1) ? 2 : 3; + case Opcodes.DUP2_X1 -> isWide(frame, 0) ? 2 : 3; + case Opcodes.DUP2_X2 -> { + if (isWide(frame, 0)) { + yield isWide(frame, 1) ? 2 : 3; + } + yield isWide(frame, 2) ? 3 : 4; + } + + case Opcodes.IASTORE, Opcodes.LASTORE, Opcodes.FASTORE, Opcodes.DASTORE, Opcodes.AASTORE, Opcodes.BASTORE, Opcodes.CASTORE, + Opcodes.SASTORE -> 3; + + case Opcodes.IADD, Opcodes.LADD, Opcodes.FADD, Opcodes.DADD, Opcodes.ISUB, Opcodes.LSUB, Opcodes.FSUB, + Opcodes.DSUB, Opcodes.IMUL, Opcodes.LMUL, Opcodes.FMUL, Opcodes.DMUL, Opcodes.IDIV, Opcodes.LDIV, + Opcodes.FDIV, Opcodes.DDIV, Opcodes.IREM, Opcodes.LREM, Opcodes.FREM, Opcodes.DREM, Opcodes.ISHL, + Opcodes.LSHL, Opcodes.ISHR, Opcodes.LSHR, Opcodes.IUSHR, Opcodes.LUSHR, Opcodes.IAND, Opcodes.LAND, + Opcodes.IOR, Opcodes.LOR, Opcodes.IXOR, Opcodes.LXOR, Opcodes.LCMP, Opcodes.FCMPL, Opcodes.FCMPG, + Opcodes.DCMPL, Opcodes.DCMPG, + Opcodes.IALOAD, Opcodes.LALOAD, Opcodes.FALOAD, Opcodes.DALOAD, Opcodes.AALOAD, Opcodes.BALOAD, Opcodes.CALOAD, + Opcodes.SALOAD, + Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE, Opcodes.IF_ICMPLT, Opcodes.IF_ICMPGE, Opcodes.IF_ICMPGT, Opcodes.IF_ICMPLE, + Opcodes.IF_ACMPEQ, Opcodes.IF_ACMPNE, + Opcodes.PUTFIELD, Opcodes.SWAP, Opcodes.DUP_X1 -> 2; + + case Opcodes.I2L, Opcodes.I2F, Opcodes.I2D, Opcodes.L2I, Opcodes.L2F, Opcodes.L2D, Opcodes.F2I, Opcodes.F2L, + Opcodes.F2D, Opcodes.D2I, Opcodes.D2L, Opcodes.D2F, Opcodes.I2B, Opcodes.I2C, Opcodes.I2S, Opcodes.INEG, + Opcodes.LNEG, Opcodes.FNEG, Opcodes.DNEG, Opcodes.CHECKCAST, Opcodes.INSTANCEOF, + Opcodes.ISTORE, Opcodes.LSTORE, Opcodes.FSTORE, Opcodes.DSTORE, Opcodes.ASTORE, Opcodes.POP, Opcodes.DUP, + Opcodes.IFEQ, Opcodes.IFNE, Opcodes.IFLT, Opcodes.IFGE, Opcodes.IFGT, Opcodes.IFLE, Opcodes.IFNULL, Opcodes.IFNONNULL, + Opcodes.TABLESWITCH, Opcodes.LOOKUPSWITCH, + Opcodes.IRETURN, Opcodes.LRETURN, Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN, Opcodes.ATHROW, + Opcodes.MONITORENTER, Opcodes.MONITOREXIT, Opcodes.ARRAYLENGTH, Opcodes.NEWARRAY, Opcodes.ANEWARRAY, + Opcodes.GETFIELD, Opcodes.PUTSTATIC -> 1; + + default -> 0; + }; + } + + private static boolean isWide(@Nullable Frame frame, int depth) { + if (frame == null) { + return false; + } + int index = frame.getStackSize() - 1 - depth; + return index >= 0 && frame.getStack(index).getSize() == 2; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/injection/InjectionLocator.java b/core/src/main/java/org/sinytra/adapter/analysis/injection/InjectionLocator.java new file mode 100644 index 00000000..5d409748 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/injection/InjectionLocator.java @@ -0,0 +1,162 @@ +package org.sinytra.adapter.analysis.injection; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.InsnList; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.SliceData; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.MixinBridge; + +import java.util.*; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.*; + +public final class InjectionLocator { + private final MixinContext context; + private final Map> cache; + + private record CacheKey(TargetPair target, boolean keepShift, boolean ignoreSlice, + @Nullable Object injectionPoint, @Nullable Object methodAnnotation + ) { + } + + public InjectionLocator(MixinContext context) { + this.context = context; + this.cache = new HashMap<>(); + } + + /// {@return instructions matching mixin's @At selector in the target method} + public List find(@Nullable TargetPair target) { + return find(target, InjectionQuery.current()); + } + + /// {@return instructions matching the query in the target method} + public List find(@Nullable TargetPair target, InjectionQuery query) { + if (target == null) { + return List.of(); + } + if (query.source() != InjectionQuery.Source.CURRENT) { + return compute(target, query); + } + CacheKey key = new CacheKey( + target, query.keepShift(), query.ignoreSlice(), + annotationKey(this.context.injectionPointAnnotation()), annotationKey(this.context.methodAnnotation()) + ); + List insns = this.cache.get(key); + if (insns == null) { + insns = compute(target, query); + this.cache.put(key, insns); + } + return insns; + } + + /// {@return a single instruction the mixin's @At matches in the target method or null if there isn't exactly one} + @Nullable + public AbstractInsnNode resolve(@Nullable TargetPair target) { + return resolve(target, InjectionQuery.current()); + } + + /// {@return a single instruction the query matches in the target method or null if there isn't exactly one} + @Nullable + public AbstractInsnNode resolve(@Nullable TargetPair target, InjectionQuery query) { + List insns = find(target, query); + return insns.size() == 1 ? insns.getFirst() : null; + } + + /// {@return the first instruction the mixin's @At matches in the target method, or null if it matches none} + @Nullable + public AbstractInsnNode resolveFirst(@Nullable TargetPair target) { + return resolveFirst(target, InjectionQuery.current()); + } + + /// {@return the first instruction the query matches in the target method, or null if it matches none} + @Nullable + public AbstractInsnNode resolveFirst(@Nullable TargetPair target, InjectionQuery query) { + List insns = find(target, query); + return insns.isEmpty() ? null : insns.getFirst(); + } + + /// {@return whether the mixin's @At matches any instructions in the target method} + public boolean matches(@Nullable TargetPair target) { + return !find(target).isEmpty(); + } + + /// {@return whether the query matches any instructions in the target method} + public boolean matches(@Nullable TargetPair target, InjectionQuery query) { + return !find(target, query).isEmpty(); + } + + /// {@return the instructions of the target method within the slice} + public InsnList slice(TargetPair target, SliceData slice) { + return session(target).slice(slice.toAnnotationNode()); + } + + private List compute(TargetPair target, InjectionQuery query) { + AnnotationHandle injectionPoint = this.context.injectionPointAnnotation(); + InjectionQuery.Source kind = query.source() == InjectionQuery.Source.CURRENT && injectionPoint == null ? InjectionQuery.Source.CONSTANT : query.source(); + AnnotationHandle source = switch (kind) { + case CURRENT -> injectionPoint; + case AT, SLICE_BOUNDARY -> injectionPoint != null ? new AnnotationHandle(Objects.requireNonNull(query.at()).toAnnotationNode()) : null; + case CONSTANT -> query.constantData() != null ? new AnnotationHandle(query.constantData().toAnnotationNode()) + : this.context.methodAnnotation().getNested(PROPERTY_CONSTANT).orElse(null); + }; + if (source == null) { + return List.of(); + } + AnnotationHandle node = source.copy(); + if (!query.keepShift()) { + node.removeValues(AT_SHIFT); + } + + MixinBridge.Session session = session(target); + AnnotationNode slice = query.ignoreSlice() ? null : getSlice(); + AnnotationHandle methodAnnotation = this.context.methodAnnotation(); + return switch (kind) { + case CURRENT, SLICE_BOUNDARY -> session.findInjectionPoint(methodAnnotation.unwrap(), node.unwrap(), slice); + case AT -> { + AnnotationHandle parent = methodAnnotation.copy(); + parent.setOrAppendNonNull(PROPERTY_AT, node.unwrap()); + yield session.findInjectionPoint(parent.unwrap(), node.unwrap(), slice); + } + case CONSTANT -> session.findConstant(node.unwrap(), Type.getReturnType(this.context.methodNode().desc).getDescriptor(), slice); + }; + } + + private MixinBridge.Session session(TargetPair target) { + return MixinBridge.session(this.context.classNode().name, this.context.methodNode(), target.classNode(), target.methodNode(), this.context.environment()); + } + + @Nullable + private AnnotationNode getSlice() { + return this.context.methodAnnotation().getValue(PROPERTY_SLICE) + .map(handle -> handle.get() instanceof List list ? (list.isEmpty() ? null : (AnnotationNode) list.getFirst()) : (AnnotationNode) handle.get()) + .orElse(null); + } + + @Nullable + private static Object annotationKey(@Nullable AnnotationHandle handle) { + return handle != null ? annotationValueKey(handle.unwrap()) : null; + } + + private static Object annotationValueKey(Object value) { + if (value instanceof AnnotationNode node) { + List key = new ArrayList<>(); + key.add(node.desc); + if (node.values != null) { + node.values.forEach(v -> key.add(annotationValueKey(v))); + } + return key; + } + if (value instanceof List list) { + return list.stream().map(InjectionLocator::annotationValueKey).toList(); + } + if (value instanceof Object[] array) { + return Arrays.stream(array).map(InjectionLocator::annotationValueKey).toList(); + } + return value; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/injection/InjectionQuery.java b/core/src/main/java/org/sinytra/adapter/analysis/injection/InjectionQuery.java new file mode 100644 index 00000000..3a8d241b --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/injection/InjectionQuery.java @@ -0,0 +1,93 @@ +package org.sinytra.adapter.analysis.injection; + +import org.jetbrains.annotations.Nullable; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.ann.ConstantData; + +public final class InjectionQuery { + public enum Source { + /// The mixin's own @At + CURRENT, + /// The supplied @At + AT, + /// The supplied @At within the mixin's slice + SLICE_BOUNDARY, + /// The mixin's @Constant or the supplied one + CONSTANT + } + + private final Source source; + @Nullable + private final AtData at; + @Nullable + private final ConstantData constant; + private final boolean keepShift; + private final boolean ignoreSlice; + + private InjectionQuery(Source source, @Nullable AtData at, @Nullable ConstantData constant, boolean keepShift, boolean ignoreSlice) { + this.source = source; + this.at = at; + this.constant = constant; + this.keepShift = keepShift; + this.ignoreSlice = ignoreSlice; + } + + /// {@return a query for the mixin's own @At} + public static InjectionQuery current() { + return new InjectionQuery(Source.CURRENT, null, null, false, false); + } + + /// {@return a query for the given @At} + public static InjectionQuery of(AtData at) { + return new InjectionQuery(Source.AT, at, null, false, false); + } + + /// {@return a query for an @At within the mixin's slice, including shift} + public static InjectionQuery sliceBoundary(AtData at) { + return new InjectionQuery(Source.SLICE_BOUNDARY, at, null, true, true); + } + + /// {@return a query for the mixin's @Constant} + public static InjectionQuery constant() { + return new InjectionQuery(Source.CONSTANT, null, null, false, false); + } + + /// {@return a query for the given @Constant} + public static InjectionQuery of(ConstantData constant) { + return new InjectionQuery(Source.CONSTANT, null, constant, false, false); + } + + + /// {@return this query with the injection point's shift applied} + public InjectionQuery keepingShift() { + return new InjectionQuery(this.source, this.at, this.constant, true, this.ignoreSlice); + } + + /// {@return this query ignoring the mixin's slice} + public InjectionQuery ignoringSlice() { + return new InjectionQuery(this.source, this.at, this.constant, this.keepShift, true); + } + + public Source source() { + return this.source; + } + + @Nullable + public AtData at() { + return this.at; + } + + @Nullable + public ConstantData constantData() { + return this.constant; + } + + + public boolean keepShift() { + return this.keepShift; + } + + public boolean ignoreSlice() { + return this.ignoreSlice; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalAlignment.java b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalAlignment.java new file mode 100644 index 00000000..f31deb9f --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalAlignment.java @@ -0,0 +1,42 @@ +package org.sinytra.adapter.analysis.locals; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.IincInsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; + +import java.util.function.UnaryOperator; + +public final class LocalAlignment { + private final MethodAlignment alignment; + + private LocalAlignment(MethodAlignment alignment) { + this.alignment = alignment; + } + + public static LocalAlignment of(MethodAlignment alignment) { + return new LocalAlignment(alignment); + } + + public int resolve(int cleanSlot, AbstractInsnNode cleanInsn) { + int slot = findAlignedUse(cleanSlot, cleanInsn, AbstractInsnNode::getNext); + return slot != -1 ? slot : findAlignedUse(cleanSlot, cleanInsn.getPrevious(), AbstractInsnNode::getPrevious); + } + + private int findAlignedUse(int slot, @Nullable AbstractInsnNode start, UnaryOperator direction) { + for (AbstractInsnNode insn = start; insn != null; insn = direction.apply(insn)) { + if (getSlot(insn) == slot) { + AbstractInsnNode dirty = this.alignment.resolve(insn); + if (dirty != null && dirty.getOpcode() == insn.getOpcode()) { + return getSlot(dirty); + } + } + } + return -1; + } + + private static int getSlot(AbstractInsnNode insn) { + return insn instanceof VarInsnNode var ? var.var : insn instanceof IincInsnNode iinc ? iinc.var : -1; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVarAnalyzer.java b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVarAnalyzer.java deleted file mode 100644 index 7dfe7f82..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVarAnalyzer.java +++ /dev/null @@ -1,226 +0,0 @@ -package org.sinytra.adapter.analysis.locals; - -import com.mojang.logging.LogUtils; -import it.unimi.dsi.fastutil.ints.Int2IntMap; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.InsnTraversal; -import org.sinytra.adapter.analysis.InstructionMatcher; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; -import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.env.ctx.LocalVariable; -import org.sinytra.adapter.env.ctx.MethodHelper; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameter; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.transform.param.TransformParameters; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.OpcodeUtil; -import org.slf4j.Logger; - -import java.util.*; -import java.util.function.Consumer; -import java.util.stream.IntStream; - -public final class LocalVarAnalyzer { - private static final Logger LOGGER = LogUtils.getLogger(); - - public record CapturedLocals(TargetPair target, boolean isStatic, int paramLocalStart, int paramLocalEnd, int lvtOffset, - List expected, LocalVariableLookup lvt) { - } - - private record CapturedParameters( - /// The index of the first captured parameter - int start, - List types - ) { - } - - @Nullable - private static CapturedParameters findCapturedParameters(MethodNode methodNode) { - List params = Parameters.parse(methodNode); - for (int i = 0; i < params.size() - 1; i++) { - if (MethodParameters.ParamGroup.CI_CIR.predicate().test(params.get(i))) { - List types = params.subList(i + 1, params.size()).stream() - .takeWhile(p -> !p.isLocalOrShare()) - .map(Parameter::type) - .toList(); - return new CapturedParameters(i + 1, types); - } - } - return null; - } - - public static boolean capturesLocals(MixinContext context) { - CapturedParameters captured = findCapturedParameters(context.methodNode()); - return captured != null && !captured.types().isEmpty(); - } - - @Nullable - public static CapturedLocals findCapturedLocals(MixinContext context, Recipe recipe) { - TargetPair target = recipe.getDirtyTarget(); - return target != null ? findCapturedLocals(context, target) : null; - } - - @Nullable - public static CapturedLocals findCapturedLocals(MixinContext context, TargetPair dirtyTarget) { - MethodNode methodNode = context.methodNode(); - CapturedParameters captured = findCapturedParameters(methodNode); - if (captured == null) { - LOGGER.debug("Missing CI or CIR argument in injector of type {}", context.methodAnnotation().getDesc()); - return null; - } - if (captured.types().isEmpty()) { - return null; - } - LocalVariableLookup lvt = context.methods().getLVT(methodNode); - if (lvt == null) { - LOGGER.debug("Missing local variable table in injector {}", methodNode.name); - return null; - } - boolean isStatic = MethodHelper.isStatic(methodNode); - int end = captured.start() + captured.types().size() - 1; - return new CapturedLocals(dirtyTarget, isStatic, captured.start(), end, isStatic ? 0 : 1, captured.types(), lvt); - } - - public record CapturedLocalsInfo(CapturedLocals capturedLocals, LayeredParamsDiffSnapshot diff, List availableTypes) { - } - - @Nullable - public static CapturedLocalsInfo getCapturedLocals(MixinContext context, TargetPair dirtyTarget) { - CapturedLocals capturedLocals = findCapturedLocals(context, dirtyTarget); - if (capturedLocals == null) return null; - - // Get available local variables at the injection point in the target method - List available = context.methods().getTargetMethodLocals(capturedLocals.target()); - if (available == null) return null; - - List availableTypes = available.stream().map(LocalVariable::type).toList(); - // Compare expected and available params - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(capturedLocals.expected(), availableTypes); - if (diff == null) return null; - - return new CapturedLocalsInfo(capturedLocals, diff, availableTypes); - } - - public static List findStoreInsns(MethodNode methodNode, int index) { - List stores = new ArrayList<>(); - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof VarInsnNode varInsn && varInsn.var == index && OpcodeUtil.isStoreOpcode(varInsn.getOpcode())) { - stores.add(varInsn); - } - } - return stores; - } - - public static InsnList findInitializerInsns(MethodNode methodNode, int index) { - return collectInitializerInsns(methodNode, index, load -> { - }); - } - - public static Optional findLocalWithInitializer(MethodNode method, String desc, InsnList initializer) { - if (method.localVariables == null) { - return Optional.empty(); - } - return AdapterUtil.findOne(method.localVariables, lvn -> desc.equals(lvn.desc) - && InstructionMatcher.test(initializer, findInitializerInsns(method, lvn.index))); - } - - private static InsnList collectInitializerInsns(MethodNode methodNode, int index, Consumer onLoad) { - InsnList insns = new InsnList(); - List stores = findStoreInsns(methodNode, index); - if (!stores.isEmpty()) { - for (AbstractInsnNode prev : InsnTraversal.untilLabel(stores.getFirst(), InsnTraversal.BACKWARD)) { - if (prev instanceof VarInsnNode load && OpcodeUtil.isLoadOpcode(load.getOpcode())) { - onLoad.accept(load); - } - insns.insert(prev.clone(Map.of())); - } - } - return insns; - } - - public record CapturedLocalsUsage( - @Nullable LocalVariableLookup targetTable, - Int2ObjectMap usedLocals, - Int2IntMap usageCount, - Int2ObjectMap varInsnLists - ) { - } - - public record CapturedLocalsTransform(Collection used, TransformParameters remover, Collection usedLocalNodes) { - public CapturedLocalsUsage getUsage(CapturedLocals captured, MixinContext context) { - LocalVariableLookup targetTable = context.methods().getLVT(captured.target().methodNode()); - Int2ObjectMap usedLocals = new Int2ObjectOpenHashMap<>(); - Int2ObjectMap varInsnLists = new Int2ObjectOpenHashMap<>(); - Int2IntMap usageCount = new Int2IntOpenHashMap(); - - // Captured locals are the locals available at the injection point, in order - List available = context.methods().getTargetMethodLocals(captured.target()); - if (targetTable != null && available != null) { - this.used.forEach(ordinal -> { - int localIndex = ordinal - captured.paramLocalStart(); - if (localIndex >= 0 && localIndex < available.size()) { - LocalVariable local = available.get(localIndex); - usedLocals.put((int) ordinal, local); - findVariableInitializerInsns(captured.target().methodNode(), captured.isStatic(), local.index(), varInsnLists, usageCount); - } - }); - } - return new CapturedLocalsUsage(targetTable, usedLocals, usageCount, varInsnLists); - } - } - - public static CapturedLocalsTransform analyzeCapturedLocals(CapturedLocals capturedLocals, MethodNode methodNode) { - // Find used captured locals - int paramLocalStart = capturedLocals.paramLocalStart(); - LocalVariableLookup table = capturedLocals.lvt(); - Set used = new HashSet<>(); - Set usedLocalNodes = new HashSet<>(); - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof VarInsnNode varInsn) { - LocalVariableNode node = table.getByIndexOrNull(varInsn.var); - if (node == null) - continue; - int ordinal = table.getParameterOrdinal(node); - if (ordinal == -1) - continue; - if (ordinal >= paramLocalStart && ordinal <= capturedLocals.paramLocalEnd()) { - used.add(ordinal); - usedLocalNodes.add(node); - } - } - } - // Remove unused captured locals - TransformParameters remover = TransformParameters.builder() - .chain(b -> IntStream.range(paramLocalStart, capturedLocals.paramLocalEnd() + 1) - .filter(i -> !used.contains(i)) - .boxed().sorted(Collections.reverseOrder()) - .forEach(b::remove)) - .build(); - return new CapturedLocalsTransform(used, remover, usedLocalNodes); - } - - public static void findVariableInitializerInsns(MethodNode methodNode, boolean isStatic, int index, Int2ObjectMap varInsnLists, Int2IntMap usageCount) { - InsnList insns = collectInitializerInsns(methodNode, index, load -> { - // TODO Handle method params - if (isStatic || load.var != 0) { - if (!varInsnLists.containsKey(load.var)) { - findVariableInitializerInsns(methodNode, isStatic, load.var, varInsnLists, usageCount); - } - usageCount.compute(load.var, (key, existing) -> existing == null ? 1 : existing + 1); - } - }); - varInsnLists.put(index, insns); - } - - private LocalVarAnalyzer() { - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/LocalVariable.java b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVariable.java similarity index 65% rename from core/src/main/java/org/sinytra/adapter/env/ctx/LocalVariable.java rename to core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVariable.java index 1e79c497..85ae4cee 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/LocalVariable.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVariable.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.analysis.locals; import org.objectweb.asm.Type; diff --git a/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVariableLookup.java b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVariableLookup.java index 7ccaedcf..dcf6c37c 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVariableLookup.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalVariableLookup.java @@ -4,13 +4,16 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MethodHelper; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; import java.util.*; public class LocalVariableLookup { + private final InsnList instructions; private final List sortedLocals; private final boolean isNonStatic; private final int paramCount; @@ -18,8 +21,14 @@ public class LocalVariableLookup { private final Int2ObjectMap byIndex = new Int2ObjectOpenHashMap<>(); private final Map> byType = new HashMap<>(); + @Nullable + public static LocalVariableLookup of(MethodNode method) { + return method.localVariables != null ? new LocalVariableLookup(method) : null; + } + public LocalVariableLookup(MethodNode methodNode) { - this.isNonStatic = !MethodHelper.isStatic(methodNode); + this.instructions = methodNode.instructions; + this.isNonStatic = !MethodAnalyzer.isStatic(methodNode); List localVariables = methodNode.localVariables; this.sortedLocals = localVariables == null ? List.of() : localVariables.stream() @@ -67,6 +76,17 @@ public LocalVariableNode getByIndexOrNull(int index) { return this.byIndex.get(index); } + @Nullable + public LocalVariableNode getInScopeOrNull(int index, AbstractInsnNode insn) { + int position = this.instructions.indexOf(insn); + for (LocalVariableNode node : this.sortedLocals) { + if (node.index == index && this.instructions.indexOf(node.start) <= position && position < this.instructions.indexOf(node.end)) { + return node; + } + } + return null; + } + public int getOrdinal(LocalVariableNode node) { return this.sortedLocals.indexOf(node); } @@ -92,14 +112,4 @@ public List getForType(Type type) { .filter(l -> Type.getType(l.desc).equals(type)) .toList()); } - - public Optional getByTypedOrdinal(Type type, int ordinal) { - List available = getForType(type); - return ordinal >= 0 && available.size() > ordinal ? Optional.of(available.get(ordinal)) : Optional.empty(); - } - - public Optional getTypedOrdinal(LocalVariableNode node) { - int ordinal = getForType(node).indexOf(node); - return ordinal == -1 ? Optional.empty() : Optional.of(ordinal); - } } diff --git a/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalsAnalyzer.java b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalsAnalyzer.java new file mode 100644 index 00000000..fb6d1034 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/locals/LocalsAnalyzer.java @@ -0,0 +1,266 @@ +package org.sinytra.adapter.analysis.locals; + +import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.InsnTraversal; +import org.sinytra.adapter.analysis.compare.InstructionMatcher; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; +import org.sinytra.adapter.analysis.params.ParamDiffResolver; +import org.sinytra.adapter.analysis.params.ParamsEditDistance; +import org.sinytra.adapter.edit.param.TransformParameters; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.MixinBridge; +import org.sinytra.adapter.util.MixinBridge.LocalCandidate; +import org.sinytra.adapter.util.OpcodeUtil; +import org.slf4j.Logger; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; + +public final class LocalsAnalyzer { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final MixinContext context; + + public LocalsAnalyzer(MixinContext context) { + this.context = context; + } + + public record CapturedLocals(TargetPair target, boolean isStatic, int paramLocalStart, int paramLocalEnd, int lvtOffset, + List expected, LocalVariableLookup lvt) { + } + + private record CapturedParameters( + /// The index of the first captured parameter + int start, + List types + ) { + } + + @Nullable + private static CapturedParameters findCapturedParameters(MethodNode methodNode) { + List params = Parameters.parse(methodNode); + for (int i = 0; i < params.size() - 1; i++) { + if (MethodParameters.ParamGroup.CI_CIR.predicate().test(params.get(i))) { + List types = params.subList(i + 1, params.size()).stream() + .takeWhile(p -> !p.isLocalOrShare()) + .map(Parameter::type) + .toList(); + return new CapturedParameters(i + 1, types); + } + } + return null; + } + + public boolean capturesLocals() { + CapturedParameters captured = findCapturedParameters(this.context.methodNode()); + return captured != null && !captured.types().isEmpty(); + } + + @Nullable + public CapturedLocals resolveCapturedLocals(@Nullable TargetPair dirtyTarget) { + if (dirtyTarget == null) { + return null; + } + MethodNode methodNode = this.context.methodNode(); + CapturedParameters captured = findCapturedParameters(methodNode); + if (captured == null) { + LOGGER.debug("Missing CI or CIR argument in injector of type {}", this.context.methodAnnotation().getDesc()); + return null; + } + if (captured.types().isEmpty()) { + return null; + } + LocalVariableLookup lvt = LocalVariableLookup.of(methodNode); + if (lvt == null) { + LOGGER.debug("Missing local variable table in injector {}", methodNode.name); + return null; + } + boolean isStatic = MethodAnalyzer.isStatic(methodNode); + int end = captured.start() + captured.types().size() - 1; + return new CapturedLocals(dirtyTarget, isStatic, captured.start(), end, isStatic ? 0 : 1, captured.types(), lvt); + } + + public record CapturedLocalsInfo(CapturedLocals capturedLocals, LayeredParamsDiffSnapshot diff) { + } + + @Nullable + public CapturedLocalsInfo resolveCapturedLocalsInfo(TargetPair dirtyTarget) { + CapturedLocals capturedLocals = resolveCapturedLocals(dirtyTarget); + if (capturedLocals == null) return null; + + // Get available local variables at the injection point in the target method + List available = resolveAvailableLocals(capturedLocals.target()); + if (available == null) return null; + + List availableTypes = available.stream().map(LocalVariable::type).toList(); + // Compare expected and available params + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(capturedLocals.expected(), availableTypes, this.context.typeRelation()); + + return new CapturedLocalsInfo(capturedLocals, diff); + } + + public static List findStoreInsns(MethodNode methodNode, int index) { + List stores = new ArrayList<>(); + for (AbstractInsnNode insn : methodNode.instructions) { + if (insn instanceof VarInsnNode varInsn && varInsn.var == index && OpcodeUtil.isStoreOpcode(varInsn.getOpcode())) { + stores.add(varInsn); + } + } + return stores; + } + + public static InsnList findInitializerInsns(MethodNode methodNode, int index) { + return collectInitializerInsns(methodNode, index, load -> { + }); + } + + public static Optional findLocalWithInitializer(MethodNode method, String desc, InsnList initializer) { + if (method.localVariables == null) { + return Optional.empty(); + } + return AdapterUtil.findOne(method.localVariables, lvn -> desc.equals(lvn.desc) + && InstructionMatcher.test(initializer, findInitializerInsns(method, lvn.index))); + } + + private static InsnList collectInitializerInsns(MethodNode methodNode, int index, Consumer onLoad) { + InsnList insns = new InsnList(); + List stores = findStoreInsns(methodNode, index); + if (!stores.isEmpty()) { + for (AbstractInsnNode prev : InsnTraversal.untilLabel(stores.getFirst(), InsnTraversal.BACKWARD)) { + if (prev instanceof VarInsnNode load && OpcodeUtil.isLoadOpcode(load.getOpcode())) { + onLoad.accept(load); + } + insns.insert(prev.clone(Map.of())); + } + } + return insns; + } + + public record CapturedLocalsTransform(Collection used, TransformParameters remover) { + } + + public List findSelectableLocals(TargetPair target, AbstractInsnNode insn, Type type) { + return MixinBridge.session(this.context.classNode().name, this.context.methodNode(), target.classNode(), target.methodNode(), this.context.environment()) + .findLocalCandidates(type, insn, false); + } + + public int resolveSelectedLocal(TargetPair target, AbstractInsnNode insn, Type type) { + return MixinBridge.session(this.context.classNode().name, this.context.methodNode(), target.classNode(), target.methodNode(), this.context.environment()) + .findLocal(this.context.methodAnnotation().unwrap(), type, insn); + } + + public int resolveLocalOrdinal(TargetPair target, AbstractInsnNode insn, Type type, int index) { + return findSelectableLocals(target, insn, type).stream() + .filter(c -> c.index() == index) + .mapToInt(LocalCandidate::ordinal) + .findFirst() + .orElse(-1); + } + + public int resolveLocalIndex(TargetPair target, AbstractInsnNode insn, Type type, int ordinal) { + return findSelectableLocals(target, insn, type).stream() + .filter(c -> c.ordinal() == ordinal) + .mapToInt(LocalCandidate::index) + .findFirst() + .orElse(-1); + } + + @Nullable + public List resolveAvailableLocals(TargetPair target) { + Type[] targetParams = Type.getArgumentTypes(target.methodNode().desc); + int lvtOffset = MethodAnalyzer.isStatic(target.methodNode()) ? 0 : 1; + int targetLocalPos = targetParams.length + lvtOffset; + + List targetInsns = this.context.injections().find(target); + if (targetInsns.isEmpty()) { + LOGGER.debug("Skipping LVT patch, no target instructions found"); + return null; + } + + LocalVariableNode[] localVariables = MixinBridge.getLocalsAt(target.classNode(), target.methodNode(), targetInsns.getFirst(), this.context.environment().fabricLVTCompatibility()); + LocalVariable[] locals = Stream.of(localVariables) + .filter(Objects::nonNull) + .map(lv -> new LocalVariable(lv.index, Type.getType(lv.desc))) + .toArray(LocalVariable[]::new); + return AdapterUtil.summariseLocals(locals, targetLocalPos); + } + + public List findDirtyCapturedParams(Configuration clean, Configuration dirty) { + List cleanCaptured = clean.getParameters().getTypes(CAPTURED_PARAMS); + List dirtyCaptured = new ArrayList<>(); + + if (!cleanCaptured.isEmpty()) { + TargetPair cleanPair = this.context.cleanMethods().resolve(clean.getTargetMethod()); + TargetPair dirtyPair = this.context.dirtyMethods().resolve(dirty.getTargetMethod()); + if (cleanPair == null || dirtyPair == null) return dirtyCaptured; + MethodNode cleanTarget = cleanPair.methodNode(); + MethodNode dirtyTarget = dirtyPair.methodNode(); + + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffParameters(cleanTarget, dirtyTarget, this.context.typeRelation()); + + List cleanTargetParams = Parameters.getParameterTypes(cleanTarget.desc); + if (cleanCaptured.size() > cleanTargetParams.size()) return dirtyCaptured; + + List capturedSublist = cleanTargetParams.subList(0, cleanCaptured.size()); + if (!cleanCaptured.equals(capturedSublist)) return dirtyCaptured; + + ParamDiffResolver.ParamEvalResult evalResult = ParamDiffResolver.resolve(cleanTargetParams, diff); + + int maxIndex = capturedSublist.stream() + .map(evalResult::getUpdated) + .filter(Objects::nonNull) + .mapToInt(ParamDiffResolver.ParamState::dirtyIndex) + .max() + .orElse(-1); + + if (maxIndex != -1) { + List dirtyTargetParams = Parameters.getParameterTypes(dirtyTarget.desc); + dirtyCaptured = List.copyOf(dirtyTargetParams.subList(0, maxIndex + 1)); + } + } + + return dirtyCaptured; + } + + public static CapturedLocalsTransform analyzeCapturedLocals(CapturedLocals capturedLocals, MethodNode methodNode) { + // Find used captured locals + int paramLocalStart = capturedLocals.paramLocalStart(); + LocalVariableLookup table = capturedLocals.lvt(); + Set used = new HashSet<>(); + for (AbstractInsnNode insn : methodNode.instructions) { + if (insn instanceof VarInsnNode varInsn) { + LocalVariableNode node = table.getByIndexOrNull(varInsn.var); + if (node == null) + continue; + int ordinal = table.getParameterOrdinal(node); + if (ordinal == -1) + continue; + if (ordinal >= paramLocalStart && ordinal <= capturedLocals.paramLocalEnd()) { + used.add(ordinal); + } + } + } + // Remove unused captured locals + TransformParameters remover = TransformParameters.builder() + .chain(b -> IntStream.range(paramLocalStart, capturedLocals.paramLocalEnd() + 1) + .filter(i -> !used.contains(i)) + .boxed().sorted(Collections.reverseOrder()) + .forEach(b::remove)) + .build(); + return new CapturedLocalsTransform(used, remover); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/InheritanceHandler.java b/core/src/main/java/org/sinytra/adapter/analysis/method/InheritanceHandler.java similarity index 93% rename from core/src/main/java/org/sinytra/adapter/analysis/InheritanceHandler.java rename to core/src/main/java/org/sinytra/adapter/analysis/method/InheritanceHandler.java index 77b62b76..39a4bd27 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/InheritanceHandler.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/method/InheritanceHandler.java @@ -1,8 +1,8 @@ -package org.sinytra.adapter.analysis; +package org.sinytra.adapter.analysis.method; import org.objectweb.asm.tree.ClassNode; -import org.sinytra.adapter.util.provider.ClassLookup; -import org.sinytra.adapter.env.util.TypeConstants; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.model.constant.TypeConstants; import java.util.*; import java.util.concurrent.ConcurrentHashMap; diff --git a/core/src/main/java/org/sinytra/adapter/analysis/method/MethodAnalyzer.java b/core/src/main/java/org/sinytra/adapter/analysis/method/MethodAnalyzer.java index 727e00be..0c9f0c47 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/method/MethodAnalyzer.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/method/MethodAnalyzer.java @@ -4,30 +4,19 @@ import com.google.common.collect.Multimap; import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; -import org.objectweb.asm.tree.analysis.*; -import org.sinytra.adapter.analysis.selector.FrameUtil; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.util.TypeConstants; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.constant.TypeConstants; import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.OpcodeUtil; -import org.jetbrains.annotations.Nullable; import java.util.*; -import java.util.function.BiConsumer; public class MethodAnalyzer { public static final String LAMBDA_PREFIX = "lambda$"; - private static final Set BOXED_TYPES = Set.of( - "java/lang/Boolean", "java/lang/Byte", "java/lang/Character", "java/lang/Short", - "java/lang/Integer", "java/lang/Float", "java/lang/Long", "java/lang/Double" - ); - private static final Set UNBOXING_METHODS = Set.of( - "booleanValue", "byteValue", "charValue", "shortValue", "intValue", "floatValue", "longValue", "doubleValue" - ); + public static boolean isStatic(MethodNode methodNode) { + return (methodNode.access & Opcodes.ACC_STATIC) != 0; + } public static boolean isLambda(MethodNode methodNode) { return methodNode.name.startsWith(LAMBDA_PREFIX); @@ -45,55 +34,6 @@ public static boolean isDirtyDeprecatedMethod(MethodNode clean, MethodNode dirty return !isDeprecated(clean) && isDeprecated(dirty); } - // Return outer method calls where receiver = this - public static List getSelfTopTierMethodCalls(TargetPair targetPair) { - ClassNode classNode = targetPair.classNode(); - - List topTierCalls = getTopTierMethodCalls(targetPair); - List ownCalls = new ArrayList<>(); - - for (MethodInsnNode insn : topTierCalls) { - MethodNode method = AdapterUtil.findMethod(classNode, insn.name, insn.desc); - if (method != null) { - ownCalls.add(method); - } - } - - return ownCalls; - } - - // Return outer method calls - public static List getTopTierMethodCalls(TargetPair targetPair) { - return getTopTierMethodCalls(targetPair, false); - } - - public static List getTopTierMethodCalls(TargetPair targetPair, boolean skipImplicitCalls) { - MethodNode methodNode = targetPair.methodNode(); - InsnList instructions = methodNode.instructions; - Frame[] frames = FrameUtil.getFrames(methodNode); - - Set consumedCalls = new HashSet<>(); - for (AbstractInsnNode insn : instructions) { - if (insn instanceof MethodInsnNode call) { - traceInputs(call, frames, instructions, consumedCalls, new HashSet<>()); - } - } - - List topTierCalls = new ArrayList<>(); - for (AbstractInsnNode insn : instructions) { - if (insn instanceof MethodInsnNode call && !consumedCalls.contains(call)) { - if (skipImplicitCalls && isImplicitCall(call)) { - unwrapImplicitCall(call, frames, instructions, topTierCalls, new HashSet<>()); - } else if (!topTierCalls.contains(call)) { - topTierCalls.add(call); - } - } - } - topTierCalls.sort(Comparator.comparingInt(instructions::indexOf)); - - return topTierCalls; - } - public record IndyHandle(InvokeDynamicInsnNode indy, int argIndex, Handle handle, MethodNode method) { } @@ -114,16 +54,6 @@ public static List findOwnIndyHandles(ClassNode cls, MethodNode meth return handles; } - public static List findReturnInsns(MethodNode methodNode) { - List insns = new ArrayList<>(); - for (AbstractInsnNode insn : methodNode.instructions) { - if (OpcodeUtil.isReturnOpcode(insn.getOpcode())) { - insns.add(insn); - } - } - return insns; - } - public static Multimap getMethodCalls(MethodNode node) { ImmutableMultimap.Builder calls = ImmutableMultimap.builder(); for (AbstractInsnNode insn : node.instructions) { @@ -146,59 +76,6 @@ public static List findDirtyOnlyCalls(MethodNode clean, MethodNo return dirtyOnly; } - private static boolean isImplicitCall(MethodInsnNode minsn) { - if (!BOXED_TYPES.contains(minsn.owner)) { - return false; - } - if (minsn.getOpcode() == Opcodes.INVOKESTATIC && minsn.name.equals("valueOf")) { - Type[] args = Type.getArgumentTypes(minsn.desc); - return args.length == 1 && args[0].getSort() <= Type.DOUBLE; - } - return minsn.getOpcode() == Opcodes.INVOKEVIRTUAL && UNBOXING_METHODS.contains(minsn.name) && Type.getArgumentCount(minsn.desc) == 0; - } - - private static void unwrapImplicitCall(MethodInsnNode call, Frame[] frames, InsnList instructions, List out, Set seen) { - if (!seen.add(call)) return; - - Set wrapped = new LinkedHashSet<>(); - traceInputs(call, frames, instructions, wrapped, new HashSet<>()); - if (wrapped.isEmpty()) { - if (!out.contains(call)) { - out.add(call); - } - return; - } - for (MethodInsnNode inner : wrapped) { - if (isImplicitCall(inner)) { - unwrapImplicitCall(inner, frames, instructions, out, seen); - } else if (!out.contains(inner)) { - out.add(inner); - } - } - } - - private static void collectProducerCalls(SourceValue value, Frame[] frames, InsnList indices, Set consumedCalls, Set visited) { - for (AbstractInsnNode producer : value.insns) { - if (!visited.add(producer)) continue; - - if (producer instanceof MethodInsnNode producerCall) { - consumedCalls.add(producerCall); - continue; - } - - traceInputs(producer, frames, indices, consumedCalls, visited); - } - } - - private static void traceInputs(AbstractInsnNode insn, Frame[] frames, InsnList indices, Set consumedCalls, Set visited) { - Frame frame = frames[indices.indexOf(insn)]; - if (frame == null) return; - - for (SourceValue input : FrameUtil.getOperands(frame, insn)) { - collectProducerCalls(input, frames, indices, consumedCalls, visited); - } - } - public static List findCalls(MethodNode methodNode, MethodQualifier qualifier) { List calls = new ArrayList<>(); for (AbstractInsnNode insn : methodNode.instructions) { @@ -208,114 +85,4 @@ public static List findCalls(MethodNode methodNode, MethodQualif } return calls; } - - public static void forEachCall(MethodNode methodNode, BiConsumer> action) { - Frame[] frames = FrameUtil.getFrames(methodNode); - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof MethodInsnNode minsn) { - Frame frame = frames[methodNode.instructions.indexOf(insn)]; - if (frame != null) { // Dead code otherwise - action.accept(minsn, FrameUtil.getOperands(frame, minsn)); - } - } - } - } - - public static void forEachReceiver(MethodNode methodNode, BiConsumer> action) { - forEachCall(methodNode, (call, operands) -> { - if (call.getOpcode() != Opcodes.INVOKESTATIC && operands.size() == Type.getArgumentCount(call.desc) + 1) { - action.accept(call, operands.getFirst().insns); - } - }); - } - - @Nullable - public static List getMethodCallInsns(MethodNode methodNode, MethodInsnNode minsn) { - List insns = getMethodCallSrcInsns(methodNode, minsn); - return insns != null && !insns.isEmpty() ? AdapterUtil.subListInsnsExc(insns.getFirst(), minsn) : null; - } - - public static List> getAllMethodCallSrcInsnsInclusive(MethodNode methodNode, MethodQualifier qualifier) { - Frame[] frames = FrameUtil.getStableFrames(methodNode); - List> list = new ArrayList<>(); - for (MethodInsnNode minsn : findCalls(methodNode, qualifier)) { - List insns = getMethodCallSrcInsns(frames, methodNode, minsn); - if (insns != null) { - insns.add(minsn); - list.add(insns); - } - } - return list; - } - - @Nullable - public static List getMethodCallSrcInsns(MethodNode methodNode, MethodInsnNode minsn) { - return getMethodCallSrcInsns(methodNode, minsn, false); - } - - @Nullable - public static List getMethodCallSrcInsns(MethodNode methodNode, MethodInsnNode minsn, boolean stable) { - // Handle cases where the minsn comes from another method - if (!methodNode.instructions.contains(minsn)) { - List minsns = findCalls(methodNode, MethodQualifier.create(minsn)); - return minsns.size() == 1 ? getMethodCallSrcInsns(methodNode, minsns.getFirst(), stable) : null; - } - Frame[] frames = stable ? FrameUtil.getStableFrames(methodNode) : FrameUtil.getFrames(methodNode); - return getMethodCallSrcInsns(frames, methodNode, minsn); - } - - @Nullable - private static List getMethodCallSrcInsns(Frame[] frames, MethodNode methodNode, MethodInsnNode minsn) { - Frame frame = frames[methodNode.instructions.indexOf(minsn)]; - if (frame == null) { - return null; - } - - List insns = new ArrayList<>(); - for (SourceValue src : FrameUtil.getOperands(frame, minsn)) { - AbstractInsnNode srcInsn = AdapterUtil.getSingleInsn(src); - if (srcInsn == null) { - return null; - } - insns.add(srcInsn); - } - return insns; - } - - public static List> getMethodCallArgInsns(MethodNode methodNode, MethodInsnNode minsn) { - List> args = new ArrayList<>(); - if (!methodNode.instructions.contains(minsn)) { - return args; - } - - Frame[] frames = FrameUtil.getFrames(methodNode); - Frame frame = frames[methodNode.instructions.indexOf(minsn)]; - if (frame == null) { - return args; - } - - for (SourceValue value : FrameUtil.getOperands(frame, minsn)) { - List insns = new ArrayList<>(); - for (AbstractInsnNode insn : value.insns) { - expandArgInitInsnsRecursive(insns, methodNode.instructions, frames, insn); - insns.add(insn); - } - args.add(insns); - } - return args; - } - - private static void expandArgInitInsnsRecursive(List out, InsnList insns, Frame[] frames, AbstractInsnNode insn) { - int index = insns.indexOf(insn); - if (index < 0 || index >= frames.length || frames[index] == null) return; - - for (SourceValue value : FrameUtil.getOperands(frames[index], insn)) { - for (AbstractInsnNode sourceInsn : value.insns) { - if (!out.contains(sourceInsn)) { - expandArgInitInsnsRecursive(out, insns, frames, sourceInsn); - out.add(sourceInsn); - } - } - } - } } diff --git a/core/src/main/java/org/sinytra/adapter/analysis/method/MethodInsnMatcher.java b/core/src/main/java/org/sinytra/adapter/analysis/method/MethodInsnMatcher.java index 6dc1df60..4e360450 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/method/MethodInsnMatcher.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/method/MethodInsnMatcher.java @@ -3,8 +3,8 @@ import org.jetbrains.annotations.Nullable; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.LabelNode; -import org.sinytra.adapter.analysis.InstructionMatcher; import org.sinytra.adapter.analysis.InsnTraversal; +import org.sinytra.adapter.analysis.compare.InstructionMatcher; import java.util.List; import java.util.function.UnaryOperator; @@ -12,11 +12,6 @@ public class MethodInsnMatcher { private static final UnaryOperator FORWARD = InsnTraversal.FORWARD; private static final UnaryOperator BACKWARDS = InsnTraversal.BACKWARD; - private static final int INSN_RANGE = 5; - - public static InstructionMatcher findSurroundingInstructions(AbstractInsnNode insn) { - return findSurroundingInstructions(insn, INSN_RANGE); - } public static InstructionMatcher findSurroundingInstructions(AbstractInsnNode insn, int range) { LabelNode previousLabel = findFirstLabelInsn(insn, BACKWARDS); @@ -28,38 +23,6 @@ public static InstructionMatcher findSurroundingInstructions(AbstractInsnNode in return new InstructionMatcher(insn, previousInsns, nextInsns); } - public static InstructionMatcher findBackwardsInstructions(AbstractInsnNode insn) { - return findBackwardsInstructions(insn, INSN_RANGE); - } - - public static InstructionMatcher findBackwardsInstructions(AbstractInsnNode insn, int range) { - LabelNode previousLabel = findFirstLabelInsn(insn, BACKWARDS); - List previousInsns = getInsns(previousLabel, range, BACKWARDS); - - return new InstructionMatcher(insn, previousInsns, List.of()); - } - - public static InstructionMatcher findForwardInstructions(AbstractInsnNode insn) { - return findForwardInstructions(insn, INSN_RANGE); - } - - public static InstructionMatcher findForwardInstructions(AbstractInsnNode insn, int range) { - LabelNode nextLabel = findFirstLabelInsn(insn, FORWARD); - List nextInsns = getInsns(nextLabel, range, FORWARD); - - return new InstructionMatcher(insn, List.of(), nextInsns); - } - - public static InstructionMatcher findForwardInstructionsDirect(AbstractInsnNode insn) { - return findForwardInstructionsDirect(insn, INSN_RANGE); - } - - public static InstructionMatcher findForwardInstructionsDirect(AbstractInsnNode insn, int range) { - List nextInsns = getInsns(insn, range, FORWARD); - - return new InstructionMatcher(insn, List.of(), nextInsns); - } - @Nullable private static LabelNode findFirstLabelInsn(AbstractInsnNode insn, UnaryOperator operator) { return InsnTraversal.walk(insn, operator) diff --git a/core/src/main/java/org/sinytra/adapter/analysis/method/MethodLocator.java b/core/src/main/java/org/sinytra/adapter/analysis/method/MethodLocator.java new file mode 100644 index 00000000..c0663870 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/method/MethodLocator.java @@ -0,0 +1,136 @@ +package org.sinytra.adapter.analysis.method; + +import com.mojang.datafixers.util.Pair; +import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.slf4j.Logger; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +public final class MethodLocator { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final PatchEnvironment environment; + private final ClassLookup lookup; + @Nullable + private final String defaultOwner; + + public MethodLocator(PatchEnvironment environment, ClassLookup lookup, @Nullable String defaultOwner) { + this.environment = environment; + this.lookup = lookup; + this.defaultOwner = defaultOwner; + } + + public ClassLookup lookup() { + return this.lookup; + } + + /// {@return a single method matching the qualifier, searched in the specified class or default owner} + @Nullable + public TargetPair resolve(MethodQualifier qualifier) { + return resolveSingle(qualifier, true); + } + + /// {@return the single method matching the qualifier in the class it specifies} + @Nullable + public TargetPair resolveDeclared(MethodQualifier qualifier) { + return resolveSingle(qualifier, false); + } + + /// {@return the first method matching the qualifier in the class it specifies or its parents} + @Nullable + public TargetPair resolveInherited(MethodQualifier qualifier) { + String owner = qualifier.internalOwnerName(); + if (owner == null || qualifier.name() == null) return null; + + ClassNode node = this.lookup.getClass(owner).orElse(null); + if (node == null) return null; + + Collection parents = this.environment.inheritanceHandler(this.lookup).getClassParents(node.name); + + return Stream.concat(Stream.of(node.name), parents.stream()) + .flatMap(cls -> this.lookup.findMethod(cls, qualifier.name(), qualifier.desc()).stream() + .map(m -> new TargetPair(this.lookup.getClass(cls).orElseThrow(), m))) + .findFirst() + .orElse(null); + } + + /// {@return all methods with the qualifier's name, searched in the specified class or default owner} + public List findByName(MethodQualifier qualifier) { + Pair> pair = findCandidates(qualifier, true, true); + return pair != null ? pair.getSecond() : List.of(); + } + + public List findTargetsByName(MethodQualifier qualifier) { + Pair> pair = findCandidates(qualifier, true, true); + return pair != null ? pair.getSecond().stream().map(method -> new TargetPair(pair.getFirst(), method)).toList() : List.of(); + } + + @Nullable + private TargetPair resolveSingle(MethodQualifier qualifier, boolean useDefaultOwner) { + Pair> pair = findCandidates(qualifier, useDefaultOwner, false); + if (pair == null) { + return null; + } + + if (pair.getSecond().isEmpty()) { + LOGGER.debug("Target method not found: {}{}{}", qualifier.owner(), qualifier.name(), qualifier.desc()); + return null; + } else if (pair.getSecond().size() > 1) { + LOGGER.debug("Multiple candidates found for method: {}{}{}", qualifier.owner(), qualifier.name(), qualifier.desc()); + return null; + } + + return new TargetPair(pair.getFirst(), pair.getSecond().getFirst()); + } + + @Nullable + private Pair> findCandidates(@Nullable MethodQualifier qualifier, boolean useDefaultOwner, boolean ignoreDesc) { + if (qualifier == null || qualifier.name() == null && qualifier.desc() == null) { + return null; + } + + // Determine target class + String owner; + if (qualifier.internalOwnerName() != null) { + owner = qualifier.internalOwnerName(); + } else if (useDefaultOwner && this.defaultOwner != null) { + owner = this.defaultOwner; + } else { + return null; + } + + // Find target class + ClassNode targetClass = this.lookup.getClass(owner).orElse(null); + if (targetClass == null) { + return null; + } + + // Find target method in class + String desc = qualifier.desc(); + List candidates = targetClass.methods.stream() + .filter(mtd -> ignoreDesc ? qualifier.ignoreDesc().matches(null, mtd.name, null) : qualifier.withOwner(null).matches(null, mtd.name, mtd.desc)) + .toList(); + + // If there's multiple candidates, try removing bouncer methods + if (candidates.size() > 1 && desc == null) { + candidates = candidates.stream().filter(mtd -> (mtd.access & Opcodes.ACC_SYNTHETIC) == 0 && (mtd.access & Opcodes.ACC_BRIDGE) == 0).toList(); + } + + if (candidates.isEmpty() && targetClass.superName != null) { + return findCandidates(new MethodQualifier(Type.getObjectType(targetClass.superName).getDescriptor(), qualifier.name(), qualifier.desc()), useDefaultOwner, ignoreDesc); + } + + return Pair.of(targetClass, candidates); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpOriginalCall.java b/core/src/main/java/org/sinytra/adapter/analysis/method/WrapOpOriginalCall.java similarity index 86% rename from core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpOriginalCall.java rename to core/src/main/java/org/sinytra/adapter/analysis/method/WrapOpOriginalCall.java index 29f92c47..2c743df5 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpOriginalCall.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/method/WrapOpOriginalCall.java @@ -1,13 +1,14 @@ -package org.sinytra.adapter.patch.processor.wrapop; +package org.sinytra.adapter.analysis.method; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.util.TypeConstants; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; import org.sinytra.adapter.analysis.InsnTraversal; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.analysis.flow.DataFlow.Producers; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.constant.TypeConstants; +import org.sinytra.adapter.util.AdapterUtil; import java.util.ArrayList; import java.util.Collection; @@ -19,12 +20,12 @@ public record WrapOpOriginalCall(List args) { public static final MethodQualifier QUALIFIER = new MethodQualifier(TypeConstants.OPERATION_TYPE.getDescriptor(), "call", "([Ljava/lang/Object;)Ljava/lang/Object;"); public static List> findAll(MethodNode methodNode) { - return MethodAnalyzer.getAllMethodCallSrcInsnsInclusive(methodNode, QUALIFIER); + return DataFlow.of(methodNode, Producers.COMPUTER).findCallsWithProducers(QUALIFIER); } public static List findFirstCallInsns(MethodNode methodNode) { List calls = MethodAnalyzer.findCalls(methodNode, QUALIFIER); - return calls.isEmpty() ? List.of() : Objects.requireNonNullElseGet(MethodAnalyzer.getMethodCallInsns(methodNode, calls.getFirst()), List::of); + return calls.isEmpty() ? List.of() : Objects.requireNonNullElseGet(DataFlow.of(methodNode).resolveOperandProducers(calls.getFirst()), List::of); } public static List findCallVarInsns(MethodNode methodNode) { @@ -43,7 +44,7 @@ public static WrapOpOriginalCall parse(MethodNode methodNode, List pre, List load, List post) { diff --git a/core/src/main/java/org/sinytra/adapter/analysis/params/EnhancedParamsDiff.java b/core/src/main/java/org/sinytra/adapter/analysis/params/EnhancedParamsDiff.java deleted file mode 100644 index 20f214c3..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/params/EnhancedParamsDiff.java +++ /dev/null @@ -1,512 +0,0 @@ -package org.sinytra.adapter.analysis.params; - -import com.google.common.collect.MapDifference; -import com.google.common.collect.Maps; -import com.mojang.datafixers.util.Pair; -import com.mojang.logging.LogUtils; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.LocalVariableNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.util.GeneratedVariables; -import org.slf4j.Logger; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; - -import java.util.*; - -public class EnhancedParamsDiff { - private static final Logger LOGGER = LogUtils.getLogger(); - private static final boolean DEBUG = Boolean.getBoolean("adapter.core.paramdiff.debug"); - - @Nullable - public static LayeredParamsDiffSnapshot createLayered(List clean, List dirty) { - LayeredParamsDiffSnapshot.Builder builder = LayeredParamsDiffSnapshot.builder(); - try { - buildDiff(builder, clean, dirty); - } catch (UnsupportedParamsDiffException e) { - LOGGER.debug("Cannot compare parameters {} and {}: {}", clean, dirty, e.getMessage()); - return null; - } - // TODO Sanity check - return builder.build(); - } - - @Nullable - public static LayeredParamsDiffSnapshot compareMethodParameters(MethodNode clean, MethodNode dirty) { - if (clean.localVariables == null || dirty.localVariables == null) { - return LayeredParamsDiffSnapshot.EMPTY; - } - - List cleanParams = getParameterLocals(clean); - List dirtyParams = getParameterLocals(dirty); - boolean compareNames = cleanParams.stream().allMatch(t -> t.name() != null) && dirtyParams.stream().anyMatch(TypeWithContext::isGenerated); - - LayeredParamsDiffSnapshot.Builder builder = LayeredParamsDiffSnapshot.builder(); - try { - buildDiffWithContext(builder, cleanParams, dirtyParams, compareNames); - } catch (UnsupportedParamsDiffException e) { - LOGGER.debug("Cannot compare parameters of {}{} and {}{}: {}", clean.name, clean.desc, dirty.name, dirty.desc, e.getMessage()); - return null; - } - return builder.build(); - } - - private static List getParameterLocals(MethodNode method) { - LocalVariableLookup lookup = new LocalVariableLookup(method); - List params = new ArrayList<>(); - int paramCount = Type.getArgumentCount(method.desc); - for (int i = 0; i < paramCount; i++) { - LocalVariableNode lvn = lookup.getByParameterOrdinalOrNull(i); - if (lvn == null) { - break; - } - params.add(TypeWithContext.of(lvn, i)); - } - return params; - } - - private static void buildDiff(LayeredParamsDiffSnapshot.Builder builder, List clean, List dirty) { - buildDiffWithContext(builder, createPositionedList(clean), createPositionedList(dirty), false); - } - - private static void buildDiffWithContext(LayeredParamsDiffSnapshot.Builder builder, List cleanQueue, List dirtyQueue, boolean compareNames) { - int dirtySize = dirtyQueue.size(); - boolean sameSize = cleanQueue.size() == dirtyQueue.size(); - - if (DEBUG) { - LOGGER.info("Comparing types:\n{}", printTable(cleanQueue, dirtyQueue)); - } - - while (!cleanQueue.isEmpty()) { - // Look ahead for matching types at the beginning of the list - // If the first two are equal, remove the first ones and repeat - if (predictParameterMatch(builder, cleanQueue, dirtyQueue, compareNames, sameSize)) { - cleanQueue.removeFirst(); - dirtyQueue.removeFirst(); - continue; - } - // Handle replaced types, needs improving - if (tryReplacingParams(builder, cleanQueue, dirtyQueue)) { - continue; - } - // Check for swapped / reordered types. Also handles unique-type insertions - SwapResult swapResult = checkForSwaps(builder, cleanQueue, dirtyQueue); - if (swapResult != null) { - dirtyQueue.removeAll(swapResult.removeDirty()); - cleanQueue.clear(); - dirtyQueue.clear(); - break; - } - // Find the smallest set of matching types by locating the position of the next clean type in the dirty list - int dirtyTypeIndex = cleanQueue.size() == 1 ? dirtySize - 1 : lookAhead(dirtyQueue, cleanQueue.get(1)); - if (dirtyTypeIndex != -1) { - boolean matchesName = !compareNames || cleanQueue.size() < 2 || dirtyQueue.size() < 2 || cleanQueue.get(1).matches(dirtyQueue.get(1)); - int offset = cleanQueue.get(0).sameType(dirtyQueue.get(0)) && matchesName ? 1 : 2; - List compareClean = extract(cleanQueue, matchesName ? 2 : 1); - List compareDirty = extract(dirtyQueue, dirtyTypeIndex + offset); - - compare(builder, compareClean, compareDirty); - } - // If there is no match for the first available type, look further ahead. This is good for handling cases - // where the sizes of both sets are equal and the first couple types have been replaced / modified - else if (cleanQueue.size() == dirtyQueue.size() && (dirtyTypeIndex = findClosestMatch(cleanQueue, dirtyQueue)) > 1) { - List compareClean = extract(cleanQueue, dirtyTypeIndex); - List compareDirty = extract(dirtyQueue, dirtyTypeIndex); - - compare(builder, compareClean, compareDirty); - } else if (cleanQueue.size() > 1) { - TypeWithContext type = cleanQueue.remove(1); - if (DEBUG) { - LOGGER.info("Removing type {}", type); - } - builder.remove(type.pos()); - } else { - throw new UnsupportedParamsDiffException("undefined behavior"); - } - } - if (!dirtyQueue.isEmpty()) { - for (TypeWithContext type : dirtyQueue) { - builder.insert(type.pos(), type.type()); - } - } - } - - private static boolean predictParameterMatch(LayeredParamsDiffSnapshot.Builder builder, List cleanQueue, List dirtyQueue, boolean compareNames, boolean sameSize) { - // For same-sized comparisons, we can be certain there have been no insertions - if (sameSize && !cleanQueue.isEmpty() && !dirtyQueue.isEmpty()) { - if (!cleanQueue.get(0).sameType(dirtyQueue.get(0))) { - if (checkMovedParam(builder, cleanQueue, dirtyQueue, 0)) { - return true; - } - // If the first parameters differ, it's possible one of them has been removed - // == Clean == - // 0 Foo - // 1 Bar - // 2 World - // == Dirty == - // 0 Bar - // 1 World - // 2 Sheep - if (cleanQueue.size() > 2 && dirtyQueue.size() > 2 && cleanQueue.get(1).matches(dirtyQueue.get(0)) && cleanQueue.get(2).matches(dirtyQueue.get(1))) { - builder.remove(cleanQueue.getFirst().pos()); - cleanQueue.removeFirst(); - return true; - } - return false; - } - return true; - } else if (cleanQueue.size() > 1 && dirtyQueue.size() > 1) { - if (cleanQueue.get(0).sameType(dirtyQueue.get(0))) { - if (cleanQueue.get(1).matches(dirtyQueue.get(1))) { - // If the first two parameters match, we can be sure the first ones are identical without any insertions - return true; - } - // Comparing names can be useful in cases where a parameter is inserted in between multiple ones of the same type - else if (compareNames) { - // Check for inserted parameters - // == Clean == - // 0 F one - // 1 F two - // 2 F three - // == Dirty == - // 0 F one - // 1 F - // 2 F two - if (dirtyQueue.size() > 2 && cleanQueue.get(1).matches(dirtyQueue.get(2))) { - builder.insert(dirtyQueue.get(1).pos(), dirtyQueue.get(1).type()); - cleanQueue.removeFirst(); - dirtyQueue.removeFirst(); - dirtyQueue.removeFirst(); - return true; - } - } else { - return checkMovedParam(builder, cleanQueue, dirtyQueue, 1); - } - } - return false; - } - return false; - } - - private static boolean checkMovedParam(LayeredParamsDiffSnapshot.Builder builder, List cleanQueue, List dirtyQueue, int index) { - TypeWithContext param = cleanQueue.get(index); - TypeWithContext dirtyParam = dirtyQueue.get(index); - Map cleanGroup = groupTypes(cleanQueue); - Map dirtyGroup = groupTypes(dirtyQueue); - if (cleanGroup.get(param.type()) == 1 && dirtyGroup.getOrDefault(param.type(), 0) == 1 - // Make sure the next param isn't injected - && Objects.equals(cleanGroup.getOrDefault(dirtyParam.type(), 0), dirtyGroup.get(dirtyParam.type())) - ) { - // The parameter has been moved - TypeWithContext newParam = dirtyQueue.stream().filter(t -> t.type().equals(param.type())).findFirst().orElseThrow(); - if (Math.abs(param.pos() - newParam.pos()) == 1) { - builder.swap(param.pos(), newParam.pos()); - } else { - builder.move(param.pos(), newParam.pos()); - } - cleanQueue.remove(param); - dirtyQueue.remove(newParam); - return true; - } - return false; - } - - private static int findClosestMatch(List cleanQueue, List dirtyQueue) { - for (TypeWithContext typeWithContext : cleanQueue) { - int pos = lookAhead(dirtyQueue, typeWithContext); - if (pos != -1) { - return pos; - } - } - return -1; - } - - private static boolean tryReplacingParams(LayeredParamsDiffSnapshot.Builder builder, List cleanQueue, List dirtyQueue) { - if (replaceType(builder, 0, cleanQueue, dirtyQueue)) { - return true; - } else if (cleanQueue.size() == 2 && dirtyQueue.size() == 2) { - // Handle swapped parameters in simple cases - if (cleanQueue.get(0).sameType(dirtyQueue.get(1)) && cleanQueue.get(1).sameType(dirtyQueue.get(0))) { - builder.swap(dirtyQueue.get(0).pos(), dirtyQueue.get(1).pos()); - cleanQueue.clear(); - dirtyQueue.clear(); - return true; - } - return replaceType(builder, 1, cleanQueue, dirtyQueue); - } - return false; - } - - private static boolean replaceType(LayeredParamsDiffSnapshot.Builder builder, int index, List cleanQueue, List dirtyQueue) { - if (index >= cleanQueue.size() || index >= dirtyQueue.size()) { - return false; - } - TypeWithContext cleanType = cleanQueue.get(index); - TypeWithContext dirtyType = dirtyQueue.get(index); - int next = index + 1; - // Handle possible insertions - if (cleanQueue.size() != dirtyQueue.size() && dirtyQueue.size() > next && cleanType.sameType(dirtyQueue.get(next)) || isPossiblyInjected(next, cleanQueue, dirtyQueue)) { - return false; - } - if (!cleanType.sameType(dirtyType)) { - if (DEBUG) { - LOGGER.info("Replacing {} with {}", cleanType, dirtyType); - } - cleanQueue.remove(index); - dirtyQueue.remove(index); - builder.replace(dirtyType.pos(), dirtyType.type()); - return true; - } - return false; - } - - private static boolean isPossiblyInjected(int index, List cleanQueue, List dirtyQueue) { - for (int i = index; i < cleanQueue.size() && i < dirtyQueue.size(); i++) { - if (!cleanQueue.get(i).sameType(dirtyQueue.get(i))) { - return true; - } - } - return false; - } - - private record SwapResult(List removeDirty) { - } - - private static Comparator maybeInverse(Comparator comparator, boolean inverse) { - return inverse ? comparator.reversed() : comparator; - } - - private static List> sortMapDiff(Map map, List context, boolean descending) { - return map.entrySet().stream() - .map(e -> { - Type type = e.getKey(); - Integer count = e.getValue(); - if (count == 1) { - TypeWithContext inserted = context.stream().filter(t -> t.type().equals(type)).findFirst().orElseThrow(); - return Pair.of(type, inserted); - } - return null; - }) - .filter(Objects::nonNull) - .sorted(maybeInverse(Comparator.comparingInt(p -> p.getSecond().pos()), descending)) - .toList(); - } - - @Nullable - private static SwapResult checkForSwaps(LayeredParamsDiffSnapshot.Builder builder, List clean, List dirty) { - Map cleanGroup = groupTypes(clean); - Map dirtyGroup = groupTypes(dirty); - MapDifference diff = Maps.difference(cleanGroup, dirtyGroup); - - List rearrangeClean = new ArrayList<>(clean); - List rearrangeDirty = new ArrayList<>(dirty); - List removeDirty = new ArrayList<>(); - LayeredParamsDiffSnapshot.Builder tempDiff = LayeredParamsDiffSnapshot.builder(); - // Remove inserted parameters - if (diff.entriesOnlyOnLeft().isEmpty() && !diff.entriesOnlyOnRight().isEmpty()) { - // Sort by insertion order first - for (Pair p : sortMapDiff(diff.entriesOnlyOnRight(), dirty, false)) { - Type type = p.getFirst(); - TypeWithContext inserted = p.getSecond(); - - dirtyGroup.remove(type); - rearrangeDirty.remove(inserted); - removeDirty.add(inserted); - int offset = inserted.pos() + (int) builder.getRemovals().stream().filter(i -> i < inserted.pos()).count(); - tempDiff.insert(offset, inserted.type()); - } - } else if (!diff.entriesOnlyOnLeft().isEmpty() && diff.entriesOnlyOnRight().isEmpty()) { - // Sort by insertion order first - for (Pair p : sortMapDiff(diff.entriesOnlyOnLeft(), clean, true)) { - Type type = p.getFirst(); - TypeWithContext inserted = p.getSecond(); - - int offset = inserted.pos() - (int) builder.getRemovals().stream().filter(i -> i < inserted.pos()).count(); - tempDiff.remove(offset); - cleanGroup.remove(type); - rearrangeClean.remove(inserted); - } - } - - if (!sameTypeCount(cleanGroup, dirtyGroup)) { - return null; - } - if (DEBUG) { - LOGGER.info("Checking for swaps in parameters:\n{}", printTable(rearrangeClean, rearrangeDirty)); - } - - // Remove leading equal types - while (!rearrangeClean.isEmpty() && rearrangeClean.getFirst().sameType(rearrangeDirty.getFirst())) { - rearrangeClean.removeFirst(); - rearrangeDirty.removeFirst(); - } - // Remove trailing equal types - while (!rearrangeClean.isEmpty() && rearrangeClean.getLast().sameType(rearrangeDirty.getLast())) { - rearrangeClean.removeLast(); - rearrangeDirty.removeLast(); - } - if (!rearrangeClean.isEmpty() && !rearrangeDirty.isEmpty()) { - builder.merge(tempDiff.build()); - // Run rearrangement - rearrange(builder, rearrangeClean, rearrangeDirty); - return new SwapResult(removeDirty); - } else if (rearrangeClean.isEmpty() && rearrangeDirty.isEmpty()) { - builder.merge(tempDiff.build()); - return new SwapResult(removeDirty); - } - return null; - } - - private static void rearrange(LayeredParamsDiffSnapshot.Builder builder, List clean, List dirty) { - record Rearrangement(TypeWithContext cleanType, TypeWithContext dirtyType, int fromRelative, int toRelative) { - } - - Map cleanGroup = groupTypes(clean); - Map dirtyGroup = groupTypes(dirty); - if (!sameTypeCount(cleanGroup, dirtyGroup)) { - return; - } - if (DEBUG) { - LOGGER.info("Rearranging parameters:\n{}", printTable(clean, dirty)); - } - - List rearrangements = new ArrayList<>(); - for (TypeWithContext cleanType : clean) { - if (cleanGroup.get(cleanType.type()) == 1 && dirtyGroup.get(cleanType.type()) == 1) { - // Determine new type pos - TypeWithContext dirtyType = dirty.stream().filter(cleanType::sameType).findFirst().orElseThrow(); - rearrangements.add(new Rearrangement(cleanType, dirtyType, clean.indexOf(cleanType), dirty.indexOf(dirtyType))); - } - } - rearrangements.sort(Comparator.comparingInt(Rearrangement::toRelative)); - - List rearrangeClean = new ArrayList<>(clean); - List rearrangeDirty = new ArrayList<>(dirty); - for (Rearrangement rearrangement : rearrangements) { - boolean same = true; - for (int i = 0; i < rearrangeClean.size(); i++) { - if (!rearrangeClean.get(i).sameType(rearrangeDirty.get(i))) { - same = false; - } - } - if (same) { - break; - } - if (DEBUG) { - LOGGER.info("Moving param {} to index {}", rearrangement.cleanType(), rearrangement.dirtyType().pos()); - } - // The original pos in the dirty list might differ due to prior insertions - int offsetOriginalPos = rearrangeDirty.get(rearrangement.fromRelative()).pos(); - int destPos = rearrangement.dirtyType().pos(); - if (Math.abs(offsetOriginalPos - destPos) == 1) { - builder.swap(offsetOriginalPos, destPos); - } else { - builder.move(offsetOriginalPos, destPos); - } - rearrangeClean.add(rearrangement.toRelative(), rearrangeClean.remove(rearrangement.fromRelative())); - } - } - - private static void compare(LayeredParamsDiffSnapshot.Builder builder, List clean, List dirty) { - if (DEBUG) { - LOGGER.info("Running comparison for:\n{}", printTable(clean, dirty)); - } - - List cleanTypes = clean.stream().map(TypeWithContext::toMethodParameter).toList(); - List dirtyTypes = dirty.stream().map(TypeWithContext::toMethodParameter).toList(); - ParametersDiff diff = ParametersDiff.compareParameters(cleanTypes, dirtyTypes, false); - if (DEBUG) { - LOGGER.info("Comparison results:\n\tInserted: {}\n\tReplaced: {}\n\tSwapped: {}\n\tRemoved: {}", diff.insertions(), diff.replacements(), diff.swaps(), diff.removals()); - } - int indexOffset = !dirty.isEmpty() ? dirty.getFirst().pos() : 0; - builder.merge(diff.toSnapshot(), indexOffset); - } - - private static List createPositionedList(List list) { - List ret = new ArrayList<>(); - for (int i = 0; i < list.size(); i++) { - ret.add(new TypeWithContext(list.get(i), i)); - } - return ret; - } - - private static boolean sameTypeCount(Map cleanGroup, Map dirtyGroup) { - if (cleanGroup.size() != dirtyGroup.size()) { - return false; - } - for (Map.Entry entry : cleanGroup.entrySet()) { - Integer dirtyCount = dirtyGroup.get(entry.getKey()); - if (dirtyCount == null || dirtyCount.intValue() != entry.getValue()) { - return false; - } - } - return true; - } - - private static List extract(List list, int amount) { - List res = new ArrayList<>(); - for (int i = 0; i < amount && !list.isEmpty(); i++) { - res.add(list.removeFirst()); - } - return res; - } - - private static int lookAhead(List types, TypeWithContext target) { - for (int i = 0; i < types.size(); i++) { - if (target.sameType(types.get(i))) { - return i; - } - } - return -1; - } - - private static Map groupTypes(List list) { - Map map = new HashMap<>(); - for (TypeWithContext type : list) { - map.compute(type.type(), (t, old) -> old == null ? 1 : old + 1); - } - return map; - } - - private static String printTable(List clean, List dirty) { - StringBuilder builder = new StringBuilder(); - builder.append("\t| %5s | %25s%s%26s | %25s%s%26s |\n".formatted("Index", "", "Clean", "", "", "Dirty", "")); - builder.append("\t"); - builder.repeat("=", 127); - builder.append("\n"); - int max = Math.max(clean.size(), dirty.size()); - for (int i = 0; i < max; i++) { - builder.append("\t| %-5s | %56s | %-56s |\n".formatted(i, i < clean.size() ? clean.get(i).type() : "", i < dirty.size() ? dirty.get(i).type() : "")); - } - builder.append("\t"); - builder.repeat("=", 127); - return builder.toString(); - } - - private record TypeWithContext(@Nullable String name, Type type, int pos, boolean isGenerated) { - public TypeWithContext(Type type, int pos) { - this(null, type, pos, false); - } - - public static TypeWithContext of(LocalVariableNode lvn, int pos) { - Type type = Type.getType(lvn.desc); - return new TypeWithContext(lvn.name, type, pos, lvn.name != null && GeneratedVariables.isGeneratedVariableName(lvn.name, type)); - } - - public ParametersDiff.MethodParameter toMethodParameter() { - return new ParametersDiff.MethodParameter(this.type, this.isGenerated); - } - - public boolean sameType(TypeWithContext other) { - return type().equals(other.type()); - } - - public boolean sameName(TypeWithContext other) { - return this.name == null || other.name() == null || this.isGenerated == other.isGenerated(); - } - - public boolean matches(TypeWithContext other) { - return sameType(other) && sameName(other); - } - } -} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/params/LayeredParamsDiffSnapshot.java b/core/src/main/java/org/sinytra/adapter/analysis/params/LayeredParamsDiffSnapshot.java index 20adfa61..08c91412 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/params/LayeredParamsDiffSnapshot.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/params/LayeredParamsDiffSnapshot.java @@ -2,12 +2,16 @@ import com.google.errorprone.annotations.CheckReturnValue; import org.objectweb.asm.Type; -import org.objectweb.asm.commons.InstructionAdapter; -import org.sinytra.adapter.transform.param.*; +import org.sinytra.adapter.edit.param.InjectParameterTransform; +import org.sinytra.adapter.edit.param.MoveParametersTransformer; +import org.sinytra.adapter.edit.param.ParameterTransformer; +import org.sinytra.adapter.edit.param.RemoveParameterTransformer; +import org.sinytra.adapter.edit.param.ReplaceParametersTransformer; +import org.sinytra.adapter.edit.param.SwapParametersTransformer; +import org.sinytra.adapter.edit.param.TransformParameters; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; public record LayeredParamsDiffSnapshot(List modifications) { public static final LayeredParamsDiffSnapshot EMPTY = new LayeredParamsDiffSnapshot(List.of()); @@ -73,10 +77,6 @@ public Builder swap(int from, int to) { return add(new SwapParametersTransformer(from, to)); } - public Builder substitute(int target, int substitute) { - return add(new SubstituteParameterTransformer(target, substitute)); - } - public Builder move(int from, int to) { return add(new MoveParametersTransformer(from, to)); } @@ -86,10 +86,6 @@ public Builder remove(int index) { return add(new RemoveParameterTransformer(index, false)); } - public Builder inline(int target, Consumer adapter) { - return add(new InlineParameterTransformer(target, adapter)); - } - public Builder merge(LayeredParamsDiffSnapshot diff) { return merge(diff, 0); } @@ -99,13 +95,6 @@ public Builder merge(LayeredParamsDiffSnapshot diff, int indexOffset) { return this; } - public List getRemovals() { - return this.modifications.stream() - .filter(RemoveParameterTransformer.class::isInstance) - .map(m -> ((RemoveParameterTransformer) m).index()) - .toList(); - } - private Builder add(ParameterTransformer modification) { this.modifications.add(modification); return this; diff --git a/core/src/main/java/org/sinytra/adapter/analysis/params/Param.java b/core/src/main/java/org/sinytra/adapter/analysis/params/Param.java new file mode 100644 index 00000000..7292ebf0 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/params/Param.java @@ -0,0 +1,12 @@ +package org.sinytra.adapter.analysis.params; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; + +import java.util.List; + +public record Param(Type type, @Nullable String name) { + public static List of(List types) { + return types.stream().map(type -> new Param(type, null)).toList(); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/param/ParamDiffResolver.java b/core/src/main/java/org/sinytra/adapter/analysis/params/ParamDiffResolver.java similarity index 84% rename from core/src/main/java/org/sinytra/adapter/env/param/ParamDiffResolver.java rename to core/src/main/java/org/sinytra/adapter/analysis/params/ParamDiffResolver.java index 9af81e35..43541445 100644 --- a/core/src/main/java/org/sinytra/adapter/env/param/ParamDiffResolver.java +++ b/core/src/main/java/org/sinytra/adapter/analysis/params/ParamDiffResolver.java @@ -1,10 +1,9 @@ -package org.sinytra.adapter.env.param; +package org.sinytra.adapter.analysis.params; import it.unimi.dsi.fastutil.objects.ReferenceArrayList; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; -import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.transform.param.*; +import org.sinytra.adapter.edit.param.*; import java.util.ArrayList; import java.util.List; @@ -23,8 +22,9 @@ public PositionedParam(int cleanPos, Type type) { } } - public record ParamState(Type type, int cleanIndex, int dirtyIndex) {} - + public record ParamState(Type type, int cleanIndex, int dirtyIndex) { + } + public static class ParamEvalResult { private final List original; private final Map params; @@ -43,7 +43,23 @@ public ParamState getUpdated(Type param) { } } + public static List applyTypes(List input, LayeredParamsDiffSnapshot diff) { + return simulate(input, diff).stream().map(param -> param.type).toList(); + } + public static ParamEvalResult resolve(List input, LayeredParamsDiffSnapshot diff) { + List state = simulate(input, diff); + + List result = new ArrayList<>(); + for (int i = 0; i < state.size(); i++) { + PositionedParam param = state.get(i); + result.add(new ParamState(param.type, param.cleanPos, i)); + } + + return new ParamEvalResult(input, result); + } + + private static List simulate(List input, LayeredParamsDiffSnapshot diff) { List state = new ArrayList<>(); for (int i = 0; i < input.size(); i++) { Type type = input.get(i); @@ -64,18 +80,9 @@ case SwapParametersTransformer(int from, int to) -> { } case MoveParametersTransformer(int from, int to) -> state.add(to, state.remove(from)); case RemoveParameterTransformer remove -> state.remove(remove.index()); - case InlineParameterTransformer inline -> state.remove(inline.target()); - case SubstituteParameterTransformer substitute -> state.remove(substitute.target()); case null, default -> throw new RuntimeException("Unknown parameter modification: " + modification); } } - - List result = new ArrayList<>(state.size()); - for (int i = 0; i < state.size(); i++) { - PositionedParam param = state.get(i); - result.add(new ParamState(param.type, param.cleanPos, i)); - } - - return new ParamEvalResult(input, result); + return state; } } diff --git a/core/src/main/java/org/sinytra/adapter/analysis/params/ParametersDiff.java b/core/src/main/java/org/sinytra/adapter/analysis/params/ParametersDiff.java deleted file mode 100644 index 5deb8de8..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/params/ParametersDiff.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.sinytra.adapter.analysis.params; - -import com.mojang.datafixers.util.Pair; -import org.objectweb.asm.Type; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -/** - * @deprecated Use {@link EnhancedParamsDiff} where possible - */ -@Deprecated -public record ParametersDiff( - int originalCount, - List> insertions, - List> replacements, - List> swaps, - List removals, - List> moves -) { - public record MethodParameter(Type type, boolean isGeneratedType) { - public boolean matchName(MethodParameter other) { - return this.isGeneratedType == other.isGeneratedType; - } - } - - public LayeredParamsDiffSnapshot toSnapshot() { - LayeredParamsDiffSnapshot.Builder builder = LayeredParamsDiffSnapshot.builder(); - this.insertions.forEach(p -> builder.insert(p.getFirst(), p.getSecond())); - this.replacements.forEach(p -> builder.replace(p.getFirst(), p.getSecond())); - this.swaps.forEach(p -> builder.swap(p.getFirst(), p.getSecond())); - this.removals.forEach(builder::remove); - this.moves.forEach(p -> builder.move(p.getFirst(), p.getSecond())); - return builder.build(); - } - - public static ParametersDiff compareTypeParameters(Type[] parameterTypes, Type[] newParameterTypes) { - List cleanParameters = Stream.of(parameterTypes) - .map(type -> new MethodParameter(type, false)) - .toList(); - List dirtyParameters = Stream.of(newParameterTypes) - .map(type -> new MethodParameter(type, false)) - .toList(); - return compareParameters(cleanParameters, dirtyParameters, false); - } - - public static ParametersDiff compareParameters(List cleanParameters, List dirtyParameters, boolean lvtIndexes) { - // Indexes we insert new params at - List> insertions = new ArrayList<>(); - // Indexes we replace params at - List> replacements = new ArrayList<>(); - // Indexes to swap one for another - List> swaps = new ArrayList<>(); - List removals = new ArrayList<>(); - int i = 0; - int j = 0; - int lvtIndex = 0; - // New params are expected to be at least the same size as the old ones, so we use them for the outer loop - outer: - while (j < dirtyParameters.size()) { - boolean skipJIncr = false; - // Start iterating over the original params - if (i < cleanParameters.size()) { - MethodParameter cleanParam = cleanParameters.get(i); - MethodParameter dirtyParam = dirtyParameters.get(j); - // Check if old and new params at this index are the same - boolean sameType = cleanParam.type.equals(dirtyParam.type); - if (!sameType || !cleanParam.matchName(dirtyParam)) { - boolean handled = false; - boolean removing = false; - // Check if the params have been swapped - if (i + 1 < cleanParameters.size() && j + 1 < dirtyParameters.size()) { - MethodParameter nextCleanParam = cleanParameters.get(i + 1); - MethodParameter nextDirtyParam = dirtyParameters.get(j + 1); - // Detect swapped params - if (nextCleanParam.type.equals(dirtyParam.type) && nextDirtyParam.equals(cleanParam)) { - swaps.add(Pair.of(j, j + 1)); - i++; - lvtIndex++; - j++; - handled = true; - } - // Detect removed parameters, check the next 2 params for matching types (if possible) - if (nextCleanParam.equals(dirtyParam) && (i + 2 >= cleanParameters.size() || cleanParameters.get(i + 2).equals(nextDirtyParam))) { - removing = true; - } - } - if (!handled) { - // If not, it is possible a new param was injected onto this index. - // In that case, the original param was moved further down the array, and we must find it. - for (int k = j + 1; k < dirtyParameters.size(); k++) { - MethodParameter dirtyParamAhead = dirtyParameters.get(k); - if (cleanParam.type.equals(dirtyParamAhead.type) && (sameType || cleanParam.matchName(dirtyParamAhead))) { - // If the param is found, add all parameters between the original and new pos to the insertion list - for (; j < k; j++, lvtIndex++) { - insertions.add(Pair.of(lvtIndex, dirtyParameters.get(j).type)); - } - // Continue onto the next params - continue outer; - } - } - if (removing) { - removals.add(j); - skipJIncr = true; - } - // If the param is not found, then it was likely replaced - else if (!cleanParam.type.equals(dirtyParam.type)) { - replacements.add(Pair.of(lvtIndex, dirtyParam.type)); - } - } - } - i++; - lvtIndex += lvtIndexes ? dirtyParam.type.getSize() : 1; - } - // For appending parameters at the end of the list - else { - Type type = dirtyParameters.get(j).type; - insertions.add(Pair.of(lvtIndex, type)); - lvtIndex += lvtIndexes ? type.getSize() : 1; - } - if (!skipJIncr) { - j++; - } - } - if (j - i + removals.size() != insertions.size()) { - throw new UnsupportedParamsDiffException("Unexpected difference in params size"); - } - return new ParametersDiff(i, insertions, replacements, swaps, removals, List.of()); - } - - public boolean isEmpty() { - return this.insertions.isEmpty() && this.replacements.isEmpty() && this.swaps.isEmpty() && this.removals.isEmpty() && this.moves.isEmpty(); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/params/ParamsEditDistance.java b/core/src/main/java/org/sinytra/adapter/analysis/params/ParamsEditDistance.java new file mode 100644 index 00000000..7d125c57 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/params/ParamsEditDistance.java @@ -0,0 +1,275 @@ +package org.sinytra.adapter.analysis.params; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.util.GeneratedVariables; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Diffs two parameter lists by weighted edit distance with swaps (Damerau-Levenshtein distance) + */ +public final class ParamsEditDistance { + private static final int INSERT = 100; + private static final int DELETE = 100; + private static final int SWAP = 100; + private static final int CONVERTIBLE = 50; + private static final int UNRELATED = 100; + private static final int OTHER_NAME = 1; + + private enum Operation { + MATCH, + DELETE, + INSERT, + SWAP + } + + private record Step(Operation op, int clean, int dirty, int swapClean, int swapDirty) { + public Step(Operation op, int clean, int dirty) { + this(op, clean, dirty, -1, -1); + } + } + + private record Table(int[][] costs, int[][][] swapFrom) { + } + + /// Represents a temp working parameter + /// @param target target dirty index or -1 if it's removed + private record Slot(Type type, int target) { + } + + public static LayeredParamsDiffSnapshot diff(List clean, List dirty, TypeRelation relation) { + Table table = computeCosts(clean, dirty, relation); + return toSnapshot(clean, dirty, backtrack(clean, dirty, relation, table)); + } + + public static LayeredParamsDiffSnapshot diffTypes(List clean, List dirty) { + return diffTypes(clean, dirty, TypeRelation.STATIC); + } + + public static LayeredParamsDiffSnapshot diffTypes(List clean, List dirty, TypeRelation relation) { + return diff(Param.of(clean), Param.of(dirty), relation); + } + + public static LayeredParamsDiffSnapshot diffParameters(MethodNode clean, MethodNode dirty, TypeRelation relation) { + return diff(getParameters(clean), getParameters(dirty), relation); + } + + private static List getParameters(MethodNode method) { + LocalVariableLookup lookup = LocalVariableLookup.of(method); + Type[] types = Type.getArgumentTypes(method.desc); + List params = new ArrayList<>(types.length); + + for (int i = 0; i < types.length; i++) { + LocalVariableNode local = lookup != null ? lookup.getByParameterOrdinalOrNull(i) : null; + boolean stable = local != null && !GeneratedVariables.isPositionalName(local.name, types[i]); + params.add(new Param(types[i], stable ? local.name : null)); + } + + return params; + } + + private static Table computeCosts(List clean, List dirty, TypeRelation relation) { + int cleanSize = clean.size(); + int dirtySize = dirty.size(); + + int[][] costs = new int[cleanSize + 1][dirtySize + 1]; + int[][][] swapFrom = new int[cleanSize + 1][dirtySize + 1][]; + + for (int i = 0; i <= cleanSize; i++) { + for (int j = 0; j <= dirtySize; j++) { + if (i == 0 || j == 0) { + costs[i][j] = i * DELETE + j * INSERT; + continue; + } + + int cost = Math.min(costs[i - 1][j] + DELETE, costs[i][j - 1] + INSERT); + cost = Math.min(cost, costs[i - 1][j - 1] + getMatchCost(clean.get(i - 1), dirty.get(j - 1), relation)); + + for (int k = 1; k < i; k++) { + // Swapping the same types changes nothing + if (relation.relate(clean.get(k - 1).type(), clean.get(i - 1).type()) == TypeRelation.Kind.SAME) { + continue; + } + + for (int l = 1; l < j; l++) { + int swap = getSwapCost(clean, dirty, relation, costs, i, j, k, l); + if (swap < cost) { + cost = swap; + swapFrom[i][j] = new int[]{k, l}; + } + } + } + + costs[i][j] = cost; + } + } + return new Table(costs, swapFrom); + } + + private static int getSwapCost(List clean, List dirty, TypeRelation relation, int[][] costs, + int cleanLate, int dirtyLate, int cleanEarly, int dirtyEarly + ) { + // Turn the parameters before the swapped two into each other + int before = costs[cleanEarly - 1][dirtyEarly - 1]; + // Delete the clean parameters between swapped params and insert the dirty ones + int between = (cleanLate - cleanEarly - 1) * DELETE + (dirtyLate - dirtyEarly - 1) * INSERT; + // The earlier clean parameter takes the later dirty position and the later one the earlier position + int exchanged = getMatchCost(clean.get(cleanEarly - 1), dirty.get(dirtyLate - 1), relation) + + getMatchCost(clean.get(cleanLate - 1), dirty.get(dirtyEarly - 1), relation); + return before + between + exchanged + SWAP; + } + + private static List backtrack(List clean, List dirty, TypeRelation relation, Table table) { + int[][] costs = table.costs(); + List steps = new ArrayList<>(); + int i = clean.size(); + int j = dirty.size(); + + while (i > 0 || j > 0) { + int cost = costs[i][j]; + int[] swap = i > 0 && j > 0 ? table.swapFrom()[i][j] : null; + + if (j > 0 && cost == costs[i][j - 1] + INSERT) { + steps.add(new Step(Operation.INSERT, -1, --j)); + } else if (i > 0 && cost == costs[i - 1][j] + DELETE) { + steps.add(new Step(Operation.DELETE, --i, -1)); + } else if (swap != null && cost == getSwapCost(clean, dirty, relation, costs, i, j, swap[0], swap[1])) { + int k = swap[0]; + int l = swap[1]; + + // clean k - 1 goes to dirty j - 1 + // clean i - 1 to the dirty l - 1 + // the rest between them is replaced + for (int insert = j - 2; insert >= l; insert--) { + steps.add(new Step(Operation.INSERT, -1, insert)); + } + for (int delete = i - 2; delete >= k; delete--) { + steps.add(new Step(Operation.DELETE, delete, -1)); + } + + steps.add(new Step(Operation.SWAP, k - 1, j - 1, i - 1, l - 1)); + i = k - 1; + j = l - 1; + } else { + steps.add(new Step(Operation.MATCH, --i, --j)); + } + } + + return steps.reversed(); + } + + private static LayeredParamsDiffSnapshot toSnapshot(List clean, List dirty, List steps) { + // Where each clean parameter goes + int[] targets = new int[clean.size()]; + List insertions = new ArrayList<>(); + + for (Step step : steps) { + switch (step.op()) { + case MATCH -> targets[step.clean()] = step.dirty(); + case SWAP -> { + targets[step.clean()] = step.dirty(); + targets[step.swapClean()] = step.swapDirty(); + } + case DELETE -> targets[step.clean()] = -1; + case INSERT -> insertions.add(step.dirty()); + } + } + + // A deleted parameter inserted again elsewhere is a move + List moved = new ArrayList<>(); + for (Step step : steps) { + if (step.op() == Operation.DELETE) { + Type type = clean.get(step.clean()).type(); + insertions.stream() + .filter(j -> dirty.get(j).type().equals(type)) + .findFirst() + .ifPresent(j -> { + insertions.remove(j); + targets[step.clean()] = j; + moved.add(step.clean()); + }); + } + } + + LayeredParamsDiffSnapshot.Builder builder = LayeredParamsDiffSnapshot.builder(); + List state = new ArrayList<>(); + for (int i = 0; i < clean.size(); i++) { + state.add(new Slot(clean.get(i).type(), targets[i])); + } + + for (int index = state.size() - 1; index >= 0; index--) { + if (state.get(index).target() == -1) { + builder.remove(index); + state.remove(index); + } + } + + for (int index = 0; index < state.size(); index++) { + Slot slot = state.get(index); + Type type = dirty.get(slot.target()).type(); + if (!slot.type().equals(type)) { + builder.replace(index, type); + state.set(index, new Slot(type, slot.target())); + } + } + + for (Step step : steps) { + if (step.op() == Operation.SWAP) { + int from = getIndexOfTarget(state, step.dirty()); + int to = getIndexOfTarget(state, step.swapDirty()); + builder.swap(from, to); + state.set(from, state.set(to, state.get(from))); + } + } + + // Moved parameters that aren't placed yet may still be in the way + // Place each after the last one that goes before it + moved.sort(Comparator.comparingInt(i -> targets[i])); + for (int cleanIndex : moved) { + int from = getIndexOfTarget(state, targets[cleanIndex]); + Slot slot = state.remove(from); + int to = 0; + for (int index = 0; index < state.size(); index++) { + if (state.get(index).target() < slot.target()) { + to = index + 1; + } + } + builder.move(from, to); + state.add(to, slot); + } + + Collections.sort(insertions); + for (int j : insertions) { + builder.insert(j, dirty.get(j).type()); + state.add(j, new Slot(dirty.get(j).type(), j)); + } + return builder.build(); + } + + private static int getMatchCost(Param clean, Param dirty, TypeRelation relation) { + int cost = switch (relation.relate(clean.type(), dirty.type())) { + case SAME -> 0; + case CONVERTIBLE -> CONVERTIBLE; + case UNRELATED -> UNRELATED; + }; + return clean.name() != null && dirty.name() != null && !clean.name().equals(dirty.name()) ? cost + OTHER_NAME : cost; + } + + private static int getIndexOfTarget(List state, int target) { + for (int index = 0; index < state.size(); index++) { + if (state.get(index).target() == target) { + return index; + } + } + throw new IllegalStateException("No parameter goes to " + target); + } + + private ParamsEditDistance() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/params/TypeRelation.java b/core/src/main/java/org/sinytra/adapter/analysis/params/TypeRelation.java new file mode 100644 index 00000000..b9d24d5a --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/analysis/params/TypeRelation.java @@ -0,0 +1,59 @@ +package org.sinytra.adapter.analysis.params; + +import org.objectweb.asm.Type; +import org.sinytra.adapter.analysis.method.InheritanceHandler; + +public interface TypeRelation { + enum Kind { + /// The same type + SAME, + /// A different type we can convert, such as a supertype or a number + CONVERTIBLE, + /// A type with nothing in common + UNRELATED + } + + Kind relate(Type clean, Type dirty); + + /// Class context-free comparison + TypeRelation STATIC = (clean, dirty) -> { + if (clean.equals(dirty)) { + return Kind.SAME; + } + if (isReference(clean) && isReference(dirty) && (isObject(clean) || isObject(dirty))) { + return Kind.CONVERTIBLE; + } + if (isNumber(clean) && isNumber(dirty)) { + return Kind.CONVERTIBLE; + } + return Kind.UNRELATED; + }; + + static TypeRelation inheritance(InheritanceHandler inheritance) { + return (clean, dirty) -> { + Kind kind = STATIC.relate(clean, dirty); + if (kind == Kind.UNRELATED && clean.getSort() == Type.OBJECT && dirty.getSort() == Type.OBJECT + && (inheritance.isClassInherited(clean.getInternalName(), dirty.getInternalName()) + || inheritance.isClassInherited(dirty.getInternalName(), clean.getInternalName())) + ) { + return Kind.CONVERTIBLE; + } + return kind; + }; + } + + private static boolean isReference(Type type) { + return type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY; + } + + private static boolean isObject(Type type) { + return type.getSort() == Type.OBJECT && type.getInternalName().equals("java/lang/Object"); + } + + private static boolean isNumber(Type type) { + return switch (type.getSort()) { + case Type.BYTE, Type.SHORT, Type.CHAR, Type.INT, Type.LONG, Type.FLOAT, Type.DOUBLE -> true; + default -> false; + }; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/params/UnsupportedParamsDiffException.java b/core/src/main/java/org/sinytra/adapter/analysis/params/UnsupportedParamsDiffException.java deleted file mode 100644 index c4c3a5d7..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/params/UnsupportedParamsDiffException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.sinytra.adapter.analysis.params; - -public class UnsupportedParamsDiffException extends IllegalStateException { - public UnsupportedParamsDiffException(String message) { - super(message); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/selector/FrameUtil.java b/core/src/main/java/org/sinytra/adapter/analysis/selector/FrameUtil.java deleted file mode 100644 index d95f4206..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/selector/FrameUtil.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.sinytra.adapter.analysis.selector; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.InvokeDynamicInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.MultiANewArrayInsnNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.SourceInterpreter; -import org.objectweb.asm.tree.analysis.SourceValue; -import org.objectweb.asm.tree.analysis.Value; -import org.sinytra.adapter.analysis.method.AdvancedSourceInterpreter; -import java.util.ArrayList; -import java.util.List; - -public class FrameUtil { - public static Frame[] getFrames(MethodNode methodNode) { - try { - Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); - return analyzer.analyze(Object.class.getName(), methodNode); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static Frame[] getStableFrames(MethodNode methodNode) { - try { - Analyzer analyzer = new Analyzer<>(new AdvancedSourceInterpreter(true)); - return analyzer.analyze(Object.class.getName(), methodNode); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static List getOperands(Frame frame, AbstractInsnNode insn) { - int stackSize = frame.getStackSize(); - int start = Math.max(0, stackSize - getPopCount(insn, frame)); - List operands = new ArrayList<>(stackSize - start); - for (int i = start; i < stackSize; i++) { - operands.add(frame.getStack(i)); - } - return operands; - } - - public static int getPopCount(AbstractInsnNode insn) { - return getPopCount(insn, null); - } - - public static int getPopCount(AbstractInsnNode insn, @Nullable Frame frame) { - int op = insn.getOpcode(); - - if (insn instanceof MethodInsnNode minsn) { - int args = Type.getArgumentCount(minsn.desc); - return op == Opcodes.INVOKESTATIC ? args : args + 1; - } - - if (insn instanceof InvokeDynamicInsnNode indy) { - return Type.getArgumentCount(indy.desc); - } - - if (insn instanceof MultiANewArrayInsnNode multiANewArray) { - return multiANewArray.dims; - } - - return switch (op) { - case Opcodes.POP2, Opcodes.DUP2 -> isWide(frame, 0) ? 1 : 2; - case Opcodes.DUP_X2 -> isWide(frame, 1) ? 2 : 3; - case Opcodes.DUP2_X1 -> isWide(frame, 0) ? 2 : 3; - case Opcodes.DUP2_X2 -> { - if (isWide(frame, 0)) { - yield isWide(frame, 1) ? 2 : 3; - } - yield isWide(frame, 2) ? 3 : 4; - } - - case Opcodes.IASTORE, Opcodes.LASTORE, Opcodes.FASTORE, Opcodes.DASTORE, Opcodes.AASTORE, Opcodes.BASTORE, Opcodes.CASTORE, - Opcodes.SASTORE -> 3; - - case Opcodes.IADD, Opcodes.LADD, Opcodes.FADD, Opcodes.DADD, Opcodes.ISUB, Opcodes.LSUB, Opcodes.FSUB, - Opcodes.DSUB, Opcodes.IMUL, Opcodes.LMUL, Opcodes.FMUL, Opcodes.DMUL, Opcodes.IDIV, Opcodes.LDIV, - Opcodes.FDIV, Opcodes.DDIV, Opcodes.IREM, Opcodes.LREM, Opcodes.FREM, Opcodes.DREM, Opcodes.ISHL, - Opcodes.LSHL, Opcodes.ISHR, Opcodes.LSHR, Opcodes.IUSHR, Opcodes.LUSHR, Opcodes.IAND, Opcodes.LAND, - Opcodes.IOR, Opcodes.LOR, Opcodes.IXOR, Opcodes.LXOR, Opcodes.LCMP, Opcodes.FCMPL, Opcodes.FCMPG, - Opcodes.DCMPL, Opcodes.DCMPG, - Opcodes.IALOAD, Opcodes.LALOAD, Opcodes.FALOAD, Opcodes.DALOAD, Opcodes.AALOAD, Opcodes.BALOAD, Opcodes.CALOAD, - Opcodes.SALOAD, - Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE, Opcodes.IF_ICMPLT, Opcodes.IF_ICMPGE, Opcodes.IF_ICMPGT, Opcodes.IF_ICMPLE, - Opcodes.IF_ACMPEQ, Opcodes.IF_ACMPNE, - Opcodes.PUTFIELD, Opcodes.SWAP, Opcodes.DUP_X1 -> 2; - - case Opcodes.I2L, Opcodes.I2F, Opcodes.I2D, Opcodes.L2I, Opcodes.L2F, Opcodes.L2D, Opcodes.F2I, Opcodes.F2L, - Opcodes.F2D, Opcodes.D2I, Opcodes.D2L, Opcodes.D2F, Opcodes.I2B, Opcodes.I2C, Opcodes.I2S, Opcodes.INEG, - Opcodes.LNEG, Opcodes.FNEG, Opcodes.DNEG, Opcodes.CHECKCAST, Opcodes.INSTANCEOF, - Opcodes.ISTORE, Opcodes.LSTORE, Opcodes.FSTORE, Opcodes.DSTORE, Opcodes.ASTORE, Opcodes.POP, Opcodes.DUP, - Opcodes.IFEQ, Opcodes.IFNE, Opcodes.IFLT, Opcodes.IFGE, Opcodes.IFGT, Opcodes.IFLE, Opcodes.IFNULL, Opcodes.IFNONNULL, - Opcodes.TABLESWITCH, Opcodes.LOOKUPSWITCH, - Opcodes.IRETURN, Opcodes.LRETURN, Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN, Opcodes.ATHROW, - Opcodes.MONITORENTER, Opcodes.MONITOREXIT, Opcodes.ARRAYLENGTH, Opcodes.NEWARRAY, Opcodes.ANEWARRAY, - Opcodes.GETFIELD, Opcodes.PUTSTATIC -> 1; - - default -> 0; - }; - } - - private static boolean isWide(@Nullable Frame frame, int depth) { - if (frame == null) { - return false; - } - int index = frame.getStackSize() - 1 - depth; - return index >= 0 && frame.getStack(index).getSize() == 2; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/analysis/tree/CodePaths.java b/core/src/main/java/org/sinytra/adapter/analysis/tree/CodePaths.java deleted file mode 100644 index 2a65ead4..00000000 --- a/core/src/main/java/org/sinytra/adapter/analysis/tree/CodePaths.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.sinytra.adapter.analysis.tree; - -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.LocalVariableNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.VarInsnNode; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.*; -import java.util.function.Predicate; - -public class CodePaths { - private static final Set IGNORED_PACKAGES = Set.of("java/", "javax/", "jdk/", "sun/", "com/sun/"); - private static final int MAX_DEPTH = 5; - - @Nullable - public static CodePath findCodePath(TargetPair from, TargetPair to, Set trackLocals, MixinContext context, Predicate entryFilter) { - if (from == null || to == null) { - return null; - } - - CodePath path = findCodePathRecursive(from, to, trackLocals, context, entryFilter, new HashMap<>(), 0); - if (path == null) { - return null; - } - - List steps = path.steps(); - - Map finalLocals = new Int2IntOpenHashMap(); - for (Integer original : trackLocals) { - Integer current = original; - - for (int i = 0; i < steps.size() - 1; i++) { - Integer next = steps.get(i).locals().get(current); - if (next == null) { - current = null; - break; - } - current = next; - } - - if (current != null) { - finalLocals.put(original, current); - } - } - - return new CodePath(path.from(), path.to(), steps, finalLocals); - } - - @Nullable - private static CodePath findCodePathRecursive(TargetPair from, TargetPair to, Set trackLocals, MixinContext context, - Predicate filter, Map visited, int depth - ) { - if (depth >= MAX_DEPTH) { - return null; - } - String key = from.classNode().name + "." + from.methodNode().name + from.methodNode().desc + trackLocals; - Integer visitedDepth = visited.get(key); - if (visitedDepth != null && visitedDepth <= depth) { - return null; - } - visited.put(key, depth); - - List topTierCalls = MethodAnalyzer.getTopTierMethodCalls(from, true); - - for (MethodInsnNode minsn : topTierCalls) { - if (!filter.test(minsn) - || IGNORED_PACKAGES.stream().anyMatch(minsn.owner::startsWith) - || !context.patchContext().environment().isKnownPackage(AdapterUtil.internalNameToPkg(minsn.owner)) - ) { - continue; - } - - TargetPair nextFrom = context.methods().findInheritedMethodPair(context.dirtyLookup(), MethodQualifier.create(minsn)); - if (nextFrom == null) continue; - - LocalVariableLookup lvt = context.methods().getLVT(nextFrom.methodNode()); - if (lvt == null) { - continue; - } - - Map locals = new Int2IntOpenHashMap(); - if (!trackLocals.isEmpty()) { - List> receiverInsns = MethodAnalyzer.getMethodCallArgInsns(from.methodNode(), minsn); - for (int i = 0; i < receiverInsns.size(); i++) { - List argInsns = receiverInsns.get(i); - if (argInsns.size() != 1 || !(argInsns.getFirst() instanceof VarInsnNode varInsn)) continue; - - LocalVariableNode local = lvt.getByOrdinalOrNull(i); - if (local == null) continue; - - for (Integer index : trackLocals) { - if (varInsn.var == index) { - locals.put(index, local.index); - } - } - } - } - - if (!locals.keySet().containsAll(trackLocals)) { - continue; - } - - CodePathStep step = new CodePathStep(from, nextFrom, locals); - - if (nextFrom.classNode().name.equals(to.classNode().name) - && nextFrom.methodNode().name.equals(to.methodNode().name) - && nextFrom.methodNode().desc.equals(to.methodNode().desc) - ) { - return new CodePath(from, to, List.of(step), Map.of()); - } - - Set nextTrackLocals = Set.copyOf(locals.values()); - CodePath subPath = findCodePathRecursive(nextFrom, to, nextTrackLocals, context, call -> true, visited, depth + 1); - if (subPath != null) { - List steps = new ArrayList<>(); - steps.add(step); - steps.addAll(subPath.steps()); - return new CodePath(from, to, steps, Map.of()); - } - } - - return null; - } - - public record CodePath(TargetPair from, TargetPair to, List steps, Map locals) { - } - - public record CodePathStep(TargetPair from, TargetPair to, Map locals) { - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/AuditTrail.java b/core/src/main/java/org/sinytra/adapter/api/AuditTrail.java similarity index 84% rename from core/src/main/java/org/sinytra/adapter/env/ctx/AuditTrail.java rename to core/src/main/java/org/sinytra/adapter/api/AuditTrail.java index 28fc7490..da49b724 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/AuditTrail.java +++ b/core/src/main/java/org/sinytra/adapter/api/AuditTrail.java @@ -1,11 +1,13 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.api; import it.unimi.dsi.fastutil.Pair; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.MixinGroups; -import org.sinytra.adapter.patch.config.Configuration; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.audit.AuditTrailImpl; +import org.sinytra.adapter.pipeline.audit.MixinGroups; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import java.util.ArrayList; import java.util.List; @@ -25,6 +27,10 @@ static AuditTrail create() { void recordResult(MixinContext context, Configuration configuration, Match match); + default void recordRejection(MixinContext context, Configuration configuration) { + recordResult(context, configuration, Match.NONE); + } + void processGroups(ClassNode classNode, MixinGroups groups); String getCompleteReport(); diff --git a/core/src/main/java/org/sinytra/adapter/types/BytecodeFixerUpper.java b/core/src/main/java/org/sinytra/adapter/api/BytecodeFixerUpper.java similarity index 91% rename from core/src/main/java/org/sinytra/adapter/types/BytecodeFixerUpper.java rename to core/src/main/java/org/sinytra/adapter/api/BytecodeFixerUpper.java index 8874ef54..3524871a 100644 --- a/core/src/main/java/org/sinytra/adapter/types/BytecodeFixerUpper.java +++ b/core/src/main/java/org/sinytra/adapter/api/BytecodeFixerUpper.java @@ -1,10 +1,13 @@ -package org.sinytra.adapter.types; +package org.sinytra.adapter.api; import com.mojang.datafixers.util.Pair; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; import org.objectweb.asm.tree.FieldNode; -import org.sinytra.adapter.util.provider.ClassLookup; +import org.sinytra.adapter.types.BytecodeFixerJarGenerator; +import org.sinytra.adapter.types.ObjectTypeAdapter; +import org.sinytra.adapter.types.SupplierTypeAdapter; +import org.sinytra.adapter.types.TypeAdapterProvider; import java.util.List; import java.util.Map; diff --git a/core/src/main/java/org/sinytra/adapter/util/provider/ClassLookup.java b/core/src/main/java/org/sinytra/adapter/api/ClassLookup.java similarity index 94% rename from core/src/main/java/org/sinytra/adapter/util/provider/ClassLookup.java rename to core/src/main/java/org/sinytra/adapter/api/ClassLookup.java index 1b6ba4ca..0e07a315 100644 --- a/core/src/main/java/org/sinytra/adapter/util/provider/ClassLookup.java +++ b/core/src/main/java/org/sinytra/adapter/api/ClassLookup.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.util.provider; +package org.sinytra.adapter.api; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; diff --git a/core/src/main/java/org/sinytra/adapter/api/ClassPatchContext.java b/core/src/main/java/org/sinytra/adapter/api/ClassPatchContext.java new file mode 100644 index 00000000..f892c6dd --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/api/ClassPatchContext.java @@ -0,0 +1,28 @@ +package org.sinytra.adapter.api; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.sinytra.adapter.pipeline.ctx.PatchContextImpl; + +import java.util.List; + +public interface ClassPatchContext { + static ClassPatchContext of(ClassNode classNode, List targetTypes, PatchEnvironment environment) { + return new PatchContextImpl(classNode, null, targetTypes, environment); + } + + ClassNode classNode(); + + List targetTypes(); + + @Nullable + default Type singleTarget() { + List types = targetTypes(); + return types.size() == 1 ? types.getFirst() : null; + } + + PatchEnvironment environment(); + + String remap(String reference); +} diff --git a/core/src/main/java/org/sinytra/adapter/api/ClassTransformer.java b/core/src/main/java/org/sinytra/adapter/api/ClassTransformer.java new file mode 100644 index 00000000..7d32f3d4 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/api/ClassTransformer.java @@ -0,0 +1,7 @@ +package org.sinytra.adapter.api; + +import org.objectweb.asm.tree.ClassNode; + +public interface ClassTransformer { + PatchResult apply(ClassNode classNode, ClassPatchContext context); +} diff --git a/core/src/main/java/org/sinytra/adapter/api/DynamicPatches.java b/core/src/main/java/org/sinytra/adapter/api/DynamicPatches.java new file mode 100644 index 00000000..50c25ba5 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/api/DynamicPatches.java @@ -0,0 +1,26 @@ +package org.sinytra.adapter.api; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import org.sinytra.adapter.pipeline.PipelineMethodTransformer; +import org.sinytra.adapter.pipeline.transform.AnonymousTargetTransformer; +import org.sinytra.adapter.pipeline.transform.LocalCaptureUpgradeTransformer; +import org.sinytra.adapter.types.FieldTypeUsageTransformer; + +import java.util.List; + +public final class DynamicPatches { + public static final List CLASS_PATCHES = List.of( + new AnonymousTargetTransformer(), + new FieldTypeUsageTransformer() + ); + + public static Multimap methodTransformers(List patches) { + return ImmutableMultimap.of( + TxPhase.EARLY, new LocalCaptureUpgradeTransformer(), + TxPhase.VALIDATED, new PipelineMethodTransformer(patches, false) + ); + } + + private DynamicPatches() {} +} diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatch.java b/core/src/main/java/org/sinytra/adapter/api/MethodPatch.java similarity index 57% rename from core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatch.java rename to core/src/main/java/org/sinytra/adapter/api/MethodPatch.java index 7f4aea90..f2ae7d87 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatch.java +++ b/core/src/main/java/org/sinytra/adapter/api/MethodPatch.java @@ -1,8 +1,9 @@ -package org.sinytra.adapter.transform.patch; +package org.sinytra.adapter.api; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.transform.MethodTransformer; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.pipeline.manual.ConfigurationMatcher; +import org.sinytra.adapter.pipeline.manual.MethodPatchBuilderImpl; import java.util.List; import java.util.function.BiConsumer; diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilder.java b/core/src/main/java/org/sinytra/adapter/api/MethodPatchBuilder.java similarity index 80% rename from core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilder.java rename to core/src/main/java/org/sinytra/adapter/api/MethodPatchBuilder.java index 8be5d2e2..f6e73db3 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilder.java +++ b/core/src/main/java/org/sinytra/adapter/api/MethodPatchBuilder.java @@ -1,11 +1,8 @@ -package org.sinytra.adapter.transform.patch; +package org.sinytra.adapter.api; import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.commons.InstructionAdapter; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.transform.MethodTransformer; +import org.sinytra.adapter.model.param.MethodParameters; -import java.util.function.Consumer; import java.util.function.UnaryOperator; public interface MethodPatchBuilder { @@ -43,8 +40,6 @@ public interface MethodPatchBuilder { MethodPatchBuilder modifyMixinType(String newType); - MethodPatchBuilder divertRedirector(Consumer patcher); - MethodPatchBuilder disable(); MethodPatchBuilder transform(MethodTransformer transformer); diff --git a/core/src/main/java/org/sinytra/adapter/api/MethodPatchContext.java b/core/src/main/java/org/sinytra/adapter/api/MethodPatchContext.java new file mode 100644 index 00000000..642561d2 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/api/MethodPatchContext.java @@ -0,0 +1,17 @@ +package org.sinytra.adapter.api; + +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +public interface MethodPatchContext { + ClassNode classNode(); + + MethodNode methodNode(); + + AnnotationNode injectorAnnotation(); + + PatchEnvironment environment(); + + void recordAudit(Object actor, String message, Object... args); +} diff --git a/core/src/main/java/org/sinytra/adapter/api/MethodTransformer.java b/core/src/main/java/org/sinytra/adapter/api/MethodTransformer.java new file mode 100644 index 00000000..bfe57087 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/api/MethodTransformer.java @@ -0,0 +1,5 @@ +package org.sinytra.adapter.api; + +public interface MethodTransformer { + PatchResult apply(MethodPatchContext context); +} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/MixinClassGenerator.java b/core/src/main/java/org/sinytra/adapter/api/MixinClassGenerator.java similarity index 91% rename from core/src/main/java/org/sinytra/adapter/env/ctx/MixinClassGenerator.java rename to core/src/main/java/org/sinytra/adapter/api/MixinClassGenerator.java index 57bb1cf7..08e3efd4 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/MixinClassGenerator.java +++ b/core/src/main/java/org/sinytra/adapter/api/MixinClassGenerator.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.api; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.tree.ClassNode; diff --git a/core/src/main/java/org/sinytra/adapter/util/provider/MixinClassLookup.java b/core/src/main/java/org/sinytra/adapter/api/MixinClassLookup.java similarity index 95% rename from core/src/main/java/org/sinytra/adapter/util/provider/MixinClassLookup.java rename to core/src/main/java/org/sinytra/adapter/api/MixinClassLookup.java index 805cb9cb..65151af7 100644 --- a/core/src/main/java/org/sinytra/adapter/util/provider/MixinClassLookup.java +++ b/core/src/main/java/org/sinytra/adapter/api/MixinClassLookup.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.util.provider; +package org.sinytra.adapter.api; import com.mojang.logging.LogUtils; import org.objectweb.asm.tree.ClassNode; diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchEnvironment.java b/core/src/main/java/org/sinytra/adapter/api/PatchEnvironment.java similarity index 86% rename from core/src/main/java/org/sinytra/adapter/env/ctx/PatchEnvironment.java rename to core/src/main/java/org/sinytra/adapter/api/PatchEnvironment.java index e82698d0..7aa5d46f 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchEnvironment.java +++ b/core/src/main/java/org/sinytra/adapter/api/PatchEnvironment.java @@ -1,9 +1,8 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.api; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.analysis.InheritanceHandler; -import org.sinytra.adapter.types.BytecodeFixerUpper; -import org.sinytra.adapter.util.provider.ClassLookup; +import org.sinytra.adapter.analysis.method.InheritanceHandler; +import org.sinytra.adapter.pipeline.ctx.PatchEnvironmentImpl; import java.util.Collection; diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchResult.java b/core/src/main/java/org/sinytra/adapter/api/PatchResult.java similarity index 89% rename from core/src/main/java/org/sinytra/adapter/env/ctx/PatchResult.java rename to core/src/main/java/org/sinytra/adapter/api/PatchResult.java index a083c60c..158b1c88 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchResult.java +++ b/core/src/main/java/org/sinytra/adapter/api/PatchResult.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.api; public enum PatchResult { PASS, diff --git a/core/src/main/java/org/sinytra/adapter/api/Patcher.java b/core/src/main/java/org/sinytra/adapter/api/Patcher.java new file mode 100644 index 00000000..c486012d --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/api/Patcher.java @@ -0,0 +1,57 @@ +package org.sinytra.adapter.api; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import org.objectweb.asm.tree.ClassNode; +import org.sinytra.adapter.pipeline.PatcherImpl; + +import java.util.ArrayList; +import java.util.List; + +/** + * Patches mixin classes so that they apply to the dirty environment. + */ +public interface Patcher { + /** + * Patch a mixin class in place. Classes that are not mixins are left unchanged. + */ + PatchResult process(ClassNode classNode); + + static Builder builder(PatchEnvironment environment) { + return new Builder(environment); + } + + class Builder { + private final PatchEnvironment environment; + private final List classTransformers = new ArrayList<>(); + private final Multimap methodTransformers = HashMultimap.create(); + + public Builder(PatchEnvironment environment) { + this.environment = environment; + } + + public Builder classTransformer(ClassTransformer transformer) { + this.classTransformers.add(transformer); + return this; + } + + public Builder classTransformers(List transformers) { + this.classTransformers.addAll(transformers); + return this; + } + + public Builder methodTransformer(TxPhase phase, MethodTransformer transformer) { + this.methodTransformers.put(phase, transformer); + return this; + } + + public Builder methodTransformers(Multimap transformers) { + this.methodTransformers.putAll(transformers); + return this; + } + + public Patcher build() { + return new PatcherImpl(this.environment, this.classTransformers, this.methodTransformers); + } + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/RefmapHolder.java b/core/src/main/java/org/sinytra/adapter/api/RefmapHolder.java similarity index 77% rename from core/src/main/java/org/sinytra/adapter/env/ctx/RefmapHolder.java rename to core/src/main/java/org/sinytra/adapter/api/RefmapHolder.java index 28befb0e..d63874ea 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/RefmapHolder.java +++ b/core/src/main/java/org/sinytra/adapter/api/RefmapHolder.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.api; public interface RefmapHolder { String remap(String cls, String reference); diff --git a/core/src/main/java/org/sinytra/adapter/types/SimpleTypeAdapter.java b/core/src/main/java/org/sinytra/adapter/api/SimpleTypeAdapter.java similarity index 92% rename from core/src/main/java/org/sinytra/adapter/types/SimpleTypeAdapter.java rename to core/src/main/java/org/sinytra/adapter/api/SimpleTypeAdapter.java index aded5018..94da6c47 100644 --- a/core/src/main/java/org/sinytra/adapter/types/SimpleTypeAdapter.java +++ b/core/src/main/java/org/sinytra/adapter/api/SimpleTypeAdapter.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.types; +package org.sinytra.adapter.api; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; diff --git a/core/src/main/java/org/sinytra/adapter/patch/TxPhase.java b/core/src/main/java/org/sinytra/adapter/api/TxPhase.java similarity index 87% rename from core/src/main/java/org/sinytra/adapter/patch/TxPhase.java rename to core/src/main/java/org/sinytra/adapter/api/TxPhase.java index cfb70222..066ffad5 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/TxPhase.java +++ b/core/src/main/java/org/sinytra/adapter/api/TxPhase.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.patch; +package org.sinytra.adapter.api; public enum TxPhase { /** diff --git a/core/src/main/java/org/sinytra/adapter/types/TypeAdapter.java b/core/src/main/java/org/sinytra/adapter/api/TypeAdapter.java similarity index 93% rename from core/src/main/java/org/sinytra/adapter/types/TypeAdapter.java rename to core/src/main/java/org/sinytra/adapter/api/TypeAdapter.java index a98a80ed..600652bf 100644 --- a/core/src/main/java/org/sinytra/adapter/types/TypeAdapter.java +++ b/core/src/main/java/org/sinytra/adapter/api/TypeAdapter.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.types; +package org.sinytra.adapter.api; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; diff --git a/core/src/main/java/org/sinytra/adapter/util/provider/ZipClassLookup.java b/core/src/main/java/org/sinytra/adapter/api/ZipClassLookup.java similarity index 97% rename from core/src/main/java/org/sinytra/adapter/util/provider/ZipClassLookup.java rename to core/src/main/java/org/sinytra/adapter/api/ZipClassLookup.java index 1e9f82b3..2ff14761 100644 --- a/core/src/main/java/org/sinytra/adapter/util/provider/ZipClassLookup.java +++ b/core/src/main/java/org/sinytra/adapter/api/ZipClassLookup.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.util.provider; +package org.sinytra.adapter.api; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; diff --git a/core/src/main/java/org/sinytra/adapter/analysis/locals/LVTSnapshot.java b/core/src/main/java/org/sinytra/adapter/edit/LVTSnapshot.java similarity index 98% rename from core/src/main/java/org/sinytra/adapter/analysis/locals/LVTSnapshot.java rename to core/src/main/java/org/sinytra/adapter/edit/LVTSnapshot.java index 171af333..d960fdb2 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/locals/LVTSnapshot.java +++ b/core/src/main/java/org/sinytra/adapter/edit/LVTSnapshot.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.analysis.locals; +package org.sinytra.adapter.edit; import it.unimi.dsi.fastutil.ints.Int2IntArrayMap; import it.unimi.dsi.fastutil.ints.Int2IntMap; diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/MixinClassGeneratorImpl.java b/core/src/main/java/org/sinytra/adapter/edit/MixinClassGeneratorImpl.java similarity index 90% rename from core/src/main/java/org/sinytra/adapter/env/ctx/MixinClassGeneratorImpl.java rename to core/src/main/java/org/sinytra/adapter/edit/MixinClassGeneratorImpl.java index 808ce577..1e46f039 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/MixinClassGeneratorImpl.java +++ b/core/src/main/java/org/sinytra/adapter/edit/MixinClassGeneratorImpl.java @@ -1,13 +1,14 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.edit; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.util.provider.ClassLookup; -import org.sinytra.adapter.env.util.TypeConstants; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.MixinClassGenerator; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.constant.TypeConstants; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/InjectParameterTransform.java b/core/src/main/java/org/sinytra/adapter/edit/param/InjectParameterTransform.java similarity index 72% rename from core/src/main/java/org/sinytra/adapter/transform/param/InjectParameterTransform.java rename to core/src/main/java/org/sinytra/adapter/edit/param/InjectParameterTransform.java index 9a334446..ddc3a32a 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/InjectParameterTransform.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/InjectParameterTransform.java @@ -1,23 +1,21 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.TypeReference; import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.locals.LVTSnapshot; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ctx.MethodHelper; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.mixin.MixinFlag; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.edit.LVTSnapshot; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.sinytra.adapter.util.AdapterUtil; import java.util.Comparator; import java.util.List; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.PROPERTY_INDEX; -import static org.sinytra.adapter.transform.param.ParamTransformationUtil.getLVTIndex; +import static org.sinytra.adapter.edit.param.ParamTransformationUtil.getLVTIndex; // TODO Cleanup param transformers public record InjectParameterTransform(int index, Type type) implements ParameterTransformer { @@ -28,26 +26,13 @@ public InjectParameterTransform offset(int offset) { @Override public PatchResult apply(ClassNode classNode, MethodNode methodNode, MixinContext context, List parameters, int offset) { - boolean isNonStatic = !MethodHelper.isStatic(methodNode); + boolean isNonStatic = !MethodAnalyzer.isStatic(methodNode); int index = this.index + offset; if (index >= parameters.size() + 1) { return PatchResult.PASS; } AnnotationHandle annotation = context.methodAnnotation(); - if (context.hasFlag(MixinFlag.TARGETS_VARIABLE)) { - if (index < 1) { - return PatchResult.PASS; - } - int insertedLvtIndex = getLVTIndex(parameters.subList(1, parameters.size()), isNonStatic, index - 1); - annotation.getValue(PROPERTY_INDEX).ifPresent(indexHandle -> { - int indexValue = indexHandle.get(); - if (indexValue >= insertedLvtIndex) { - indexHandle.set(indexValue + this.type.getSize()); - } - }); - return PatchResult.APPLY; - } if (annotation.matchesDesc(MixinAnnotations.MODIFY_ARGS)) { return ModifyArgsOffsetUpgrader.upgradeAfterParamInsert(methodNode, index) ? PatchResult.APPLY : PatchResult.PASS; } diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/ModifyArgsOffsetUpgrader.java b/core/src/main/java/org/sinytra/adapter/edit/param/ModifyArgsOffsetUpgrader.java similarity index 82% rename from core/src/main/java/org/sinytra/adapter/transform/param/ModifyArgsOffsetUpgrader.java rename to core/src/main/java/org/sinytra/adapter/edit/param/ModifyArgsOffsetUpgrader.java index 75c6f7dd..89523966 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/ModifyArgsOffsetUpgrader.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/ModifyArgsOffsetUpgrader.java @@ -1,14 +1,14 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.constant.TypeConstants; import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.env.util.TypeConstants; -import java.util.Set; import java.util.LinkedHashSet; +import java.util.Set; public class ModifyArgsOffsetUpgrader { private static final MethodQualifier ARGS_GET = new MethodQualifier(TypeConstants.ARGS_TYPE.getDescriptor(), "get", "(I)Ljava/lang/Object;"); @@ -16,7 +16,7 @@ public class ModifyArgsOffsetUpgrader { public static boolean upgradeAfterParamInsert(MethodNode methodNode, int index) { Set insns = new LinkedHashSet<>(); - MethodAnalyzer.forEachCall(methodNode, (call, operands) -> { + DataFlow.of(methodNode).forEachCall((call, operands) -> { if ((ARGS_GET.matches(call) || ARGS_SET.matches(call)) && operands.size() > 1) { insns.add(AdapterUtil.getSingleInsn(operands.get(1))); } diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/MoveParametersTransformer.java b/core/src/main/java/org/sinytra/adapter/edit/param/MoveParametersTransformer.java similarity index 92% rename from core/src/main/java/org/sinytra/adapter/transform/param/MoveParametersTransformer.java rename to core/src/main/java/org/sinytra/adapter/edit/param/MoveParametersTransformer.java index af0a59d9..2bff68fe 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/MoveParametersTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/MoveParametersTransformer.java @@ -1,11 +1,11 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import com.mojang.logging.LogUtils; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.slf4j.Logger; import java.util.ArrayList; diff --git a/core/src/main/java/org/sinytra/adapter/edit/param/ParamTransformationUtil.java b/core/src/main/java/org/sinytra/adapter/edit/param/ParamTransformationUtil.java new file mode 100644 index 00000000..e2b2c6a6 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/edit/param/ParamTransformationUtil.java @@ -0,0 +1,169 @@ +package org.sinytra.adapter.edit.param; + +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.param.Annotation; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.util.AdapterUtil; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class ParamTransformationUtil { + public static void updateParameterAnnotationArrays(MethodNode methodNode, int paramCount, Consumer>> action) { + methodNode.visibleParameterAnnotations = updateParameterAnnotations(methodNode.visibleParameterAnnotations, paramCount, action); + methodNode.invisibleParameterAnnotations = updateParameterAnnotations(methodNode.invisibleParameterAnnotations, paramCount, action); + // Derive the count from the final descriptor later + methodNode.visibleAnnotableParameterCount = 0; + methodNode.invisibleAnnotableParameterCount = 0; + } + + @Nullable + @SuppressWarnings("unchecked") + private static List[] updateParameterAnnotations(@Nullable List[] annotations, int paramCount, Consumer>> action) { + if (annotations == null) { + return null; + } + List> list = new ArrayList<>(Arrays.asList(annotations)); + while (list.size() < paramCount) { + list.add(null); + } + action.accept(list); + return list.toArray(List[]::new); + } + + public static void reorderParameters(MethodNode methodNode, List parameters, boolean nonStatic, int[] order) { + List permuted = Arrays.stream(order).mapToObj(parameters::get).toList(); + + Int2IntMap slots = new Int2IntOpenHashMap(); + for (int pos = 0; pos < order.length; pos++) { + slots.put(getLVTIndex(parameters, nonStatic, order[pos]), getLVTIndex(permuted, nonStatic, pos)); + } + AdapterUtil.replaceLVT(methodNode, idx -> slots.getOrDefault(idx, idx)); + if (methodNode.localVariables != null) { + for (LocalVariableNode lvn : methodNode.localVariables) { + lvn.index = slots.getOrDefault(lvn.index, lvn.index); + } + } + + updateParameterAnnotationArrays(methodNode, parameters.size(), annotations -> { + List> original = new ArrayList<>(annotations); + for (int pos = 0; pos < order.length; pos++) { + annotations.set(pos, original.get(order[pos])); + } + }); + + for (int pos = 0; pos < order.length; pos++) { + parameters.set(pos, permuted.get(pos)); + } + } + + public static void retypeParameter(MethodNode method, int index, Type type, boolean castLoads) { + Type[] args = Type.getArgumentTypes(method.desc); + Type old = args[index]; + int slot = getLVTIndex(Arrays.asList(args), !MethodAnalyzer.isStatic(method), index); + args[index] = type; + method.desc = Type.getMethodDescriptor(Type.getReturnType(method.desc), args); + if (method.localVariables != null) { + method.localVariables.stream() + .filter(local -> local.index == slot) + .findFirst() + .ifPresent(local -> { + local.desc = type.getDescriptor(); + local.signature = null; + }); + } + if (castLoads) { + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof VarInsnNode load && load.getOpcode() == Opcodes.ALOAD && load.var == slot) { + method.instructions.insert(load, new TypeInsnNode(Opcodes.CHECKCAST, old.getInternalName())); + } + } + } + } + + public static int getLVTIndex(List parameters, boolean nonStatic, int index) { + int lvt = nonStatic ? 1 : 0; + for (int i = 0; i < index; i++) { + lvt += parameters.get(i).getSize(); + } + return lvt; + } + + public static Map> gatherVarMappings(MethodNode method, List cleanParameters, List dirtyParameters, Map replacements) { + LocalVariableLookup lookup = method.localVariables == null ? null : new LocalVariableLookup(method); + Map> map = replacements.entrySet().stream() + .map(entry -> { + int oldIndex = cleanParameters.indexOf(entry.getKey()); + if (oldIndex == -1) return null; + + int newIndex = dirtyParameters.indexOf(entry.getValue()); + if (newIndex == -1) return null; + + LocalVariableNode oldVar = lookup == null ? null : lookup.getByParameterOrdinalOrNull(oldIndex); + if (oldVar == null) return null; + + LocalVariableNode newVar = lookup.getByParameterOrdinalOrNull(newIndex); + if (newVar == null) return null; + + return Pair.of(oldVar.index, Pair.of(newVar.index, entry.getValue().type())); + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond, (first, second) -> first)); + + Map> varMap = new HashMap<>(); + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof VarInsnNode varInsn && map.containsKey(varInsn.var)) { + varMap.put(varInsn, map.get(varInsn.var)); + } + } + return varMap; + } + + public static void applyVarMappings(MethodNode methodNode, Map> map) { + map.forEach((i, v) -> retypeVariableAndReceivers(methodNode, i, v.getFirst(), v.getSecond().getInternalName())); + } + + public static void applyAnnotations(MethodNode method, List parameters) { + updateParameterAnnotationArrays(method, Type.getArgumentCount(method.desc), annotations -> {}); + + for (int i = 0; i < parameters.size(); i++) { + Parameter param = parameters.get(i); + + // TODO Always re-apply all annotations or? + // Remove old annotations + Set descs = param.annotations().stream().map(Annotation::getDesc).collect(Collectors.toSet()); + final int finalI = i; + Stream.of(method.visibleParameterAnnotations, method.invisibleParameterAnnotations) + .filter(arr -> arr != null && finalI < arr.length) + .map(arr -> arr[finalI]) + .filter(Objects::nonNull) + .forEach(list -> list.removeIf(n -> descs.contains(n.desc))); + + for (Annotation annotation : param.annotations()) { + AnnotationVisitor visitor = method.visitParameterAnnotation(i, annotation.getDesc(), annotation.isVisible()); + annotation.accept(visitor); + } + } + } + + public static void retypeVariableAndReceivers(MethodNode methodNode, VarInsnNode targetVarInsn, int newIndex, String newOwner) { + targetVarInsn.var = newIndex; + DataFlow.of(methodNode).forEachReceiver((call, receiver) -> { + if (receiver.contains(targetVarInsn)) { + call.owner = newOwner; + } + }); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/ParameterTransformer.java b/core/src/main/java/org/sinytra/adapter/edit/param/ParameterTransformer.java similarity index 71% rename from core/src/main/java/org/sinytra/adapter/transform/param/ParameterTransformer.java rename to core/src/main/java/org/sinytra/adapter/edit/param/ParameterTransformer.java index 1ac1a148..76ab4181 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/ParameterTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/ParameterTransformer.java @@ -1,10 +1,10 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import java.util.List; diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/RemoveParameterTransformer.java b/core/src/main/java/org/sinytra/adapter/edit/param/RemoveParameterTransformer.java similarity index 89% rename from core/src/main/java/org/sinytra/adapter/transform/param/RemoveParameterTransformer.java rename to core/src/main/java/org/sinytra/adapter/edit/param/RemoveParameterTransformer.java index 8c833d6c..a45de1d9 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/RemoveParameterTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/RemoveParameterTransformer.java @@ -1,12 +1,12 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.analysis.locals.LVTSnapshot; -import org.sinytra.adapter.env.ctx.PatchResult; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.edit.LVTSnapshot; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.sinytra.adapter.util.AdapterUtil; import java.util.List; diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/ReplaceParametersTransformer.java b/core/src/main/java/org/sinytra/adapter/edit/param/ReplaceParametersTransformer.java similarity index 75% rename from core/src/main/java/org/sinytra/adapter/transform/param/ReplaceParametersTransformer.java rename to core/src/main/java/org/sinytra/adapter/edit/param/ReplaceParametersTransformer.java index 63a60d11..3054b13a 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/ReplaceParametersTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/ReplaceParametersTransformer.java @@ -1,18 +1,18 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.flow.DataFlow; import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.types.TypeAdapter; +import org.sinytra.adapter.analysis.method.WrapOpOriginalCall; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.slf4j.Logger; -import org.sinytra.adapter.patch.processor.wrapop.WrapOpOriginalCall; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; import java.util.List; @@ -35,12 +35,13 @@ public ReplaceParametersTransformer offset(int offset) { public PatchResult apply(ClassNode classNode, MethodNode methodNode, MixinContext context, List parameters, int offset) { final int paramIndex = this.index + offset; - if (Type.getArgumentCount(methodNode.desc) <= paramIndex) { + if (paramIndex >= parameters.size()) { return PatchResult.PASS; } - LocalVariableLookup lvtLookup = context.methods().getLVT(methodNode); - LocalVariableNode localVar = lvtLookup == null ? null : lvtLookup.getByParameterOrdinalOrNull(paramIndex); + boolean nonStatic = !context.isStatic(); + LocalVariableLookup lvtLookup = LocalVariableLookup.of(methodNode); + LocalVariableNode localVar = lvtLookup == null ? null : findParameterLocal(lvtLookup, ParamTransformationUtil.getLVTIndex(parameters, nonStatic, paramIndex)); if (localVar == null) { LOGGER.info(MIXINPATCH, "Cannot replace parameter {}, missing local variable info in {}.{}", paramIndex, classNode.name, methodNode.name); return PatchResult.PASS; @@ -68,10 +69,9 @@ public PatchResult apply(ClassNode classNode, MethodNode methodNode, MixinContex TypeAdapter typeFix = context.getTypeAdapter(type, originalType); // If this is a wrap operation, make an educated guess and try adapting the instance type if (typeFix == null && context.methodAnnotation().matchesDesc(MixinAnnotations.WRAP_OPERATION)) { - List params = Parameters.getParameterTypes(methodNode.desc); - LocalVariableNode firstParam = lvtLookup.getByParameterOrdinalOrNull(0); - if (!params.isEmpty() && firstParam != null) { - typeFix = context.getTypeAdapter(params.getFirst(), originalType); + LocalVariableNode firstParam = parameters.isEmpty() ? null : findParameterLocal(lvtLookup, nonStatic ? 1 : 0); + if (firstParam != null) { + typeFix = context.getTypeAdapter(parameters.getFirst(), originalType); if (typeFix != null) { varInsn.var = firstParam.index; } @@ -85,7 +85,7 @@ public PatchResult apply(ClassNode classNode, MethodNode methodNode, MixinContex } // Calls made on the variable must target the new type unless its value was adapted back to the original above - MethodAnalyzer.forEachReceiver(methodNode, (call, receiver) -> { + DataFlow.of(methodNode).forEachReceiver((call, receiver) -> { if (call.owner.equals(originalType.getInternalName()) && receiver.stream().anyMatch(p -> p instanceof VarInsnNode varInsn && varInsn.var == localVar.index) ) { @@ -96,4 +96,9 @@ public PatchResult apply(ClassNode classNode, MethodNode methodNode, MixinContex return PatchResult.COMPUTE_FRAMES; } + + @Nullable + private static LocalVariableNode findParameterLocal(LocalVariableLookup lookup, int slot) { + return lookup.getLocals().stream().filter(local -> local.index == slot).findFirst().orElse(null); + } } diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/SwapParametersTransformer.java b/core/src/main/java/org/sinytra/adapter/edit/param/SwapParametersTransformer.java similarity index 90% rename from core/src/main/java/org/sinytra/adapter/transform/param/SwapParametersTransformer.java rename to core/src/main/java/org/sinytra/adapter/edit/param/SwapParametersTransformer.java index 4c20fb69..a8c9a446 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/SwapParametersTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/SwapParametersTransformer.java @@ -1,11 +1,11 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import com.mojang.logging.LogUtils; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.slf4j.Logger; import java.util.List; diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/TransformParameters.java b/core/src/main/java/org/sinytra/adapter/edit/param/TransformParameters.java similarity index 94% rename from core/src/main/java/org/sinytra/adapter/transform/param/TransformParameters.java rename to core/src/main/java/org/sinytra/adapter/edit/param/TransformParameters.java index 1647a298..cb700ed5 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/param/TransformParameters.java +++ b/core/src/main/java/org/sinytra/adapter/edit/param/TransformParameters.java @@ -1,13 +1,13 @@ -package org.sinytra.adapter.transform.param; +package org.sinytra.adapter.edit.param; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.patch.mixin.MixinFlag; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.injector.MixinFlag; import java.util.ArrayList; import java.util.Arrays; diff --git a/core/src/main/java/org/sinytra/adapter/env/MockMixinRuntime.java b/core/src/main/java/org/sinytra/adapter/env/MockMixinRuntime.java deleted file mode 100644 index 42f0a80d..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/MockMixinRuntime.java +++ /dev/null @@ -1,313 +0,0 @@ -package org.sinytra.adapter.env; - -import com.mojang.logging.LogUtils; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ctx.PatchEnvironment; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.slf4j.Logger; -import org.spongepowered.asm.mixin.FabricUtil; -import org.spongepowered.asm.mixin.MixinEnvironment; -import org.spongepowered.asm.mixin.extensibility.IMixinConfig; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigSource; -import org.spongepowered.asm.mixin.extensibility.IMixinInfo; -import org.spongepowered.asm.mixin.injection.InjectionPoint; -import org.spongepowered.asm.mixin.injection.code.ISliceContext; -import org.spongepowered.asm.mixin.injection.code.MethodSlice; -import org.spongepowered.asm.mixin.injection.selectors.ISelectorContext; -import org.spongepowered.asm.mixin.injection.struct.CallbackInjectionInfo; -import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; -import org.spongepowered.asm.mixin.injection.struct.ModifyVariableInjectionInfo; -import org.spongepowered.asm.mixin.injection.struct.Target; -import org.spongepowered.asm.mixin.refmap.IMixinContext; -import org.spongepowered.asm.mixin.refmap.IReferenceMapper; -import org.spongepowered.asm.mixin.struct.AnnotatedMethodInfo; -import org.spongepowered.asm.mixin.struct.SpecialMethodInfo; -import org.spongepowered.asm.mixin.transformer.ClassInfo; -import org.spongepowered.asm.mixin.transformer.ext.Extensions; -import org.spongepowered.asm.util.asm.IAnnotationHandle; -import sun.misc.Unsafe; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.invoke.VarHandle; -import java.lang.reflect.Field; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -public class MockMixinRuntime { - private static final Logger LOGGER = LogUtils.getLogger(); - private static final MethodHandles.Lookup TRUSTED_LOOKUP; - private static final Unsafe UNSAFE; - private static final MethodHandle TARGET_CTR; - private static final Class LOCAL_VARIABLE_INJECTION_POINT_CLASS; - private static final VarHandle ANNOTATED_METHOD_INFO_CONTEXT; - private static final MethodHandle LOCAL_VARIABLE_INJECTION_POINT_FIND; - - static { - try { - Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); - theUnsafe.setAccessible(true); - UNSAFE = (Unsafe) theUnsafe.get(null); - Field hackfield = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); - TRUSTED_LOOKUP = (MethodHandles.Lookup) UNSAFE.getObject(UNSAFE.staticFieldBase(hackfield), UNSAFE.staticFieldOffset(hackfield)); - - TARGET_CTR = MethodHandles.privateLookupIn(Target.class, MethodHandles.lookup()).findConstructor(Target.class, MethodType.methodType(void.class, ClassInfo.class, ClassNode.class, MethodNode.class)); - - LOCAL_VARIABLE_INJECTION_POINT_CLASS = Class.forName("org.spongepowered.asm.mixin.injection.modify.ModifyVariableInjector$LocalVariableInjectionPoint"); - ANNOTATED_METHOD_INFO_CONTEXT = TRUSTED_LOOKUP.findVarHandle(AnnotatedMethodInfo.class, "context", IMixinContext.class); - LOCAL_VARIABLE_INJECTION_POINT_FIND = MethodHandles.privateLookupIn(LOCAL_VARIABLE_INJECTION_POINT_CLASS, MethodHandles.lookup()).findVirtual(LOCAL_VARIABLE_INJECTION_POINT_CLASS, "find", MethodType.methodType(boolean.class, InjectionInfo.class, InsnList.class, Collection.class, Target.class)); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static Target createMixinTarget(TargetPair pair) { - ClassInfo info = ClassInfo.forName(pair.classNode().name); - try { - return (Target) TARGET_CTR.invoke(info, pair.classNode(), pair.methodNode()); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static IMixinContext forClass(String className, String targetClass, PatchEnvironment environment) { - return new ClassMixinContext(className, targetClass, environment); - } - - public static ISliceContext forSlice(IMixinContext context, MethodNode methodNode) { - return new MethodSliceContext(context, methodNode); - } - - public static InjectionInfo forInjectionInfo(String className, String targetClass, PatchEnvironment environment) { - try { - InjectionInfo injectionInfo = (InjectionInfo) UNSAFE.allocateInstance(CallbackInjectionInfo.class); - IMixinContext context = forClass(className, targetClass, environment); - ANNOTATED_METHOD_INFO_CONTEXT.set(injectionInfo, context); - CompatibilityContexts.attach(injectionInfo, environment.fabricLVTCompatibility()); - return injectionInfo; - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static boolean findModifyVariableInjectionInsns(InjectionPoint injectionPoint, IMixinContext mixinContext, InsnList insns, List list, Target target) throws Throwable { - InjectionInfo info = MockMixinRuntime.createModifyVariableInjectionInfo(mixinContext); - return (boolean) LOCAL_VARIABLE_INJECTION_POINT_FIND.invoke(injectionPoint, info, insns, list, target); - } - - public static boolean injectionPointNeedsSpecialCare(InjectionPoint injectionPoint) { - return LOCAL_VARIABLE_INJECTION_POINT_CLASS.isInstance(injectionPoint); - } - - public static InjectionInfo createModifyVariableInjectionInfo(IMixinContext mixinContext) throws Exception { - ModifyVariableInjectionInfo info = (ModifyVariableInjectionInfo) UNSAFE.allocateInstance(ModifyVariableInjectionInfo.class); - ANNOTATED_METHOD_INFO_CONTEXT.set(info, mixinContext); - CompatibilityContexts.attach(info, lvtCompatibility(mixinContext)); - return info; - } - - private static int lvtCompatibility(IMixinContext mixinContext) { - IMixinConfig config = mixinContext.getMixin().getConfig(); - return config.hasDecoration(FabricUtil.KEY_COMPATIBILITY) ? config.getDecoration(FabricUtil.KEY_COMPATIBILITY) : FabricUtil.COMPATIBILITY_LATEST; - } - - private static final class CompatibilityContexts { - @Nullable - private static final VarHandle[] HANDLES; - @Nullable - private static final Class[] CLASSES; - private static final Map CONTEXTS = new ConcurrentHashMap<>(); - - static { - VarHandle[] handles = null; - Class[] classes = null; - try { - Class targetContext = Class.forName("org.spongepowered.asm.mixin.transformer.MixinTargetContext"); - Class mixinInfo = Class.forName("org.spongepowered.asm.mixin.transformer.MixinInfo"); - Class mixinConfig = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig"); - handles = new VarHandle[]{ - TRUSTED_LOOKUP.findVarHandle(SpecialMethodInfo.class, "mixin", targetContext), - TRUSTED_LOOKUP.findVarHandle(targetContext, "mixin", mixinInfo), - TRUSTED_LOOKUP.findVarHandle(mixinInfo, "parent", mixinConfig), - TRUSTED_LOOKUP.findVarHandle(mixinConfig, "decorations", Map.class) - }; - classes = new Class[]{targetContext, mixinInfo, mixinConfig}; - } catch (Throwable t) { - LOGGER.debug("Mixin target contexts are unavailable, local variable compatibility uses the selector context", t); - } - HANDLES = handles; - CLASSES = classes; - } - - public static void attach(InjectionInfo info, int lvtCompatibility) { - if (HANDLES != null) { - HANDLES[0].set(info, CONTEXTS.computeIfAbsent(lvtCompatibility, CompatibilityContexts::create)); - } - } - - private static Object create(int lvtCompatibility) { - try { - Object config = UNSAFE.allocateInstance(CLASSES[2]); - HANDLES[3].set(config, new HashMap<>(Map.of(FabricUtil.KEY_COMPATIBILITY, lvtCompatibility))); - Object mixinInfo = UNSAFE.allocateInstance(CLASSES[1]); - HANDLES[2].set(mixinInfo, config); - Object context = UNSAFE.allocateInstance(CLASSES[0]); - HANDLES[1].set(context, mixinInfo); - return context; - } catch (InstantiationException e) { - throw new RuntimeException(e); - } - } - } - - private record MethodSliceContext(IMixinContext context, MethodNode methodNode) implements ISliceContext { - @Override - public IMixinContext getMixin() { - return this.context; - } - - @Override - public String remap(String reference) { - return this.context.getReferenceMapper().remap(this.context.getClassName(), reference); - } - - //@formatter:off - @Override public String getElementDescription() {return "dummy";} - @Override public MethodSlice getSlice(String id) {throw new UnsupportedOperationException();} - @Override public MethodNode getMethod() {return this.methodNode;} - @Override public AnnotationNode getAnnotationNode() {throw new UnsupportedOperationException();} - @Override public ISelectorContext getParent() {throw new UnsupportedOperationException();} - @Override public IAnnotationHandle getAnnotation() {throw new UnsupportedOperationException();} - @Override public IAnnotationHandle getSelectorAnnotation() {throw new UnsupportedOperationException();} - @Override public String getSelectorCoordinate(boolean leaf) {throw new UnsupportedOperationException();} - @Override public void addMessage(String format, Object... args) {} - //@formatter:on - } - - private record ReferenceRemapper(PatchEnvironment env) implements IReferenceMapper { - @Override - public String remapWithContext(String context, String className, String reference) { - return this.env.refmapHolder().remap(className, reference); - } - - //@formatter:off - @Override public boolean isDefault() {return false;} - @Override public String getResourceName() {return null;} - @Override public String getStatus() {return null;} - @Override public String getContext() {return null;} - @Override public void setContext(String context) {} - @Override public String remap(String className, String reference) {return remapWithContext(null, className, reference);} - //@formatter:on - } - - private record DummyMixinConfig(int lvtCompatibility) implements IMixinConfig { - @Override - public MixinEnvironment getEnvironment() { - return MixinEnvironment.getCurrentEnvironment(); - } - - @Override - public boolean hasDecoration(String key) { - return FabricUtil.KEY_COMPATIBILITY.equals(key); - } - - @Override - @SuppressWarnings("unchecked") - public V getDecoration(String key) { - return FabricUtil.KEY_COMPATIBILITY.equals(key) ? (V) (Integer) this.lvtCompatibility : null; - } - - //@formatter:off - @Override public IMixinConfigSource getSource() {throw new UnsupportedOperationException();} - @Override public String getCleanSourceId() {return "dummy";} - @Override public String getName() {throw new UnsupportedOperationException();} - @Override public String getMixinPackage() {throw new UnsupportedOperationException();} - @Override public int getPriority() {return 0;} - @Override public IMixinConfigPlugin getPlugin() {return null;} - @Override public boolean isRequired() {throw new UnsupportedOperationException();} - @Override public Set getTargets() {throw new UnsupportedOperationException();} - @Override public void decorate(String key, V value) {} - //@formatter:on - } - - private record DummyMixinInfo(IMixinConfig config) implements IMixinInfo { - @Override - public IMixinConfig getConfig() { - return config; - } - - //@formatter:off - @Override public String getName() {throw new UnsupportedOperationException();} - @Override public String getClassName() {throw new UnsupportedOperationException();} - @Override public String getClassRef() {throw new UnsupportedOperationException();} - @Override public byte[] getClassBytes() {throw new UnsupportedOperationException();} - @Override public boolean isDetachedSuper() {throw new UnsupportedOperationException();} - @Override public ClassNode getClassNode(int flags) {throw new UnsupportedOperationException();} - @Override public List getTargetClasses() {throw new UnsupportedOperationException();} - @Override public int getPriority() {throw new UnsupportedOperationException();} - @Override public MixinEnvironment.Phase getPhase() {throw new UnsupportedOperationException();} - //@formatter:on - } - - private static final class ClassMixinContext implements IMixinContext { - private final String className; - private final String targetClass; - private final ReferenceRemapper referenceRemapper; - private final IMixinInfo mixinInfo; - - public ClassMixinContext(String className, String targetClass, PatchEnvironment env) { - this.className = className; - this.targetClass = targetClass; - this.referenceRemapper = new ReferenceRemapper(env); - this.mixinInfo = new DummyMixinInfo(new DummyMixinConfig(env.fabricLVTCompatibility())); - } - - @Override - public IMixinInfo getMixin() { - return this.mixinInfo; - } - - @Override - public Extensions getExtensions() { - throw new UnsupportedOperationException(); - } - - @Override - public String getClassName() { - return this.className.replace('/', '.'); - } - - @Override - public String getClassRef() { - return this.className; - } - - @Override - public String getTargetClassName() { - return this.targetClass.replace('/', '.'); - } - - @Override - public String getTargetClassRef() { - return this.targetClass; - } - - @Override - public IReferenceMapper getReferenceMapper() { - return this.referenceRemapper; - } - - @Override - public boolean getOption(MixinEnvironment.Option option) { - return false; - } - - @Override - public int getPriority() { - return 0; - } - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ann/SliceData.java b/core/src/main/java/org/sinytra/adapter/env/ann/SliceData.java deleted file mode 100644 index 98f6dc28..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/ann/SliceData.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.sinytra.adapter.env.ann; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.tree.AnnotationNode; -import org.sinytra.adapter.env.ctx.RefMapper; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.analysis.selector.AnnotationValueHandle; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.SLICE_FROM; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.SLICE_ID; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.SLICE_TO; - -public class SliceData { - @Nullable - private final String id; - @Nullable - private final AtData from; - @Nullable - private final AtData to; - - public SliceData(@Nullable String id, @Nullable AtData from, @Nullable AtData to) { - this.id = id; - this.from = from; - this.to = to; - } - - @Nullable - public String id() { - return this.id; - } - - @Nullable - public AtData from() { - return this.from; - } - - @Nullable - public AtData to() { - return this.to; - } - - public static SliceData parse(AnnotationHandle handle, RefMapper context) { - AtData from = handle.getNested(SLICE_FROM) - .flatMap(s -> AtData.parse(s, context)) - .orElse(null); - AtData to = handle.getNested(SLICE_TO) - .flatMap(s -> AtData.parse(s, context)) - .orElse(null); - String id = handle.getValue(SLICE_ID) - .map(AnnotationValueHandle::get) - .orElse(null); - return new SliceData(id, from, to); - } - - public AnnotationNode toAnnotationNode() { - AnnotationNode slice = new AnnotationNode(MixinAnnotations.SLICE); - if (this.id != null) { - slice.visit(SLICE_ID, this.id); - } - if (this.from != null) { - AnnotationVisitor fromNode = slice.visitAnnotation(SLICE_FROM, MixinAnnotations.AT); - this.from.toAnnotationNode().accept(fromNode); - } - if (this.to != null) { - AnnotationVisitor fromNode = slice.visitAnnotation(SLICE_TO, MixinAnnotations.AT); - this.to.toAnnotationNode().accept(fromNode); - } - return slice; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/MethodFinder.java b/core/src/main/java/org/sinytra/adapter/env/ctx/MethodFinder.java deleted file mode 100644 index ee1bdcd7..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/MethodFinder.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.sinytra.adapter.env.ctx; - -import com.mojang.datafixers.util.Pair; -import com.mojang.logging.LogUtils; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.provider.ClassLookup; -import org.slf4j.Logger; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; - -public class MethodFinder { - private static final Logger LOGGER = LogUtils.getLogger(); - - public static class Flags { - public static final int IGNORE_DESC = 0b01; - public static final int FALLBACK_OWNER = 0b10; - } - - private final PatchEnvironment environment; - @Nullable - private final String fallbackOwner; - - public MethodFinder(PatchEnvironment environment, String fallbackOwner) { - this.environment = environment; - this.fallbackOwner = fallbackOwner; - } - - @Nullable - public TargetPair findInheritedMethod(ClassLookup lookup, MethodQualifier qualifier) { - String owner = qualifier.internalOwnerName(); - if (owner == null || qualifier.name() == null) return null; - - ClassNode node = lookup.getClass(owner).orElse(null); - if (node == null) return null; - - Collection parents = this.environment.inheritanceHandler(lookup).getClassParents(node.name); - - return Stream.concat(Stream.of(node.name), parents.stream()) - .flatMap(cls -> lookup.findMethod(cls, qualifier.name(), qualifier.desc()).stream() - .map(m -> new TargetPair(lookup.getClass(cls).orElseThrow(), m))) - .findFirst() - .orElse(null); - } - - @Nullable - public TargetPair findMethod(ClassLookup lookup, MethodQualifier qualifier, int flags) { - Pair> pair = findMethods(lookup, qualifier, flags); - if (pair == null) { - return null; - } - - if (pair.getSecond().isEmpty()) { - LOGGER.debug("Target method not found: {}{}{}", qualifier.owner(), qualifier.name(), qualifier.desc()); - return null; - } else if (pair.getSecond().size() > 1) { - LOGGER.debug("Multiple candidates found for method: {}{}{}", qualifier.owner(), qualifier.name(), qualifier.desc()); - return null; - } - - return new TargetPair(pair.getFirst(), pair.getSecond().getFirst()); - } - - @Nullable - public Pair> findMethods(ClassLookup lookup, MethodQualifier qualifier, int flags) { - if (qualifier == null || qualifier.name() == null) { - return null; - } - - // Determine target class - String owner; - if (qualifier.internalOwnerName() != null) { - owner = qualifier.internalOwnerName(); - } else if (hasFlag(flags, Flags.FALLBACK_OWNER) && this.fallbackOwner != null) { - owner = this.fallbackOwner; - } else { - return null; - } - - // Find target class - ClassNode targetClass = lookup.getClass(owner).orElse(null); - if (targetClass == null) { - return null; - } - - // Find target method in class - String desc = qualifier.desc(); - List candidates = targetClass.methods.stream() - .filter(mtd -> mtd.name.equals(qualifier.name()) - && (hasFlag(flags, Flags.IGNORE_DESC) || desc == null || mtd.desc.equals(desc))) - .toList(); - - // If there's multiple candidates, try removing bouncer methods - if (candidates.size() > 1 && desc == null) { - candidates = candidates.stream().filter(mtd -> (mtd.access & Opcodes.ACC_SYNTHETIC) == 0 && (mtd.access & Opcodes.ACC_BRIDGE) == 0).toList(); - } - - if (candidates.isEmpty() && targetClass.superName != null) { - return findMethods(lookup, new MethodQualifier(Type.getObjectType(targetClass.superName).getDescriptor(), qualifier.name(), qualifier.desc()), flags); - } - - return Pair.of(targetClass, candidates); - } - - private static boolean hasFlag(int flags, int flag) { - return (flags & flag) != 0; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/MethodHelper.java b/core/src/main/java/org/sinytra/adapter/env/ctx/MethodHelper.java deleted file mode 100644 index 16261ba7..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/MethodHelper.java +++ /dev/null @@ -1,302 +0,0 @@ -package org.sinytra.adapter.env.ctx; - -import com.mojang.datafixers.util.Pair; -import com.mojang.logging.LogUtils; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; -import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.MockMixinRuntime; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.param.ParamDiffResolver; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.provider.ClassLookup; -import org.slf4j.Logger; -import org.spongepowered.asm.mixin.injection.InjectionPoint; -import org.spongepowered.asm.mixin.injection.code.ISliceContext; -import org.spongepowered.asm.mixin.injection.code.MethodSlice; -import org.spongepowered.asm.mixin.injection.struct.Target; -import org.spongepowered.asm.mixin.refmap.IMixinContext; -import org.spongepowered.asm.util.Locals; - -import java.util.*; -import java.util.function.BiFunction; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.*; - -public class MethodHelper { - private static final Logger LOGGER = LogUtils.getLogger(); - private static final Object LOCALS_LOCK = new Object(); - - private final MixinContext context; - private final MethodFinder methodFinder; - - private final Map> targetInstructionsCache = new HashMap<>(); - - private record TargetInsnsKey(@Nullable TargetPair target, boolean ignoreOrdinal, @Nullable Object injectionPoint, @Nullable Object methodAnnotation) { - } - - public MethodHelper(MixinContext context, List targetTypes) { - this.context = context; - - String singleTargetClass = targetTypes.getFirst().getInternalName(); - this.methodFinder = new MethodFinder(context.environment(), singleTargetClass); - } - - @Nullable - public TargetPair findInheritedMethodPair(ClassLookup lookup, MethodQualifier qualifier) { - return this.methodFinder.findInheritedMethod(lookup, qualifier); - } - - @Nullable - public TargetPair findMethodPair(ClassLookup lookup, MethodQualifier qualifier) { - return this.methodFinder.findMethod(lookup, qualifier, 0); - } - - @Nullable - public TargetPair findOwnMethodPair(ClassLookup lookup, MethodQualifier qualifier) { - return this.methodFinder.findMethod(lookup, qualifier, MethodFinder.Flags.FALLBACK_OWNER); - } - - @Nullable - public Targets findTargets(MethodQualifier qualifier) { - TargetPair clean = findOwnMethodPair(this.context.cleanLookup(), qualifier); - TargetPair dirty = findOwnMethodPair(this.context.dirtyLookup(), qualifier); - return clean != null && dirty != null ? new Targets(clean, dirty) : null; - } - - @Nullable - public Pair> findOwnMethodsByName(ClassLookup lookup, MethodQualifier qualifier) { - return this.methodFinder.findMethods(lookup, qualifier, MethodFinder.Flags.FALLBACK_OWNER | MethodFinder.Flags.IGNORE_DESC); - } - - public boolean hasInjectionTargetInsns(@Nullable TargetPair target) { - return !findInjectionTargetInsns(target).isEmpty(); - } - - @Nullable - public AbstractInsnNode findInjectionTargetInsn(@Nullable TargetPair target) { - List cleanInsns = findInjectionTargetInsns(target); - return cleanInsns.size() != 1 ? null : cleanInsns.getFirst(); - } - - public List findInjectionTargetInsns(@Nullable TargetPair target) { - return findInjectionTargetInsns(target, false); - } - - public List findInjectionTargetInsns(@Nullable TargetPair target, boolean ignoreOrdinal) { - TargetInsnsKey key = new TargetInsnsKey(target, ignoreOrdinal, annotationKey(this.context.injectionPointAnnotation()), annotationKey(this.context.methodAnnotation())); - List insns = this.targetInstructionsCache.get(key); - if (insns == null) { - insns = computeInjectionTargetInsns( - target, - this.context::injectionPointAnnotation, - (ctx, h) -> { - AnnotationHandle atAnn = h; - if (ignoreOrdinal) { - atAnn = atAnn.copy(); - atAnn.removeValues(PROPERTY_ORDINAL); - } - - return InjectionPoint.parse(ctx, this.context.methodNode(), this.context.methodAnnotation().unwrap(), atAnn.unwrap()); - }, - true - ); - this.targetInstructionsCache.put(key, insns); - } - return insns; - } - - @Nullable - private static Object annotationKey(@Nullable AnnotationHandle handle) { - return handle != null ? annotationValueKey(handle.unwrap()) : null; - } - - private static Object annotationValueKey(Object value) { - if (value instanceof AnnotationNode node) { - List key = new ArrayList<>(); - key.add(node.desc); - if (node.values != null) { - node.values.forEach(v -> key.add(annotationValueKey(v))); - } - return key; - } - if (value instanceof List list) { - return list.stream().map(MethodHelper::annotationValueKey).toList(); - } - if (value instanceof Object[] array) { - return Arrays.stream(array).map(MethodHelper::annotationValueKey).toList(); - } - return value; - } - - @Nullable - public AbstractInsnNode findInjectionTargetInsn(@Nullable TargetPair target, AtData atData) { - List cleanInsns = computeInjectionTargetInsns( - target, - this.context::injectionPointAnnotation, - (ctx, h) -> { - AnnotationNode atCopy = atData.toAnnotationNode(); - AnnotationHandle annCopy = this.context.methodAnnotation().copy(); - annCopy.setOrAppendNonNull(PROPERTY_AT, atCopy); - - return InjectionPoint.parse(ctx, this.context.methodNode(), annCopy.unwrap(), atCopy); - }, - true - ); - return cleanInsns.size() != 1 ? null : cleanInsns.getFirst(); - } - - public List resolveCapturedMethodParams(Configuration clean, Configuration dirty) { - List cleanCaptured = clean.getParameters().getTypes(CAPTURED_PARAMS); - List dirtyCaptured = new ArrayList<>(); - - // Evaluate parameter difference, capture additional params when necessary - if (!cleanCaptured.isEmpty()) { - TargetPair cleanPair = findOwnMethodPair(this.context.cleanLookup(), clean.getTargetMethod()); - TargetPair dirtyPair = findOwnMethodPair(this.context.dirtyLookup(), dirty.getTargetMethod()); - if (cleanPair == null || dirtyPair == null) return dirtyCaptured; - MethodNode cleanTarget = cleanPair.methodNode(); - MethodNode dirtyTarget = dirtyPair.methodNode(); - - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.compareMethodParameters(cleanTarget, dirtyTarget); - if (diff == null) return dirtyCaptured; - - List cleanTargetParams = Parameters.getParameterTypes(cleanTarget.desc); - // Expect cleanTargetParams to begin with or be equal to cleanCaptured - if (cleanCaptured.size() > cleanTargetParams.size()) return dirtyCaptured; - // Use a sublist instead of cleanCaptured to be able to compare Type instances directly - List capturedSublist = cleanTargetParams.subList(0, cleanCaptured.size()); - if (!cleanCaptured.equals(capturedSublist)) return dirtyCaptured; - - ParamDiffResolver.ParamEvalResult evalResult = ParamDiffResolver.resolve(cleanTargetParams, diff); - - int maxIndex = capturedSublist.stream() - .map(evalResult::getUpdated) - .filter(Objects::nonNull) - .mapToInt(ParamDiffResolver.ParamState::dirtyIndex) - .max() - .orElse(-1); - - if (maxIndex != -1) { - List dirtyTargetParams = Parameters.getParameterTypes(dirtyTarget.desc); - dirtyCaptured = List.copyOf(dirtyTargetParams.subList(0, maxIndex + 1)); - } - } - - return dirtyCaptured; - } - - @Nullable - public List computeInjectionTargetInsns(@Nullable TargetPair target, Supplier atNodeSupplier, BiFunction injectionPointParser, boolean ignoreShift) { - return computeInjectionTargetInsns(target, atNodeSupplier, injectionPointParser, ignoreShift, false); - } - - @Nullable - public List computeInjectionTargetInsns(@Nullable TargetPair target, Supplier atNodeSupplier, BiFunction injectionPointParser, boolean ignoreShift, boolean ignoreSlice) { - if (target == null) { - return List.of(); - } - AnnotationHandle atNode = atNodeSupplier.get(); - if (atNode == null) { - return List.of(); - } - AnnotationHandle atNodeCopy = atNode.copy(); - if (ignoreShift) { - atNodeCopy.removeValues(AT_SHIFT); - } - // Provide a minimum implementation of IMixinContext - IMixinContext mixinContext = MockMixinRuntime.forClass(this.context.classNode().name, target.classNode().name, this.context.environment()); - List targetInsns = new ArrayList<>(); - try { - // Parse injection point - InjectionPoint injectionPoint = injectionPointParser.apply(mixinContext, atNodeCopy); - Target mixinTarget = MockMixinRuntime.createMixinTarget(target); - // Find target instructions - InsnList instructions = ignoreSlice ? target.methodNode().instructions : getSlicedInsns(target, mixinTarget); - if (MockMixinRuntime.injectionPointNeedsSpecialCare(injectionPoint)) { - MockMixinRuntime.findModifyVariableInjectionInsns(injectionPoint, mixinContext, target.methodNode().instructions, targetInsns, mixinTarget); - } else { - injectionPoint.find(target.methodNode().desc, instructions, targetInsns); - } - } catch (Throwable e) { - LOGGER.error("Error finding injection insns", e); - return List.of(); - } - return targetInsns; - } - - private InsnList getSlicedInsns(TargetPair target, Target mixinTarget) { - return this.context.methodAnnotation().getValue(PROPERTY_SLICE) - .map(handle -> handle.get() instanceof List list ? (AnnotationNode) list.getFirst() : (AnnotationNode) handle.get()) - .map(slice -> computeSlice(target, slice, mixinTarget)) - .orElse(target.methodNode().instructions); - } - - public InsnList computeSlice(TargetPair target, AnnotationNode slice) { - return computeSlice(target, slice, MockMixinRuntime.createMixinTarget(target)); - } - - private InsnList computeSlice(TargetPair target, AnnotationNode slice, Target mixinTarget) { - IMixinContext mixinContext = MockMixinRuntime.forClass(this.context.classNode().name, target.classNode().name, this.context.environment()); - ISliceContext sliceContext = MockMixinRuntime.forSlice(mixinContext, this.context.methodNode()); - return MethodSlice.parse(sliceContext, slice).getSlice(mixinTarget); - } - - @Nullable - public List getTargetMethodLocals(TargetPair target) { - Type[] targetParams = Type.getArgumentTypes(target.methodNode().desc); - boolean isStatic = isStatic(target.methodNode()); - int lvtOffset = isStatic ? 0 : 1; - // The starting LVT index is of the first var after all method parameters. Offset by 1 for instance methods to skip 'this' - int targetLocalPos = targetParams.length + lvtOffset; - return getTargetMethodLocals(target, targetLocalPos); - } - - @Nullable - public List getTargetMethodLocals(TargetPair target, int startPos) { - return getTargetMethodLocals(target, startPos, this.context.environment().fabricLVTCompatibility()); - } - - @Nullable - public List getTargetMethodLocals(TargetPair target, int startPos, int lvtCompatLevel) { - List targetInsns = findInjectionTargetInsns(target); - if (targetInsns.isEmpty()) { - LOGGER.debug("Skipping LVT patch, no target instructions found"); - return null; - } - // Get available local variables at the injection point in the target method - LocalVariableNode[] localVariables; - // Synchronize to avoid issues in mixin. This is necessary. - synchronized (LOCALS_LOCK) { - localVariables = Locals.getLocalsAt(target.classNode(), target.methodNode(), targetInsns.getFirst(), lvtCompatLevel); - } - LocalVariable[] locals = Stream.of(localVariables) - .filter(Objects::nonNull) - .map(lv -> new LocalVariable(lv.index, Type.getType(lv.desc))) - .toArray(LocalVariable[]::new); - return AdapterUtil.summariseLocals(locals, startPos); - } - - @Nullable - public LocalVariableLookup getLVT(MethodNode method) { - if (method.localVariables == null) { - return null; - } - return new LocalVariableLookup(method); - } - - public static boolean isStatic(MethodNode node) { - return (node.access & Opcodes.ACC_STATIC) != 0; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchContext.java b/core/src/main/java/org/sinytra/adapter/env/ctx/PatchContext.java deleted file mode 100644 index 293eb053..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sinytra.adapter.env.ctx; - -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; - -import java.util.List; - -public interface PatchContext extends RefMapper { - static PatchContext create(ClassNode classNode, List targetTypes, PatchEnvironment environment) { - return new PatchContextImpl(classNode, targetTypes, environment); - } - - ClassNode classNode(); - - List targetTypes(); - - PatchEnvironment environment(); - - void postApply(Runnable consumer); -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchContextImpl.java b/core/src/main/java/org/sinytra/adapter/env/ctx/PatchContextImpl.java deleted file mode 100644 index a644ef7b..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchContextImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.sinytra.adapter.env.ctx; - -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; - -import java.util.ArrayList; -import java.util.List; - -public class PatchContextImpl implements PatchContext { - private final ClassNode classNode; - private final List targetTypes; - private final PatchEnvironment environment; - private final List postApply = new ArrayList<>(); - - public PatchContextImpl(ClassNode classNode, List targetTypes, PatchEnvironment environment) { - this.classNode = classNode; - this.targetTypes = targetTypes; - this.environment = environment; - } - - @Override - public ClassNode classNode() { - return this.classNode; - } - - @Override - public List targetTypes() { - return this.targetTypes; - } - - @Override - public PatchEnvironment environment() { - return this.environment; - } - - @Override - public String remap(String reference) { - return this.environment.refmapHolder().remap(this.classNode.name, reference); - } - - @Override - public void postApply(Runnable consumer) { - this.postApply.add(consumer); - } - - public int checkpoint() { - return this.postApply.size(); - } - - public void rollback(int checkpoint) { - this.postApply.subList(checkpoint, this.postApply.size()).clear(); - } - - public void run() { - this.postApply.forEach(Runnable::run); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/TargetPair.java b/core/src/main/java/org/sinytra/adapter/env/ctx/TargetPair.java deleted file mode 100644 index 6b1572c3..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/TargetPair.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.sinytra.adapter.env.ctx; - -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; - -public record TargetPair(ClassNode classNode, MethodNode methodNode) { -} diff --git a/core/src/main/java/org/sinytra/adapter/env/param/Parameters.java b/core/src/main/java/org/sinytra/adapter/env/param/Parameters.java deleted file mode 100644 index 246a08f6..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/param/Parameters.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.sinytra.adapter.env.param; - -import com.mojang.datafixers.util.Pair; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.transform.param.ParamTransformationUtil; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public final class Parameters { - public static List parse(MethodNode method) { - return parse(method, -1, -1); - } - - public static List parse(MethodNode method, int start, int end) { - List types = getParameterTypes(method.desc); - List parameters = new ArrayList<>(); - - int first = start == -1 ? 0 : start; - int last = end == -1 ? types.size() : end; - for (int i = first; i < last; i++) { - Type type = types.get(i); - Parameter.Builder builder = Parameter.builder(type); - parseAnnotations(builder, method.invisibleParameterAnnotations, false, i); - parseAnnotations(builder, method.visibleParameterAnnotations, true, i); - Parameter parameter = builder.build(); - - parameters.add(parameter); - } - - return parameters; - } - - private static void parseAnnotations(Parameter.Builder builder, @Nullable List[] source, boolean visible, int index) { - if (source != null && source.length > index) { - List annotations = source[index]; - if (annotations != null) { - for (AnnotationNode node : annotations) { - Annotation parsed = Annotation.parse(node, visible); - builder.annotate(parsed); - } - } - } - } - - /** - * {@return the stack operands of a call: its receiver (if any), followed by its parameters} - */ - public static List getCallOperandTypes(@Nullable Type receiver, String desc) { - List types = getParameterTypes(desc); - if (receiver != null) { - types.addFirst(receiver); - } - return types; - } - - public static List getParameterTypes(String desc) { - return new ArrayList<>(Arrays.asList(Type.getArgumentTypes(desc))); - } - - public static Map> gatherVarMappings(MethodNode method, List cleanParameters, List dirtyParameters, Map replacements, MixinContext context) { - LocalVariableLookup lookup = context.methods().getLVT(method); - Map> map = replacements.entrySet().stream() - .map(entry -> { - int oldIndex = cleanParameters.indexOf(entry.getKey()); - if (oldIndex == -1) return null; - - int newIndex = dirtyParameters.indexOf(entry.getValue()); - if (newIndex == -1) return null; - - LocalVariableNode oldVar = lookup == null ? null : lookup.getByParameterOrdinalOrNull(oldIndex); - if (oldVar == null) return null; - - LocalVariableNode newVar = lookup.getByParameterOrdinalOrNull(newIndex); - if (newVar == null) return null; - - return Pair.of(oldVar.index, Pair.of(newVar.index, entry.getValue().type())); - }) - .filter(Objects::nonNull) - .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond, (first, second) -> first)); - - Map> varMap = new HashMap<>(); - for (AbstractInsnNode insn : method.instructions) { - if (insn instanceof VarInsnNode varInsn && map.containsKey(varInsn.var)) { - varMap.put(varInsn, map.get(varInsn.var)); - } - } - return varMap; - } - - public static void applyVarMappings(MethodNode methodNode, Map> map) { - map.forEach((i, v) -> retypeVariableAndReceivers(methodNode, i, v.getFirst(), v.getSecond().getInternalName())); - } - - public static void applyAnnotations(MethodNode method, List parameters) { - ParamTransformationUtil.updateParameterAnnotationArrays(method, Type.getArgumentCount(method.desc), annotations -> {}); - - for (int i = 0; i < parameters.size(); i++) { - Parameter param = parameters.get(i); - - // TODO Always re-apply all annotations or? - // Remove old annotations - Set descs = param.annotations().stream().map(Annotation::getDesc).collect(Collectors.toSet()); - final int finalI = i; - Stream.of(method.visibleParameterAnnotations, method.invisibleParameterAnnotations) - .filter(arr -> arr != null && finalI < arr.length) - .map(arr -> arr[finalI]) - .filter(Objects::nonNull) - .forEach(list -> list.removeIf(n -> descs.contains(n.desc))); - - for (Annotation annotation : param.annotations()) { - AnnotationVisitor visitor = method.visitParameterAnnotation(i, annotation.getDesc(), annotation.isVisible()); - annotation.accept(visitor); - } - } - } - - public static void retypeVariableAndReceivers(MethodNode methodNode, VarInsnNode targetVarInsn, int newIndex, String newOwner) { - targetVarInsn.var = newIndex; - MethodAnalyzer.forEachReceiver(methodNode, (call, receiver) -> { - if (receiver.contains(targetVarInsn)) { - call.owner = newOwner; - } - }); - } - - private Parameters() { - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/util/MixinUtil.java b/core/src/main/java/org/sinytra/adapter/env/util/MixinUtil.java deleted file mode 100644 index 6659e234..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/util/MixinUtil.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sinytra.adapter.env.util; - -import org.spongepowered.asm.mixin.injection.InjectionPoint; - -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.util.Locale; -import java.util.Map; - -public class MixinUtil { - private static final VarHandle TYPES_HANDLE; - private static final String CLS_REGEX = "^([A-Za-z_][A-Za-z0-9_]*[\\.\\$])+[A-Za-z_][A-Za-z0-9_]*$"; - - static { - try { - TYPES_HANDLE = MethodHandles.privateLookupIn(InjectionPoint.class, MethodHandles.lookup()) - .findStaticVarHandle(InjectionPoint.class, "types", Map.class); - } catch (Exception e) { - throw new RuntimeException("Error unreflecting Injection Point types", e); - } - } - - @SuppressWarnings("unchecked") - public static boolean isKnownInjectionPointType(String value) { - Map> types = (Map>) TYPES_HANDLE.get(); - return types.containsKey(value.toUpperCase(Locale.ROOT)) || value.matches(CLS_REGEX); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/env/util/WeighedDisambiguation.java b/core/src/main/java/org/sinytra/adapter/env/util/WeighedDisambiguation.java deleted file mode 100644 index 91693f3e..00000000 --- a/core/src/main/java/org/sinytra/adapter/env/util/WeighedDisambiguation.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.sinytra.adapter.env.util; - -import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.util.AdapterUtil; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.BiPredicate; -import java.util.function.Supplier; - -public class WeighedDisambiguation { - @Nullable - private final BiPredicate resultsEqual; - private final List>> resultSuppliers; - - private WeighedDisambiguation(BiPredicate resultsEqual, List>> resultSuppliers) { - this.resultsEqual = resultsEqual; - this.resultSuppliers = resultSuppliers; - } - - @Nullable - public R findBestMatch() { - return this.resultSuppliers.stream() - .map(Supplier::get) - .flatMap(r -> disambiguate(r).stream()) - .findFirst() - .orElse(null); - } - - public Optional disambiguate(List results) { - return results.size() == 1 - || !results.isEmpty() && this.resultsEqual != null && AdapterUtil.allElementsEqual(results, this.resultsEqual) - ? Optional.of(results.getFirst()) - : Optional.empty(); - } - - public static Builder builder() { - return new Builder<>(); - } - - public static class Builder { - private final List>> resultSuppliers = new ArrayList<>(); - private BiPredicate resultsEqual; - - public Builder resultsEqual(BiPredicate resultsEqual) { - this.resultsEqual = resultsEqual; - return this; - } - - public Builder match(Supplier> supplier) { - this.resultSuppliers.add(supplier); - return this; - } - - public WeighedDisambiguation build() { - return new WeighedDisambiguation<>(this.resultsEqual, this.resultSuppliers); - } - } -} diff --git a/core/src/main/java/org/sinytra/adapter/util/MethodQualifier.java b/core/src/main/java/org/sinytra/adapter/model/MethodQualifier.java similarity index 75% rename from core/src/main/java/org/sinytra/adapter/util/MethodQualifier.java rename to core/src/main/java/org/sinytra/adapter/model/MethodQualifier.java index 39166f29..d43b5bb9 100644 --- a/core/src/main/java/org/sinytra/adapter/util/MethodQualifier.java +++ b/core/src/main/java/org/sinytra/adapter/model/MethodQualifier.java @@ -1,32 +1,27 @@ -package org.sinytra.adapter.util; +package org.sinytra.adapter.model; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.util.MixinBridge; import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public record MethodQualifier(@Nullable String owner, @Nullable String name, @Nullable String desc) { - public static final Pattern METHOD_QUALIFIER_PATTERN = Pattern.compile("^(?L.+?;)?(?[^(:]+)?(?\\((?:\\[*[ZCBSIFJD]|\\[*L[a-zA-Z0-9/_$]+;)*\\)(?:\\[*[VZCBSIFJD]|\\[?L[a-zA-Z0-9/_;$]+))?$"); public MethodQualifier(@Nullable String name, @Nullable String desc) { this(null, name, desc); } public static Optional parse(String qualifier) { - Matcher matcher = METHOD_QUALIFIER_PATTERN.matcher(qualifier); - if (matcher.matches()) { - String name = matcher.group("name"); - String desc = matcher.group("desc"); - if (name != null || desc != null) { - return Optional.of(new MethodQualifier(matcher.group("owner"), name, desc)); - } + MixinBridge.MemberSelector selector = MixinBridge.parseMemberSelector(qualifier); + if (selector == null || selector.field() || selector.maxMatches() != 1 || selector.name() == null && selector.desc() == null) { + return Optional.empty(); } - return Optional.empty(); + String owner = selector.owner() != null ? Type.getObjectType(selector.owner()).getDescriptor() : null; + return Optional.of(new MethodQualifier(owner, selector.name(), selector.desc())); } public static MethodQualifier create(ClassNode classNode, MethodNode methodNode) { @@ -56,7 +51,7 @@ public boolean matches(MethodInsnNode insn) { public boolean matches(@Nullable String owner, @Nullable String name, @Nullable String desc) { return (this.owner == null || this.owner.equals(owner)) - && this.name != null && this.name.equals(name) + && (this.name != null ? this.name.equals(name) : this.desc != null && name != null && !name.startsWith("<")) && (this.desc == null || this.desc.equals(desc)); } diff --git a/core/src/main/java/org/sinytra/adapter/model/Relocation.java b/core/src/main/java/org/sinytra/adapter/model/Relocation.java new file mode 100644 index 00000000..15182290 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/Relocation.java @@ -0,0 +1,15 @@ +package org.sinytra.adapter.model; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.MethodInsnNode; + +public sealed interface Relocation { + record CalledFrom() implements Relocation { + } + + record ReceivesOrigin(int argIndex, @Nullable OriginCall via) implements Relocation { + } + + record OriginCall(TargetPair method, MethodInsnNode call) { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/model/RelocationPlan.java b/core/src/main/java/org/sinytra/adapter/model/RelocationPlan.java new file mode 100644 index 00000000..9329d49d --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/RelocationPlan.java @@ -0,0 +1,32 @@ +package org.sinytra.adapter.model; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.List; + +public sealed interface RelocationPlan { + record Move(List lambdas, List widenedFields, List widenedMethods, boolean makeStatic) implements RelocationPlan { + } + + record Mirror(ClassNode handlerOwner, boolean isStatic, @Nullable ValueSource receiver, List params, @Nullable Guard guard) implements RelocationPlan { + } + + /// Limits a mirror to calls that come from the original method, through the given call + record Guard(Relocation.OriginCall via, Passthrough passthrough) { + } + + /// What a guarded mirror returns when it doesn't run the handler + enum Passthrough { + /// Nothing, the handler returns void + SKIP, + /// The value the handler would modify, its first parameter + KEEP_VALUE, + /// True, so the wrapped instruction still runs + PROCEED, + /// The result of the wrapped operation, called with the operands the handler received before it + CALL_ORIGINAL + } +} diff --git a/core/src/main/java/org/sinytra/adapter/model/TargetPair.java b/core/src/main/java/org/sinytra/adapter/model/TargetPair.java new file mode 100644 index 00000000..c08c0198 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/TargetPair.java @@ -0,0 +1,12 @@ +package org.sinytra.adapter.model; + +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +public record TargetPair(ClassNode classNode, MethodNode methodNode) { + /// {@return whether both name the same method of the same class, which may come from different class lookups} + public boolean isSameMethod(TargetPair other) { + return this.classNode.name.equals(other.classNode.name) && this.methodNode.name.equals(other.methodNode.name) + && this.methodNode.desc.equals(other.methodNode.desc); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/Targets.java b/core/src/main/java/org/sinytra/adapter/model/Targets.java similarity index 63% rename from core/src/main/java/org/sinytra/adapter/env/ctx/Targets.java rename to core/src/main/java/org/sinytra/adapter/model/Targets.java index 9a9206ed..023c4bd7 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/Targets.java +++ b/core/src/main/java/org/sinytra/adapter/model/Targets.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.model; public record Targets(TargetPair clean, TargetPair dirty) { } diff --git a/core/src/main/java/org/sinytra/adapter/model/ValueSource.java b/core/src/main/java/org/sinytra/adapter/model/ValueSource.java new file mode 100644 index 00000000..e1283e84 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/ValueSource.java @@ -0,0 +1,11 @@ +package org.sinytra.adapter.model; + +import org.objectweb.asm.Type; + +public sealed interface ValueSource { + record Forwarded(int param) implements ValueSource { + } + + record Captured(Type type, int slot, boolean argsOnly) implements ValueSource { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/ann/AnnotationData.java b/core/src/main/java/org/sinytra/adapter/model/ann/AnnotationData.java similarity index 88% rename from core/src/main/java/org/sinytra/adapter/env/ann/AnnotationData.java rename to core/src/main/java/org/sinytra/adapter/model/ann/AnnotationData.java index 82e5f510..d9f2aef1 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ann/AnnotationData.java +++ b/core/src/main/java/org/sinytra/adapter/model/ann/AnnotationData.java @@ -1,9 +1,9 @@ -package org.sinytra.adapter.env.ann; +package org.sinytra.adapter.model.ann; import com.google.common.base.MoreObjects; import org.objectweb.asm.tree.AnnotationNode; -import org.sinytra.adapter.patch.config.PropertyContainer; -import org.sinytra.adapter.patch.config.PropertyKey; +import org.sinytra.adapter.model.config.PropertyContainer; +import org.sinytra.adapter.model.config.PropertyKey; import java.util.Objects; import java.util.Optional; diff --git a/core/src/main/java/org/sinytra/adapter/analysis/selector/AnnotationHandle.java b/core/src/main/java/org/sinytra/adapter/model/ann/AnnotationHandle.java similarity index 87% rename from core/src/main/java/org/sinytra/adapter/analysis/selector/AnnotationHandle.java rename to core/src/main/java/org/sinytra/adapter/model/ann/AnnotationHandle.java index 65b9d9be..786d3651 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/selector/AnnotationHandle.java +++ b/core/src/main/java/org/sinytra/adapter/model/ann/AnnotationHandle.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.analysis.selector; +package org.sinytra.adapter.model.ann; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.tree.AnnotationNode; @@ -32,16 +32,6 @@ public AnnotationHandle copy() { return new AnnotationHandle(copy); } - public AnnotationHandle getNestedOrAppend(String key, String desc) { - AnnotationHandle existing = getNested(key).orElse(null); - if (existing != null) { - return existing; - } - AnnotationNode node = new AnnotationNode(desc); - appendValue(key, node); - return getNested(key).orElseThrow(); - } - public Optional getNested(String key) { return getValue(key) .flatMap(AnnotationValueHandle::maybeUnwrap) diff --git a/core/src/main/java/org/sinytra/adapter/analysis/selector/AnnotationValueHandle.java b/core/src/main/java/org/sinytra/adapter/model/ann/AnnotationValueHandle.java similarity index 70% rename from core/src/main/java/org/sinytra/adapter/analysis/selector/AnnotationValueHandle.java rename to core/src/main/java/org/sinytra/adapter/model/ann/AnnotationValueHandle.java index ef1b0710..b53c0ef7 100644 --- a/core/src/main/java/org/sinytra/adapter/analysis/selector/AnnotationValueHandle.java +++ b/core/src/main/java/org/sinytra/adapter/model/ann/AnnotationValueHandle.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.analysis.selector; +package org.sinytra.adapter.model.ann; import org.objectweb.asm.tree.AnnotationNode; @@ -62,25 +62,6 @@ public void set(T value) { this.origin.set(index, value); } - public Optional findNested(String name) { - assertValid(); - Object value = unwrap(); - if (value instanceof AnnotationNode annotationNode) { - return create(annotationNode, name).map(h -> h.unwrap() instanceof AnnotationNode ann ? new AnnotationHandle(ann) : null); - } - throw new IllegalArgumentException("Expected value to be an AnnotationNode, was " + value.getClass()); - } - - public void refresh(AnnotationNode annotationNode) { - AnnotationValueHandle.create(annotationNode, this.key).ifPresentOrElse( - v -> { - this.origin = v.origin; - this.index = v.index; - }, - () -> this.invalid = true - ); - } - private void assertValid() { if (this.invalid) { throw new RuntimeException("Annotation value handle for " + this.key + " is no longer valid."); diff --git a/core/src/main/java/org/sinytra/adapter/env/ann/AtData.java b/core/src/main/java/org/sinytra/adapter/model/ann/AtData.java similarity index 88% rename from core/src/main/java/org/sinytra/adapter/env/ann/AtData.java rename to core/src/main/java/org/sinytra/adapter/model/ann/AtData.java index ddbf6f6c..a98e15f3 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ann/AtData.java +++ b/core/src/main/java/org/sinytra/adapter/model/ann/AtData.java @@ -1,16 +1,14 @@ -package org.sinytra.adapter.env.ann; +package org.sinytra.adapter.model.ann; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.tree.MethodInsnNode; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ctx.RefMapper; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.env.util.MixinUtil; -import org.sinytra.adapter.patch.config.MutablePropertyContainer; -import org.sinytra.adapter.patch.config.PropertyContainer; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.config.PropertyKey; -import org.sinytra.adapter.util.MethodQualifier; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.config.MutablePropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.PropertyKey; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.util.MixinBridge; import org.spongepowered.asm.mixin.injection.At; import java.util.List; @@ -100,7 +98,7 @@ public static Builder builder(String value) { private static boolean validateInjectionPointType(PropertyContainer container) { return container.getProperty(Keys.VALUE) - .map(MixinUtil::isKnownInjectionPointType) + .map(MixinBridge::isKnownInjectionPointType) .orElse(true); } diff --git a/core/src/main/java/org/sinytra/adapter/env/ann/ClassTarget.java b/core/src/main/java/org/sinytra/adapter/model/ann/ClassTarget.java similarity index 71% rename from core/src/main/java/org/sinytra/adapter/env/ann/ClassTarget.java rename to core/src/main/java/org/sinytra/adapter/model/ann/ClassTarget.java index edf6ba7f..975346ca 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ann/ClassTarget.java +++ b/core/src/main/java/org/sinytra/adapter/model/ann/ClassTarget.java @@ -1,17 +1,14 @@ -package org.sinytra.adapter.env.ann; +package org.sinytra.adapter.model.ann; import com.google.common.collect.ImmutableList; import com.mojang.datafixers.util.Either; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.analysis.selector.AnnotationValueHandle; -import org.sinytra.adapter.env.ctx.RefMapper; import java.util.List; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.MIXIN_TARGETS; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.MIXIN_VALUE; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.MIXIN_TARGETS; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.MIXIN_VALUE; public class ClassTarget { private List types; @@ -26,11 +23,6 @@ public List getTypes() { return this.types; } - @Nullable - public Type getSingle() { - return this.types.size() != 1 ? null : this.types.getFirst(); - } - public void set(Type type) { this.types = List.of(type); this.either.ifLeft(h -> h.set(List.of(type))) @@ -44,7 +36,7 @@ public static ClassTarget parse(AnnotationHandle handle, RefMapper mapper) { .or(() -> handle.>getValue(MIXIN_TARGETS).map(v -> { List types = v.get().stream() .map(mapper::remap) - .map(Type::getObjectType) + .map(name -> Type.getObjectType(name.replace('.', '/'))) .toList(); return new ClassTarget(types, Either.right(v)); })) diff --git a/core/src/main/java/org/sinytra/adapter/env/ann/ConstantData.java b/core/src/main/java/org/sinytra/adapter/model/ann/ConstantData.java similarity index 51% rename from core/src/main/java/org/sinytra/adapter/env/ann/ConstantData.java rename to core/src/main/java/org/sinytra/adapter/model/ann/ConstantData.java index 9a7b9a65..d8c6c355 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ann/ConstantData.java +++ b/core/src/main/java/org/sinytra/adapter/model/ann/ConstantData.java @@ -1,18 +1,18 @@ -package org.sinytra.adapter.env.ann; +package org.sinytra.adapter.model.ann; +import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.config.MutablePropertyContainer; -import org.sinytra.adapter.patch.config.PropertyContainer; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.config.PropertyKey; +import org.sinytra.adapter.model.config.MutablePropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.PropertyKey; +import org.sinytra.adapter.model.constant.MixinAnnotations; import java.util.Optional; public class ConstantData extends AnnotationData { private static final PropertyContainerTemplate TEMPLATE = PropertyContainerTemplate.builder() - .requireOne(Keys.DOUBLE_VALUE, Keys.CLASS_VALUE) + .keys(Keys.DOUBLE_VALUE, Keys.CLASS_VALUE, Keys.ORDINAL, Keys.SLICE) .build(); private ConstantData(PropertyContainer properties) { @@ -23,6 +23,20 @@ public Optional doubleValue() { return getProperty(Keys.DOUBLE_VALUE); } + public Optional getOrdinal() { + return getProperty(Keys.ORDINAL); + } + + public ConstantData withOrdinal(@Nullable Integer ordinal) { + return withProperty(Keys.ORDINAL, ordinal); + } + + public ConstantData withProperty(PropertyKey key, @Nullable T value) { + MutablePropertyContainer copy = this.properties.copy(); + copy.setProperty(key, value); + return new ConstantData(copy); + } + public static ConstantData classValue(Type value) { PropertyContainer container = MutablePropertyContainer.create().setProperty(Keys.CLASS_VALUE, value); return new ConstantData(container); @@ -36,5 +50,7 @@ public static Optional parse(AnnotationHandle handle) { public static class Keys { public static final PropertyKey DOUBLE_VALUE = PropertyKey.create("doubleValue", Double.class); public static final PropertyKey CLASS_VALUE = PropertyKey.create("classValue", Type.class); + public static final PropertyKey ORDINAL = PropertyKey.create("ordinal", Integer.class); + public static final PropertyKey SLICE = PropertyKey.create("slice", String.class); } } diff --git a/core/src/main/java/org/sinytra/adapter/model/ann/ExpressionData.java b/core/src/main/java/org/sinytra/adapter/model/ann/ExpressionData.java new file mode 100644 index 00000000..a8fbeeec --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/ann/ExpressionData.java @@ -0,0 +1,38 @@ +package org.sinytra.adapter.model.ann; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.sinytra.adapter.model.constant.MixinAnnotations; + +import java.util.List; + +public record ExpressionData(String expression, List definitions) { + public record TypeDefinition(String id, Type type) { + } + + public List toAnnotationNodes() { + AnnotationNode expression = new AnnotationNode(MixinAnnotations.EXPRESSION); + expression.visit("value", List.of(this.expression)); + List definitions = this.definitions.stream() + .map(definition -> { + AnnotationNode node = new AnnotationNode(MixinAnnotations.DEFINITION); + node.visit("id", definition.id()); + node.visit("type", List.of(definition.type())); + return node; + }) + .toList(); + if (definitions.size() == 1) { + return List.of(definitions.getFirst(), expression); + } + AnnotationNode container = new AnnotationNode(MixinAnnotations.DEFINITIONS); + container.visit("value", definitions); + return List.of(container, expression); + } + + public static boolean isExpressionAnnotation(AnnotationNode annotation) { + return switch (annotation.desc) { + case MixinAnnotations.EXPRESSION, MixinAnnotations.EXPRESSIONS, MixinAnnotations.DEFINITION, MixinAnnotations.DEFINITIONS -> true; + default -> false; + }; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/RefMapper.java b/core/src/main/java/org/sinytra/adapter/model/ann/RefMapper.java similarity index 64% rename from core/src/main/java/org/sinytra/adapter/env/ctx/RefMapper.java rename to core/src/main/java/org/sinytra/adapter/model/ann/RefMapper.java index 0c6f443f..2ea071db 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/RefMapper.java +++ b/core/src/main/java/org/sinytra/adapter/model/ann/RefMapper.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.model.ann; public interface RefMapper { String remap(String refmapEntry); diff --git a/core/src/main/java/org/sinytra/adapter/model/ann/SliceData.java b/core/src/main/java/org/sinytra/adapter/model/ann/SliceData.java new file mode 100644 index 00000000..bf97210c --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/ann/SliceData.java @@ -0,0 +1,56 @@ +package org.sinytra.adapter.model.ann; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AnnotationNode; +import org.sinytra.adapter.model.config.MutablePropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.PropertyKey; +import org.sinytra.adapter.model.constant.MixinAnnotations; + +public class SliceData extends AnnotationData { + private static final PropertyContainerTemplate TEMPLATE = PropertyContainerTemplate.builder() + .keys(Keys.FROM, Keys.TO) + .build(); + + private SliceData(PropertyContainer properties) { + super(MixinAnnotations.SLICE, properties); + } + + @Nullable + public AtData from() { + return getProperty(Keys.FROM).orElse(null); + } + + @Nullable + public AtData to() { + return getProperty(Keys.TO).orElse(null); + } + + public SliceData withBoundaries(@Nullable AtData from, @Nullable AtData to) { + MutablePropertyContainer copy = this.properties.copy(); + copy.setProperty(Keys.FROM, from); + copy.setProperty(Keys.TO, to); + return new SliceData(copy); + } + + public static SliceData empty() { + return new SliceData(MutablePropertyContainer.create(TEMPLATE)); + } + + public static SliceData parse(AnnotationHandle handle, RefMapper mapper) { + return new SliceData(MutablePropertyContainer.parse(handle, TEMPLATE, mapper)); + } + + public static class Keys { + public static final PropertyKey FROM = boundary("from"); + public static final PropertyKey TO = boundary("to"); + + private static PropertyKey boundary(String name) { + return PropertyKey.builder(name) + .parser((value, mapper) -> AtData.parse(new AnnotationHandle((AnnotationNode) value), mapper).orElse(null)) + .serializer(AtData::toAnnotationNode) + .build(); + } + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/BasePropertyContainer.java b/core/src/main/java/org/sinytra/adapter/model/config/BasePropertyContainer.java similarity index 78% rename from core/src/main/java/org/sinytra/adapter/patch/config/BasePropertyContainer.java rename to core/src/main/java/org/sinytra/adapter/model/config/BasePropertyContainer.java index 51f1af58..2b7d8fe4 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/BasePropertyContainer.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/BasePropertyContainer.java @@ -1,16 +1,17 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; import com.google.common.collect.ImmutableMap; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.analysis.selector.AnnotationValueHandle; -import org.sinytra.adapter.env.ctx.RefMapper; import org.objectweb.asm.tree.AnnotationNode; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.AnnotationValueHandle; +import org.sinytra.adapter.model.ann.RefMapper; import java.util.*; public class BasePropertyContainer implements MutablePropertyContainer { private final Map, Object> properties = new HashMap<>(); + private final List unknownValues = new ArrayList<>(); @Nullable protected final PropertyContainerTemplate template; @@ -61,6 +62,9 @@ public Map, Object> getProperties() { public MutablePropertyContainer copy() { MutablePropertyContainer copy = createCopyImpl(); this.properties.forEach((p, v) -> copy.setProperty((PropertyKey) p, v)); + if (copy instanceof BasePropertyContainer base) { + base.unknownValues.addAll(this.unknownValues); + } return copy; } @@ -91,6 +95,7 @@ public Object serialize(PropertyKey key, T value) { public AnnotationNode toAnnotationNode(String desc) { AnnotationNode node = new AnnotationNode(desc); this.properties.forEach((key, value) -> node.visit(key.name(), serialize((PropertyKey) key, value))); + this.unknownValues.forEach(value -> node.visit(value.name(), value.value())); return node; } @@ -98,12 +103,16 @@ public AnnotationNode toAnnotationNode(String desc) { public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; BasePropertyContainer container = (BasePropertyContainer) o; - return Objects.equals(properties, container.properties) && Objects.equals(template, container.template); + return Objects.equals(properties, container.properties) && Objects.equals(unknownValues, container.unknownValues) + && Objects.equals(template, container.template); } @Override public int hashCode() { - return Objects.hash(properties, template); + return Objects.hash(properties, unknownValues, template); + } + + private record UnknownValue(String name, Object value) { } protected MutablePropertyContainer createCopyImpl() { @@ -117,9 +126,18 @@ public static MutablePropertyContainer parseValid(AnnotationHandle handle, @Null @SuppressWarnings({"rawtypes", "unchecked"}) public static MutablePropertyContainer parse(AnnotationHandle handle, @Nullable PropertyContainerTemplate template, RefMapper mapper) { - MutablePropertyContainer container = new BasePropertyContainer(template); + BasePropertyContainer container = new BasePropertyContainer(template); if (template == null) return container; + Set known = new HashSet<>(); + template.getKeys().forEach(key -> known.add(key.name())); + List values = Objects.requireNonNullElse(handle.unwrap().values, List.of()); + for (int i = 0; i + 1 < values.size(); i += 2) { + if (!known.contains((String) values.get(i))) { + container.unknownValues.add(new UnknownValue((String) values.get(i), values.get(i + 1))); + } + } + for (PropertyKey key : template.getKeys()) { Object value = handle.getValue(key.name()).map(AnnotationValueHandle::get).orElse(null); if (value == null) continue; diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/Configuration.java b/core/src/main/java/org/sinytra/adapter/model/config/Configuration.java similarity index 80% rename from core/src/main/java/org/sinytra/adapter/patch/config/Configuration.java rename to core/src/main/java/org/sinytra/adapter/model/config/Configuration.java index ffe2f240..a47f7e1c 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/Configuration.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/Configuration.java @@ -1,12 +1,12 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.patch.config.key.ControlKeys; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.util.MethodQualifier; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.MethodParameters; public interface Configuration extends PropertyContainer { @Nullable diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/ConfigurationImpl.java b/core/src/main/java/org/sinytra/adapter/model/config/ConfigurationImpl.java similarity index 96% rename from core/src/main/java/org/sinytra/adapter/patch/config/ConfigurationImpl.java rename to core/src/main/java/org/sinytra/adapter/model/config/ConfigurationImpl.java index aa92799f..cb631024 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/ConfigurationImpl.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/ConfigurationImpl.java @@ -1,7 +1,7 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.env.param.Copiable; +import org.sinytra.adapter.model.param.Copiable; /** * Contains a single recipe state with mixin parameters and custom variables diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/ConfigurationTemplates.java b/core/src/main/java/org/sinytra/adapter/model/config/ConfigurationTemplates.java similarity index 81% rename from core/src/main/java/org/sinytra/adapter/patch/config/ConfigurationTemplates.java rename to core/src/main/java/org/sinytra/adapter/model/config/ConfigurationTemplates.java index 370d7b8e..e752530a 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/ConfigurationTemplates.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/ConfigurationTemplates.java @@ -1,7 +1,7 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; -import org.sinytra.adapter.patch.config.key.ControlKeys; -import org.sinytra.adapter.patch.config.key.MixinKeys; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; public final class ConfigurationTemplates { public static final PropertyContainerTemplate MIXIN_BASE = PropertyContainerTemplate.builder() diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/Configurations.java b/core/src/main/java/org/sinytra/adapter/model/config/Configurations.java similarity index 84% rename from core/src/main/java/org/sinytra/adapter/patch/config/Configurations.java rename to core/src/main/java/org/sinytra/adapter/model/config/Configurations.java index 7dd28cc6..3f9fa202 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/Configurations.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/Configurations.java @@ -1,6 +1,6 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; -import org.sinytra.adapter.env.ann.AtData; +import org.sinytra.adapter.model.ann.AtData; public class Configurations { public static final Configuration DELETE = MutableConfiguration.create(ConfigurationTemplates.DELETE) diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/MutableConfiguration.java b/core/src/main/java/org/sinytra/adapter/model/config/MutableConfiguration.java similarity index 90% rename from core/src/main/java/org/sinytra/adapter/patch/config/MutableConfiguration.java rename to core/src/main/java/org/sinytra/adapter/model/config/MutableConfiguration.java index 831279e5..fa9fb3c6 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/MutableConfiguration.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/MutableConfiguration.java @@ -1,15 +1,15 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.patch.config.key.ControlKeys; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.util.MethodQualifier; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.MethodParameters; public interface MutableConfiguration extends Configuration, MutablePropertyContainer { static MutableConfiguration create() { diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/MutablePropertyContainer.java b/core/src/main/java/org/sinytra/adapter/model/config/MutablePropertyContainer.java similarity index 87% rename from core/src/main/java/org/sinytra/adapter/patch/config/MutablePropertyContainer.java rename to core/src/main/java/org/sinytra/adapter/model/config/MutablePropertyContainer.java index 511ebbca..579efd51 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/MutablePropertyContainer.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/MutablePropertyContainer.java @@ -1,8 +1,8 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.env.ctx.RefMapper; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.RefMapper; public interface MutablePropertyContainer extends PropertyContainer { static MutablePropertyContainer create() { diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/PropertyContainer.java b/core/src/main/java/org/sinytra/adapter/model/config/PropertyContainer.java similarity index 82% rename from core/src/main/java/org/sinytra/adapter/patch/config/PropertyContainer.java rename to core/src/main/java/org/sinytra/adapter/model/config/PropertyContainer.java index 95e4021b..dbba07ea 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/PropertyContainer.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/PropertyContainer.java @@ -1,7 +1,7 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; import org.objectweb.asm.tree.AnnotationNode; +import org.sinytra.adapter.model.ann.AnnotationHandle; import java.util.Map; import java.util.Optional; diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/PropertyContainerTemplate.java b/core/src/main/java/org/sinytra/adapter/model/config/PropertyContainerTemplate.java similarity index 98% rename from core/src/main/java/org/sinytra/adapter/patch/config/PropertyContainerTemplate.java rename to core/src/main/java/org/sinytra/adapter/model/config/PropertyContainerTemplate.java index dbc73dc3..4989c6f7 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/PropertyContainerTemplate.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/PropertyContainerTemplate.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/PropertyKey.java b/core/src/main/java/org/sinytra/adapter/model/config/PropertyKey.java similarity index 97% rename from core/src/main/java/org/sinytra/adapter/patch/config/PropertyKey.java rename to core/src/main/java/org/sinytra/adapter/model/config/PropertyKey.java index 88078c56..0c53a2f7 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/PropertyKey.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/PropertyKey.java @@ -1,8 +1,8 @@ -package org.sinytra.adapter.patch.config; +package org.sinytra.adapter.model.config; import com.google.common.base.MoreObjects; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.env.ctx.RefMapper; +import org.sinytra.adapter.model.ann.RefMapper; import java.util.Objects; diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/key/ControlKeys.java b/core/src/main/java/org/sinytra/adapter/model/config/key/ControlKeys.java similarity index 81% rename from core/src/main/java/org/sinytra/adapter/patch/config/key/ControlKeys.java rename to core/src/main/java/org/sinytra/adapter/model/config/key/ControlKeys.java index d0969116..d6e74db5 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/key/ControlKeys.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/key/ControlKeys.java @@ -1,8 +1,8 @@ -package org.sinytra.adapter.patch.config.key; +package org.sinytra.adapter.model.config.key; import org.objectweb.asm.Type; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.patch.config.PropertyKey; +import org.sinytra.adapter.model.config.PropertyKey; +import org.sinytra.adapter.model.param.MethodParameters; import java.util.List; @@ -17,6 +17,7 @@ public final class ControlKeys { // Method public static final PropertyKey PARAMETERS = PropertyKey.create("parameters"); public static final PropertyKey RETURN_TYPE = PropertyKey.create("return_type"); + public static final PropertyKey STATIC = PropertyKey.create("static"); // Control public static final PropertyKey DELETE = PropertyKey.create("delete"); diff --git a/core/src/main/java/org/sinytra/adapter/model/config/key/IntentKeys.java b/core/src/main/java/org/sinytra/adapter/model/config/key/IntentKeys.java new file mode 100644 index 00000000..da866c29 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/config/key/IntentKeys.java @@ -0,0 +1,15 @@ +package org.sinytra.adapter.model.config.key; + +import org.sinytra.adapter.model.Relocation; +import org.sinytra.adapter.model.ann.ExpressionData; +import org.sinytra.adapter.model.config.PropertyKey; + +public final class IntentKeys { + /// The mixin moves to the dirty target class + public static final PropertyKey RELOCATION = PropertyKey.create("relocation"); + + public static final PropertyKey EXPRESSION = PropertyKey.create("expression"); + + private IntentKeys() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/key/MixinKeys.java b/core/src/main/java/org/sinytra/adapter/model/config/key/MixinKeys.java similarity index 87% rename from core/src/main/java/org/sinytra/adapter/patch/config/key/MixinKeys.java rename to core/src/main/java/org/sinytra/adapter/model/config/key/MixinKeys.java index 1bd47447..3e698b96 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/config/key/MixinKeys.java +++ b/core/src/main/java/org/sinytra/adapter/model/config/key/MixinKeys.java @@ -1,12 +1,12 @@ -package org.sinytra.adapter.patch.config.key; +package org.sinytra.adapter.model.config.key; import org.objectweb.asm.tree.AnnotationNode; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ann.ConstantData; -import org.sinytra.adapter.env.ann.SliceData; -import org.sinytra.adapter.patch.config.PropertyKey; -import org.sinytra.adapter.util.MethodQualifier; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.ann.ConstantData; +import org.sinytra.adapter.model.ann.SliceData; +import org.sinytra.adapter.model.config.PropertyKey; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; import java.util.List; @@ -21,7 +21,7 @@ public class MixinKeys { // Get method targets String methodRef = ControlKeys.parseSingle(value, String.class); if (methodRef == null) { - // We only support single method targets for now + // Several targets are split into one handler each by MultiTargetSplitter return null; } // Resolve method reference diff --git a/core/src/main/java/org/sinytra/adapter/env/util/MixinAnnotationConstants.java b/core/src/main/java/org/sinytra/adapter/model/constant/MixinAnnotationConstants.java similarity index 74% rename from core/src/main/java/org/sinytra/adapter/env/util/MixinAnnotationConstants.java rename to core/src/main/java/org/sinytra/adapter/model/constant/MixinAnnotationConstants.java index ae6fc8b1..30d88b8a 100644 --- a/core/src/main/java/org/sinytra/adapter/env/util/MixinAnnotationConstants.java +++ b/core/src/main/java/org/sinytra/adapter/model/constant/MixinAnnotationConstants.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.util; +package org.sinytra.adapter.model.constant; public class MixinAnnotationConstants { public static final String MIXIN_VALUE = "value"; @@ -8,9 +8,11 @@ public class MixinAnnotationConstants { public static final String AT_VAL_INVOKE = "INVOKE"; public static final String AT_VAL_INVOKE_ASSIGN = "INVOKE_ASSIGN"; public static final String AT_VAL_RETURN = "RETURN"; + public static final String AT_VAL_NEW = "NEW"; public static final String AT_VAL_STORE = "STORE"; + public static final String AT_VAL_LOAD = "LOAD"; public static final String AT_VAL_CONST = "CONSTANT"; - public static final String AT_VAL_SINYTRA_INSTANCEOF = "sinytra:INSTANCEOF"; + public static final String AT_VAL_EXPRESSION = "MIXINEXTRAS:EXPRESSION"; public static final String AT_SHIFT = "shift"; public static final String PROPERTY_AT = "at"; @@ -18,8 +20,5 @@ public class MixinAnnotationConstants { public static final String PROPERTY_ORDINAL = "ordinal"; public static final String PROPERTY_SLICE = "slice"; public static final String PROPERTY_INDEX = "index"; - - public static final String SLICE_FROM = "from"; - public static final String SLICE_TO = "to"; - public static final String SLICE_ID = "id"; + public static final String PROPERTY_CONSTANT = "constant"; } diff --git a/core/src/main/java/org/sinytra/adapter/env/util/MixinAnnotations.java b/core/src/main/java/org/sinytra/adapter/model/constant/MixinAnnotations.java similarity index 78% rename from core/src/main/java/org/sinytra/adapter/env/util/MixinAnnotations.java rename to core/src/main/java/org/sinytra/adapter/model/constant/MixinAnnotations.java index 0f101219..95b2be18 100644 --- a/core/src/main/java/org/sinytra/adapter/env/util/MixinAnnotations.java +++ b/core/src/main/java/org/sinytra/adapter/model/constant/MixinAnnotations.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.util; +package org.sinytra.adapter.model.constant; public class MixinAnnotations { // Standard mixins @@ -20,10 +20,14 @@ public class MixinAnnotations { public static final String WRAP_OPERATION = "Lcom/llamalad7/mixinextras/injector/wrapoperation/WrapOperation;"; public static final String WRAP_OPERATION_INTERNAL_NAME = "com/llamalad7/mixinextras/injector/wrapoperation/WrapOperation"; public static final String WRAP_WITH_CONDITION = "Lcom/llamalad7/mixinextras/injector/WrapWithCondition;"; + public static final String WRAP_WITH_CONDITION_INTERNAL_NAME = "com/llamalad7/mixinextras/injector/WrapWithCondition"; + public static final String WRAP_WITH_CONDITION_V2_INTERNAL_NAME = "com/llamalad7/mixinextras/injector/v2/WrapWithCondition"; public static final String LOCAL = "Lcom/llamalad7/mixinextras/sugar/Local;"; public static final String SHARE = "Lcom/llamalad7/mixinextras/sugar/Share;"; - // Adapter custom mixin injectors - public static final String MODIFY_INSTANCEOF_VAL = "Lorg/sinytra/adapter/runtime/inject/ModifyInstanceofValue;"; + public static final String EXPRESSION = "Lcom/llamalad7/mixinextras/expression/Expression;"; + public static final String EXPRESSIONS = "Lcom/llamalad7/mixinextras/expression/Expressions;"; + public static final String DEFINITION = "Lcom/llamalad7/mixinextras/expression/Definition;"; + public static final String DEFINITIONS = "Lcom/llamalad7/mixinextras/expression/Definitions;"; // Misc public static final String MIXIN = "Lorg/spongepowered/asm/mixin/Mixin;"; public static final String AT = "Lorg/spongepowered/asm/mixin/injection/At;"; diff --git a/core/src/main/java/org/sinytra/adapter/env/util/TypeConstants.java b/core/src/main/java/org/sinytra/adapter/model/constant/TypeConstants.java similarity index 95% rename from core/src/main/java/org/sinytra/adapter/env/util/TypeConstants.java rename to core/src/main/java/org/sinytra/adapter/model/constant/TypeConstants.java index 67931554..f443cb71 100644 --- a/core/src/main/java/org/sinytra/adapter/env/util/TypeConstants.java +++ b/core/src/main/java/org/sinytra/adapter/model/constant/TypeConstants.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.util; +package org.sinytra.adapter.model.constant; import org.objectweb.asm.Type; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; diff --git a/core/src/main/java/org/sinytra/adapter/env/param/Annotation.java b/core/src/main/java/org/sinytra/adapter/model/param/Annotation.java similarity index 98% rename from core/src/main/java/org/sinytra/adapter/env/param/Annotation.java rename to core/src/main/java/org/sinytra/adapter/model/param/Annotation.java index bd376cfb..862aaf49 100644 --- a/core/src/main/java/org/sinytra/adapter/env/param/Annotation.java +++ b/core/src/main/java/org/sinytra/adapter/model/param/Annotation.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.param; +package org.sinytra.adapter.model.param; import com.google.common.collect.ImmutableMap; import org.objectweb.asm.AnnotationVisitor; diff --git a/core/src/main/java/org/sinytra/adapter/env/param/Copiable.java b/core/src/main/java/org/sinytra/adapter/model/param/Copiable.java similarity index 53% rename from core/src/main/java/org/sinytra/adapter/env/param/Copiable.java rename to core/src/main/java/org/sinytra/adapter/model/param/Copiable.java index a549ef4e..b96a9090 100644 --- a/core/src/main/java/org/sinytra/adapter/env/param/Copiable.java +++ b/core/src/main/java/org/sinytra/adapter/model/param/Copiable.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.param; +package org.sinytra.adapter.model.param; public interface Copiable { T copy(); diff --git a/core/src/main/java/org/sinytra/adapter/model/param/HandlerSignature.java b/core/src/main/java/org/sinytra/adapter/model/param/HandlerSignature.java new file mode 100644 index 00000000..86b33d18 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/param/HandlerSignature.java @@ -0,0 +1,60 @@ +package org.sinytra.adapter.model.param; + +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.model.param.MethodParameters.ParamGroup; + +import java.util.List; + +public record HandlerSignature(List slots, ValueSource returns) { + public enum ParamSource { + /// All arguments of the target method + TARGET_ARGS, + /// CI/CIR + CALLBACK_INFO, + /// The receiver and arguments of the call the @At selects + CALL_OPERANDS, + /// MixinExtras Operation + OPERATION, + /// A single value the injector modifies + VALUE, + /// A subset of the target method's arguments + CAPTURED_TARGET_ARGS, + /// Kept unchanged, such as captured locals and sugar + KEEP + } + + public enum ValueSource { + VOID, + /// The return type of the call the `@At` selects + CALL_RETURN, + /// The return type of the target method + TARGET_RETURN, + /// An argument of the call the `@At` selects, chosen by the injector + CALL_ARG, + /// A local variable of the target method, chosen by the injector + VARIABLE, + /// Whether the wrapped instruction runs + CONDITION, + /// The constant the `@Constant` selects + CONSTANT + } + + public record Slot(ParamGroup group, ParamSource source) { + } + + public static HandlerSignature of(ValueSource returns, Slot... slots) { + return new HandlerSignature(List.of(slots), returns); + } + + public static Slot slot(ParamGroup group, ParamSource source) { + return new Slot(group, source); + } + + public List groups() { + return this.slots.stream().map(Slot::group).toList(); + } + + public MethodParameters parse(MethodNode handler) { + return MethodParameters.create(handler, groups()); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/param/MethodParameters.java b/core/src/main/java/org/sinytra/adapter/model/param/MethodParameters.java similarity index 97% rename from core/src/main/java/org/sinytra/adapter/env/param/MethodParameters.java rename to core/src/main/java/org/sinytra/adapter/model/param/MethodParameters.java index fe1bd1b3..a049cb1a 100644 --- a/core/src/main/java/org/sinytra/adapter/env/param/MethodParameters.java +++ b/core/src/main/java/org/sinytra/adapter/model/param/MethodParameters.java @@ -1,9 +1,9 @@ -package org.sinytra.adapter.env.param; +package org.sinytra.adapter.model.param; import com.google.common.collect.ImmutableMap; import org.objectweb.asm.Type; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.util.TypeConstants; +import org.sinytra.adapter.model.constant.TypeConstants; import java.util.*; import java.util.function.Predicate; @@ -76,10 +76,6 @@ public void set(ParamGroup group, Parameter param) { set(group, List.of(param)); } - public void setTypes(ParamGroup group, List params) { - set(group, params.stream().map(Parameter::simple).toList()); - } - public void set(ParamGroup group, List params) { if (!this.groups.containsKey(group)) { throw new IllegalArgumentException("Group %s is not available".formatted(group.name())); diff --git a/core/src/main/java/org/sinytra/adapter/env/param/Parameter.java b/core/src/main/java/org/sinytra/adapter/model/param/Parameter.java similarity index 95% rename from core/src/main/java/org/sinytra/adapter/env/param/Parameter.java rename to core/src/main/java/org/sinytra/adapter/model/param/Parameter.java index 647c1df6..ae9206d0 100644 --- a/core/src/main/java/org/sinytra/adapter/env/param/Parameter.java +++ b/core/src/main/java/org/sinytra/adapter/model/param/Parameter.java @@ -1,8 +1,8 @@ -package org.sinytra.adapter.env.param; +package org.sinytra.adapter.model.param; import com.google.common.collect.ImmutableList; import org.objectweb.asm.Type; -import org.sinytra.adapter.env.util.MixinAnnotations; +import org.sinytra.adapter.model.constant.MixinAnnotations; import java.util.ArrayList; import java.util.List; diff --git a/core/src/main/java/org/sinytra/adapter/model/param/Parameters.java b/core/src/main/java/org/sinytra/adapter/model/param/Parameters.java new file mode 100644 index 00000000..e625c537 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/param/Parameters.java @@ -0,0 +1,62 @@ +package org.sinytra.adapter.model.param; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; + +import java.util.*; + +public final class Parameters { + public static List parse(MethodNode method) { + return parse(method, -1, -1); + } + + public static List parse(MethodNode method, int start, int end) { + List types = getParameterTypes(method.desc); + List parameters = new ArrayList<>(); + + int first = start == -1 ? 0 : start; + int last = end == -1 ? types.size() : end; + for (int i = first; i < last; i++) { + Type type = types.get(i); + Parameter.Builder builder = Parameter.builder(type); + parseAnnotations(builder, method.invisibleParameterAnnotations, false, i); + parseAnnotations(builder, method.visibleParameterAnnotations, true, i); + Parameter parameter = builder.build(); + + parameters.add(parameter); + } + + return parameters; + } + + private static void parseAnnotations(Parameter.Builder builder, @Nullable List[] source, boolean visible, int index) { + if (source != null && source.length > index) { + List annotations = source[index]; + if (annotations != null) { + for (AnnotationNode node : annotations) { + Annotation parsed = Annotation.parse(node, visible); + builder.annotate(parsed); + } + } + } + } + + /** + * {@return the stack operands of a call: its receiver (if any), followed by its parameters} + */ + public static List getCallOperandTypes(@Nullable Type receiver, String desc) { + List types = getParameterTypes(desc); + if (receiver != null) { + types.addFirst(receiver); + } + return types; + } + + public static List getParameterTypes(String desc) { + return new ArrayList<>(Arrays.asList(Type.getArgumentTypes(desc))); + } + + private Parameters() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/model/param/RedirectedInsn.java b/core/src/main/java/org/sinytra/adapter/model/param/RedirectedInsn.java new file mode 100644 index 00000000..8c7ce683 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/model/param/RedirectedInsn.java @@ -0,0 +1,56 @@ +package org.sinytra.adapter.model.param; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; + +import java.util.List; + +public record RedirectedInsn(List operands, Type result) { + @Nullable + public static RedirectedInsn of(AbstractInsnNode insn) { + return switch (insn) { + case MethodInsnNode call -> new RedirectedInsn( + Parameters.getCallOperandTypes(call.getOpcode() == Opcodes.INVOKESTATIC ? null : Type.getObjectType(call.owner), call.desc), + Type.getReturnType(call.desc) + ); + case FieldInsnNode field -> { + Type owner = Type.getObjectType(field.owner); + Type type = Type.getType(field.desc); + yield switch (field.getOpcode()) { + case Opcodes.GETFIELD -> new RedirectedInsn(List.of(owner), type); + case Opcodes.GETSTATIC -> new RedirectedInsn(List.of(), type); + case Opcodes.PUTFIELD -> new RedirectedInsn(List.of(owner, type), Type.VOID_TYPE); + case Opcodes.PUTSTATIC -> new RedirectedInsn(List.of(type), Type.VOID_TYPE); + default -> null; + }; + } + case TypeInsnNode creation when creation.getOpcode() == Opcodes.NEW -> { + MethodInsnNode constructor = findConstructorCall(creation); + yield constructor != null ? new RedirectedInsn(Parameters.getParameterTypes(constructor.desc), Type.getObjectType(creation.desc)) : null; + } + default -> null; + }; + } + + @Nullable + public static MethodInsnNode findConstructorCall(TypeInsnNode creation) { + int depth = 0; + for (AbstractInsnNode insn = creation.getNext(); insn != null; insn = insn.getNext()) { + if (insn instanceof TypeInsnNode nested && nested.getOpcode() == Opcodes.NEW && nested.desc.equals(creation.desc)) { + depth++; + } else if (insn instanceof MethodInsnNode call && call.getOpcode() == Opcodes.INVOKESPECIAL && call.name.equals("") + && call.owner.equals(creation.desc)) { + if (depth == 0) { + return call; + } + depth--; + } + } + return null; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/DynamicPatches.java b/core/src/main/java/org/sinytra/adapter/patch/DynamicPatches.java deleted file mode 100644 index 65efa2e9..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/DynamicPatches.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.sinytra.adapter.patch; - -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Multimap; -import org.sinytra.adapter.transform.ClassTransformer; -import org.sinytra.adapter.transform.MethodTransformer; -import org.sinytra.adapter.transform.PipelineMethodTransformer; -import org.sinytra.adapter.transform.cls.DynamicAnonClassIndexPatch; -import org.sinytra.adapter.transform.cls.DynamicAnonymousShadowFieldTypePatch; -import org.sinytra.adapter.transform.patch.MethodPatch; -import org.sinytra.adapter.transform.preprocess.LocalCaptureUpgradeTransformer; -import org.sinytra.adapter.types.FieldAccessorTypeTransformer; -import org.sinytra.adapter.types.FieldTypeUsageTransformer; - -import java.util.List; - -public class DynamicPatches { - public static final List CLASS_PATCHES = List.of( - new DynamicAnonClassIndexPatch(), - new DynamicAnonymousShadowFieldTypePatch(), - new FieldTypeUsageTransformer(), - new FieldAccessorTypeTransformer() - ); - - public static Multimap methodTransformers(List patches) { - return ImmutableMultimap.of( - TxPhase.EARLY, new LocalCaptureUpgradeTransformer(), - TxPhase.VALIDATED, new PipelineMethodTransformer(patches, false) - ); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/config/key/SpecialKeys.java b/core/src/main/java/org/sinytra/adapter/patch/config/key/SpecialKeys.java deleted file mode 100644 index 650c6cbf..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/config/key/SpecialKeys.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.sinytra.adapter.patch.config.key; - -import org.objectweb.asm.commons.InstructionAdapter; -import org.objectweb.asm.tree.MethodInsnNode; -import org.sinytra.adapter.patch.config.PropertyKey; - -import java.util.function.Consumer; - -/** - * Mixin-type-specific Configuration Keys. Never serialized. - */ -public final class SpecialKeys { - // Hidden - public static final PropertyKey EXTRACT_TARGET = PropertyKey.create("_extract_target_minsn"); - public static final PropertyKey EXTRACT_ORIGIN_PARAM = PropertyKey.create("_extract_origin_param"); - public static final PropertyKey> REDIRECT_ADAPTER = PropertyKey.create("_redirect_adapter"); - public static final PropertyKey STATIC = PropertyKey.create("_static"); - public static final PropertyKey SPLIT_CANCELLATION = PropertyKey.create("_split_cancellation"); - - private SpecialKeys() { - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/InjectMixin.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/InjectMixin.java deleted file mode 100644 index 221f4f16..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/InjectMixin.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.sinytra.adapter.patch.mixin; - -import org.objectweb.asm.Type; -import org.sinytra.adapter.patch.config.ConfigurationTemplates; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameter; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.config.*; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.patch.resolver.injection.ArbitraryInjectionPointSubResolver; -import org.sinytra.adapter.patch.resolver.injection.AtVariableAssignStoreSubResolver; -import org.sinytra.adapter.patch.resolver.injection.ComparingInjectionPointResolver; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.patch.resolver.special.InjectorOrdinalResolver; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.env.ctx.Targets; - -import java.util.List; -import java.util.Objects; - -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.*; - -public class InjectMixin extends AbstractMixinType { - private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() - .keys(MixinKeys.SLICES, MixinKeys.CANCELLABLE) - .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT) - .build(); - - public InjectMixin() { - super(TEMPLATE); - } - - @Override - public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { - resolvers - .addBefore(InjectionPointResolver.class, new InjectorOrdinalResolver()); - resolvers.getOrThrow(InjectionPointResolver.class) - .addSubResolver(new AtVariableAssignStoreSubResolver()) - .addSubResolver(new ComparingInjectionPointResolver.Inject()) - .addSubResolver(new ArbitraryInjectionPointSubResolver()); - - clean.setParameters(MethodParameters.create(context.methodNode(), List.of(METHOD_PARAMS, CI_CIR, LOCALS))); - - // TODO Handle slices -// mixin.getProperty(Configuration.Keys.SLICES) -// .ifPresent(p -> clean.setProperty(Configuration.Keys.SLICES, p)); - - return TxResult.SUCCESS; - } - - @Override - public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { - dirty.setReturnType(Type.VOID_TYPE); - if (dirty.getTargetMethod() == null || dirty.getTargetMethod().desc() == null) { - return TxResult.SUCCESS; - } - - if (sameTarget(recipe)) { - dirty.inheritParameters(); - } else { - List cleanParams = clean.getParameters().getTypes(METHOD_PARAMS); - if (!cleanParams.isEmpty()) { - MethodParameters newParams = clean.getParameters().copy(); - List dirtyTargetMethodParams = Parameters.getParameterTypes(dirty.getTargetMethod().desc()); - newParams.setTypes(METHOD_PARAMS, dirtyTargetMethodParams); - dirty.setParameters(newParams); - } else { - dirty.inheritParameters(); - } - } - - // Process static modifier - if (!clean.getProperty(SpecialKeys.STATIC).orElse(false) && dirty.getProperty(SpecialKeys.STATIC).orElse(false)) { - List types = context.targetTypes(); - if (types.size() != 1) { - return TxResult.FAIL; - } - MethodParameters params = dirty.getParameters(); - List methodParams = params.get(METHOD_PARAMS); - Type targetType = types.getFirst(); - methodParams.addFirst(Parameter.simple(targetType)); - } - - return TxResult.SUCCESS; - } - - private boolean sameTarget(Recipe recipe) { - MethodQualifier cleanQ = recipe.clean().getTargetMethod(); - MethodQualifier dirtyQ = recipe.dirty().getTargetMethod(); - if (cleanQ != null && dirtyQ != null && Objects.equals(cleanQ.name(), dirtyQ.name()) && Objects.equals(cleanQ.desc(), dirtyQ.desc())) { - return true; - } - - Targets targets = recipe.targets(); - return targets != null && MethodQualifier.create(targets.clean().methodNode()).matches(MethodQualifier.create(targets.dirty().methodNode())); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinType.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinType.java deleted file mode 100644 index cd71d1b6..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinType.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.sinytra.adapter.patch.mixin; - -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; - -import java.util.EnumSet; -import java.util.Set; - -/** - * Handles configuration and behavior specific to a Mixin type - */ -public interface MixinType { - PropertyContainerTemplate getConfigurationTemplate(); - - default Set getFlags() { - return EnumSet.noneOf(MixinFlag.class); - } - - default boolean canInject(MixinContext context, Configuration config) { - return true; - } - - TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors); - - TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe); -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyExpressionValueMixin.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyExpressionValueMixin.java deleted file mode 100644 index f68454fc..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyExpressionValueMixin.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.sinytra.adapter.patch.mixin; - -import org.objectweb.asm.Type; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.ConfigurationTemplates; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.patch.resolver.injection.ArbitraryInjectionPointSubResolver; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.patch.resolver.special.ResolverSyntheticInstanceof; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.List; - -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.LOCALS; -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.SINGLE_ANY; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; - -public class ModifyExpressionValueMixin extends AbstractMixinType { - private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() - .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT) - .build(); - - public ModifyExpressionValueMixin() { - super(TEMPLATE, MixinFlag.RETURN_TYPE_SENSITIVE); - } - - @Override - public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { - resolvers.getOrThrow(InjectionPointResolver.class) - .addSubResolver(new ArbitraryInjectionPointSubResolver()); - resolvers - .addBefore(InjectionPointResolver.class, new ResolverSyntheticInstanceof(true)); - - clean.setParameters(MethodParameters.create(context.methodNode(), List.of(SINGLE_ANY, CAPTURED_PARAMS, LOCALS))); - - return TxResult.SUCCESS; - } - - @Override - public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { - if (dirty.getTargetMethod() == null || dirty.getAtData() == null) { - return TxResult.FAIL; - } - // Best effort for non-INVOKE - if (!dirty.getAtData().getValue().equals(AT_VAL_INVOKE)) { - dirty.inheritSignature(); - return TxResult.SUCCESS; - } - - MethodQualifier targetDesc = dirty.getAtData().getTargetQualifier().orElse(null); - if (targetDesc == null || targetDesc.desc() == null) { - // Best effort - dirty.inheritSignature(); - return TxResult.SUCCESS; - } - - List dirtyCaptured = context.methods().resolveCapturedMethodParams(recipe.clean(), recipe.dirty()); - Type modifyingType = Type.getReturnType(targetDesc.desc()); - - MethodParameters params = clean.getParameters().mutableCopy() - .putType(SINGLE_ANY, modifyingType) - .putTypes(CAPTURED_PARAMS, dirtyCaptured) - .build(); - - dirty.setParameters(params); - dirty.setReturnType(modifyingType); - - return TxResult.SUCCESS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyReturnValueMixin.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyReturnValueMixin.java deleted file mode 100644 index 0ec54783..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyReturnValueMixin.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.sinytra.adapter.patch.mixin; - -import org.objectweb.asm.Type; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.ConfigurationTemplates; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.patch.resolver.special.InjectorOrdinalResolver; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.env.ctx.Targets; - -import java.util.List; - -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.*; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_RETURN; - -public class ModifyReturnValueMixin extends AbstractMixinType { - private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() - .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT) - .build(); - - public ModifyReturnValueMixin() { - super(TEMPLATE); - } - - @Override - public boolean canInject(MixinContext context, Configuration config) { - AtData at = config.getAtData(); - MethodQualifier target = config.getTargetMethod(); - if (at == null || !AT_VAL_RETURN.equals(at.getValue()) || at.getProperty(AtData.Keys.ORDINAL).isEmpty() || target == null) { - return true; - } - - Targets targets = context.methods().findTargets(target); - return targets == null || MethodAnalyzer.findReturnInsns(targets.clean().methodNode()).size() == MethodAnalyzer.findReturnInsns(targets.dirty().methodNode()).size(); - } - - @Override - public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { - resolvers - .addBefore(InjectionPointResolver.class, new InjectorOrdinalResolver()); - - clean.setParameters(MethodParameters.create(context.methodNode(), List.of(SINGLE_ANY, CAPTURED_PARAMS, LOCALS))); - - return TxResult.SUCCESS; - } - - // TODO Cleanup parameter processing in mixin types - // 1. Method params vs Captured params + reconstruction - // 2. Locals - @Override - public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { - MethodQualifier target = dirty.getTargetMethod(); - if (target == null || target.desc() == null) { - return TxResult.FAIL; - } - - List dirtyCaptured = context.methods().resolveCapturedMethodParams(recipe.clean(), recipe.dirty()); - Type modifyingType = Type.getReturnType(target.desc()); - - MethodParameters params = clean.getParameters().mutableCopy() - .putType(SINGLE_ANY, modifyingType) - .putTypes(CAPTURED_PARAMS, dirtyCaptured) - .build(); - - dirty.setParameters(params); - dirty.setReturnType(modifyingType); - - return TxResult.SUCCESS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyVariableMixin.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyVariableMixin.java deleted file mode 100644 index 33a09589..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyVariableMixin.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.sinytra.adapter.patch.mixin; - -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.LocalVariableNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameter; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.ConfigurationTemplates; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.patch.resolver.injection.ModifyVarInjectionPointSubResolver; -import org.sinytra.adapter.patch.resolver.special.InjectorOrdinalResolver; -import org.sinytra.adapter.patch.resolver.special.ModifyVarAtReturnResolver; -import org.sinytra.adapter.patch.resolver.special.ModifyVarUpgradeResolver; -import org.sinytra.adapter.types.TypeAdapter; -import org.sinytra.adapter.env.ctx.TargetPair; - -import java.util.List; - -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.LOCALS; -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.SINGLE_ANY; - -public class ModifyVariableMixin extends AbstractMixinType { - private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() - .keys(MixinKeys.ARGS_ONLY, MixinKeys.ORDINAL, MixinKeys.INDEX, MixinKeys.SLICE) - .pluralKeys(MixinKeys.TARGET_METHOD) - .build(); - - public ModifyVariableMixin() { - super(TEMPLATE, MixinFlag.TARGETS_VARIABLE); - } - - @Override - public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { - resolvers - .addFirst(new ModifyVarUpgradeResolver()) - .addBefore(InjectionPointResolver.class, new InjectorOrdinalResolver()) - .addBefore(InjectionPointResolver.class, new ModifyVarAtReturnResolver()); - resolvers.getOrThrow(InjectionPointResolver.class) - .addSubResolver(new ModifyVarInjectionPointSubResolver()); - - clean.setParameters(MethodParameters.create(context.methodNode(), List.of(SINGLE_ANY, LOCALS))); - - return TxResult.SUCCESS; - } - - @Override - public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { - if (dirty.getTargetMethod() == null || dirty.getTargetMethod().desc() == null) { - return TxResult.FAIL; - } - - dirty.inheritProperyIfAbsent(MixinKeys.SLICE); - - boolean argsOnly = clean.getProperty(MixinKeys.ARGS_ONLY).orElse(false); - String cleanDesc = clean.getTargetMethod().desc(); - if (argsOnly && cleanDesc != null && !dirty.getTargetMethod().desc().equals(cleanDesc)) { - Type cleanVarType = clean.getParameters().getTypes(SINGLE_ANY).getFirst(); - List cleanTargetMethodParams = Parameters.getParameterTypes(cleanDesc); - int cleanIndex = cleanTargetMethodParams.indexOf(cleanVarType); - - List dirtyTargetMethodParams = Parameters.getParameterTypes(dirty.getTargetMethod().desc()); - // The modified argument can't be located in the new target method - if (cleanIndex == -1 || cleanIndex >= dirtyTargetMethodParams.size()) { - return TxResult.FAIL; - } - Type dirtyVarType = dirtyTargetMethodParams.get(cleanIndex); - - TypeAdapter adapter = context.getTypeAdapter(cleanVarType, dirtyVarType); - if (adapter != null) { - MethodParameters newParams = MethodParameters.create(context.methodNode(), List.of(SINGLE_ANY, LOCALS)); - newParams.get(SINGLE_ANY).set(0, Parameter.simple(dirtyVarType)); - - dirty.setParameters(newParams); - dirty.setReturnType(dirtyVarType); - } - return TxResult.SUCCESS; - } - - dirty.inheritSignature(); - - return TxResult.SUCCESS; - } - - @Override - public boolean canInject(MixinContext context, Configuration config) { - // Ensure variable at index has the correct type - if (config.hasProperty(MixinKeys.INDEX)) { - TargetPair targetPair = context.methods().findOwnMethodPair(context.dirtyLookup(), config.getTargetMethod()); - if (targetPair == null) return false; - MethodNode target = targetPair.methodNode(); - - Integer index = config.getProperty(MixinKeys.INDEX).orElseThrow(); - LocalVariableLookup lookup = context.methods().getLVT(target); - if (lookup == null) return false; - - LocalVariableNode node = lookup.getByIndexOrNull(index); - if (node == null) return false; - - Type expectedType = config.getReturnType(); - Type actualType = Type.getType(node.desc); - return expectedType.equals(actualType); - } - return true; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/RedirectMixin.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/RedirectMixin.java deleted file mode 100644 index b3b5d2bf..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/RedirectMixin.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.sinytra.adapter.patch.mixin; - -import org.objectweb.asm.Type; -import org.sinytra.adapter.env.ctx.MethodHelper; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.MethodParameters.ParamGroup; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.env.util.MixinAnnotationConstants; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.*; -import org.sinytra.adapter.patch.config.key.ControlKeys; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.processor.ParametersProcessor; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.processor.redirect.DivertRedirectProcessor; -import org.sinytra.adapter.patch.processor.redirect.ParameterUsageProcessor; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.patch.resolver.special.ResolverSyntheticInstanceof; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.List; - -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.*; - -public class RedirectMixin extends AbstractMixinType { - private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() - .pluralKeys(MixinKeys.TARGET_METHOD) - .build(); - - public RedirectMixin() { - super(TEMPLATE, MixinFlag.AT_TARGET_SENSITIVE, MixinFlag.ACCEPTS_INSTANCE); - } - - @Override - public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { - resolvers. - addBefore(InjectionPointResolver.class, new ResolverSyntheticInstanceof(false)); - processors - .addAfter(ParametersProcessor.class, new DivertRedirectProcessor()) - .addAfter(ParametersProcessor.class, new ParameterUsageProcessor()); - - if (clean.getAtData() == null || !MixinAnnotationConstants.AT_VAL_INVOKE.equals(clean.getAtData().getValue())) - return TxResult.FAIL; - - MethodQualifier targetDesc = clean.getAtData().getTargetQualifier().orElse(null); - if (targetDesc == null || targetDesc.desc() == null) - return TxResult.FAIL; - - List methodParams = Parameters.getParameterTypes(context.methodNode().desc); - boolean isStatic = isStaticRedirect(context, targetDesc, methodParams, Parameters.getParameterTypes(targetDesc.desc())); - if (!isStatic && methodParams.isEmpty()) - return TxResult.FAIL; - List callTypes = Parameters.getCallOperandTypes(isStatic ? null : methodParams.getFirst(), targetDesc.desc()); - - MethodParameters capturedMethodParams = MethodParameters.create( - Parameters.parse(context.methodNode(), callTypes.size(), -1), - List.of(ParamGroup.CAPTURED_PARAMS, ParamGroup.LOCALS) - ); - - MethodParameters params = MethodParameters.builder() - .putTypes(METHOD_PARAMS, callTypes) - .putTypes(CAPTURED_PARAMS, capturedMethodParams.getTypes(CAPTURED_PARAMS)) - .putTypes(LOCALS, capturedMethodParams.getTypes(LOCALS)) - .putRemaining(capturedMethodParams) - .build(); - - clean.setParameters(params); - - return TxResult.SUCCESS; - } - - @Override - public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { - if (dirty.getTargetMethod() == null || dirty.getAtData() == null) - return TxResult.FAIL; - - MethodQualifier targetDesc = dirty.getAtData().getTargetQualifier().orElse(null); - if (targetDesc == null || targetDesc.desc() == null) - return TxResult.FAIL; - - TargetPair dirtyTarget = context.methods().findOwnMethodPair(context.dirtyLookup(), targetDesc); - if (dirtyTarget == null) - return TxResult.FAIL; - - List dirtyCaptured = context.methods().resolveCapturedMethodParams(recipe.clean(), recipe.dirty()); - Type receiver = MethodHelper.isStatic(dirtyTarget.methodNode()) ? null : Type.getObjectType(dirtyTarget.classNode().name); - List callTypes = Parameters.getCallOperandTypes(receiver, targetDesc.desc()); - - dirty.inheritProperyIfAbsent(ControlKeys.PARAMETERS); - MethodParameters params = (dirty.getParameters() != null ? dirty.getParameters().mutableCopy() : MethodParameters.builder()) - .putTypes(METHOD_PARAMS, callTypes) - .putTypes(CAPTURED_PARAMS, dirtyCaptured) - .build(); - - dirty.setParameters(params); - dirty.setReturnType(Type.getReturnType(targetDesc.desc())); - - return TxResult.SUCCESS; - } - - private boolean isStaticRedirect(MixinContext context, MethodQualifier targetDesc, List methodParams, List callTypes) { - TargetPair cleanTarget = context.methods().findOwnMethodPair(context.cleanLookup(), targetDesc); - if (cleanTarget != null) { - return MethodHelper.isStatic(cleanTarget.methodNode()); - } - return methodParams.size() >= callTypes.size() && methodParams.subList(0, callTypes.size()).equals(callTypes); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/WrapOperationMixin.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/WrapOperationMixin.java deleted file mode 100644 index 65667b52..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/WrapOperationMixin.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.sinytra.adapter.patch.mixin; - -import org.objectweb.asm.Type; -import org.sinytra.adapter.patch.config.*; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.MethodHelper; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.processor.ParametersProcessor; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.processor.wrapop.WrapOpParamsProcessor; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.patch.resolver.injection.AtVariableAssignStoreSubResolver; -import org.sinytra.adapter.patch.resolver.injection.ComparingInjectionPointResolver; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.env.util.TypeConstants; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.env.ctx.TargetPair; - -import java.util.List; - -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.*; - -public class WrapOperationMixin extends AbstractMixinType { - private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_BASE.extend() - .requireOne(MixinKeys.TARGET_AT, MixinKeys.TARGET_CONSTANT) - .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT, MixinKeys.TARGET_CONSTANT) - .build(); - - public WrapOperationMixin() { - super(TEMPLATE, MixinFlag.AT_TARGET_SENSITIVE, MixinFlag.ACCEPTS_INSTANCE); - } - - @Override - public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { - resolvers.getOrThrow(InjectionPointResolver.class) - .addSubResolverFirst(new ComparingInjectionPointResolver.WrapOperation()) - .addSubResolver(new AtVariableAssignStoreSubResolver()); - processors - .addAfter(ParametersProcessor.class, new WrapOpParamsProcessor()); - - clean.setParameters(MethodParameters.create(context.methodNode(), List.of(METHOD_PARAMS, OPERATION, CAPTURED_PARAMS, LOCALS))); - - return TxResult.SUCCESS; - } - - @Override - public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { - if (dirty.getTargetMethod() == null) return TxResult.FAIL; - if (dirty.getAtData() == null) return TxResult.PASS; - - MethodQualifier dirtyAtTarget = dirty.getAtData().getTargetQualifier().orElse(null); - if (dirtyAtTarget == null || dirtyAtTarget.desc() == null) - return TxResult.FAIL; - - List dirtyCaptured = context.methods().resolveCapturedMethodParams(recipe.clean(), recipe.dirty()); - TargetPair targetMethodCall = dirtyAtTarget.isFull() ? context.methods().findInheritedMethodPair(context.dirtyLookup(), dirtyAtTarget) : null; - Type receiver = targetMethodCall != null && !MethodHelper.isStatic(targetMethodCall.methodNode()) ? Type.getObjectType(dirtyAtTarget.internalOwnerName()) : null; - List callTypes = Parameters.getCallOperandTypes(receiver, dirtyAtTarget.desc()); - - MethodParameters params = clean.getParameters().mutableCopy() - .putTypes(METHOD_PARAMS, callTypes) - .putType(OPERATION, TypeConstants.OPERATION_TYPE) - .putTypes(CAPTURED_PARAMS, dirtyCaptured) - .build(); - - dirty.setParameters(params); - dirty.setReturnType(Type.getReturnType(dirtyAtTarget.desc())); - - return TxResult.SUCCESS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/ParametersPostProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/ParametersPostProcessor.java deleted file mode 100644 index c7f06370..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/ParametersPostProcessor.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.sinytra.adapter.patch.processor; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; -import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.transform.param.ReplaceParametersTransformer; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; - -import java.util.List; - -/** - * If a mixin has its parameter changed to a higher class e.g. ServerPlayer -> LivingEntity but - * uses it to call a method that only accepts a ServerPlayer, we have to cast it back. - *

- * Before: - *

{@code
- *     private void modify(ServerLevel world, ServerPlayer player, CallbackInfo ci) {
- *         trigger(player, stack, getDamageValue());
- *     }
- * }
- *

- * After: - *

{@code
- *     private void modify(ServerLevel world, LivingEntity player, CallbackInfo ci) {
- *         trigger((ServerPlayer)player, stack, getDamageValue());
- * //              ^^^^^^^^^^^^^^ Added cast after param change 
- *     }
- * }
- */ -// TODO Add type safety check -public class ParametersPostProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - MethodParameters cleanParams = recipe.clean().getParameters(); - MethodParameters dirtyParams = dirty.getParameters(); - if (dirtyParams == null || !cleanParams.has(MethodParameters.ParamGroup.METHOD_PARAMS) || !dirtyParams.has(MethodParameters.ParamGroup.METHOD_PARAMS)) { - return TxResult.PASS; - } - - List cleanTypes = cleanParams.getTypes(MethodParameters.ParamGroup.METHOD_PARAMS); - List dirtyTypes = dirtyParams.getTypes(MethodParameters.ParamGroup.METHOD_PARAMS); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(cleanTypes, dirtyTypes); - if (diff == null) return TxResult.PASS; - - MethodNode methodNode = context.methodNode(); - LocalVariableLookup lookup = context.methods().getLVT(context.methodNode()); - if (lookup == null) return TxResult.PASS; - - boolean matched = false; - for (ReplaceParametersTransformer replacement : diff.replacements()) { - int ordinal = replacement.index(); - LocalVariableNode node = lookup.getByParameterOrdinalOrNull(ordinal); - if (node == null) continue; - - Type currentType = Type.getType(node.desc); - - for (AbstractInsnNode insn : methodNode.instructions) { - if (!(insn instanceof MethodInsnNode minsn)) continue; - - // Add casts to usage in method calls - List methodArgs = Parameters.getParameterTypes(minsn.desc); - List callArgs = MethodAnalyzer.getMethodCallSrcInsns(methodNode, minsn); - if (callArgs == null) continue; - - int instanceOffset = minsn.getOpcode() == Opcodes.INVOKESTATIC ? 0 : 1; - for (int i = instanceOffset; i < callArgs.size(); i++) { - AbstractInsnNode arg = callArgs.get(i); - if (arg instanceof VarInsnNode varInsn && varInsn.var == node.index) { - Type type = methodArgs.get(i - instanceOffset); - if (!currentType.equals(type)) { - methodNode.instructions.insert(varInsn, new TypeInsnNode(Opcodes.CHECKCAST, type.getInternalName())); - matched = true; - } - } - } - } - } - - return matched ? TxResult.SUCCESS : TxResult.PASS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/ParametersProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/ParametersProcessor.java deleted file mode 100644 index 28604107..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/ParametersProcessor.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.sinytra.adapter.patch.processor; - -import com.mojang.datafixers.util.Pair; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.VarInsnNode; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; -import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; - -import java.util.List; -import java.util.Map; - -public class ParametersProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - MethodParameters cleanParams = recipe.clean().getParameters(); - MethodParameters dirtyParams = dirty.getParameters(); - if (dirtyParams == null) return TxResult.FAIL; - if (cleanParams.merge().equals(dirtyParams.merge())) - return TxResult.PASS; - - // Apply mappings of params that will be removed in dirty - Map> oldVarMap = Parameters.gatherVarMappings(context.methodNode(), cleanParams.merge(), dirtyParams.merge(), dirtyParams.getMapping(), context); - - if (cleanParams.getOrder().equals(dirtyParams.getOrder())) { - List order = dirtyParams.getOrder(); - int offset = 0; - for (MethodParameters.ParamGroup group : order) { - List cleanList = cleanParams.getTypes(group); - List dirtyList = dirtyParams.getTypes(group); - - if (!applyDiff(cleanList, dirtyList, context, offset)) { - return TxResult.FAIL; - } - offset += dirtyList.size(); - } - } else { - if (!applyDiff(cleanParams.mergeTypes(), dirtyParams.mergeTypes(), context, 0)) { - return TxResult.FAIL; - } - } - - // Apply mappings of params that only exist in dirty - Parameters.applyVarMappings(context.methodNode(), Parameters.gatherVarMappings(context.methodNode(), dirtyParams.merge(), dirtyParams.merge(), dirtyParams.getMapping(), context)); - Parameters.applyVarMappings(context.methodNode(), oldVarMap); - Parameters.applyAnnotations(context.methodNode(), dirtyParams.merge()); - - return TxResult.SUCCESS; - } - - private boolean applyDiff(List clean, List dirty, MixinContext context, int offset) { - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(clean, dirty); - if (diff == null) { - return false; - } - if (!diff.isEmpty()) { - PatchResult result = diff.offset(offset).asParameterTransformer(false) - .apply(context); - return result != PatchResult.PASS; - } - return true; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/Processor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/Processor.java deleted file mode 100644 index 093a4dc6..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/Processor.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.sinytra.adapter.patch.processor; - -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; - -public interface Processor { - TxResult process(MixinContext context, Configuration dirty, Recipe recipe); -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/ReturnTypeProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/ReturnTypeProcessor.java deleted file mode 100644 index cf5b1a4c..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/ReturnTypeProcessor.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.sinytra.adapter.patch.processor; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.SourceValue; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.types.TypeAdapter; -import org.objectweb.asm.tree.analysis.Frame; -import org.sinytra.adapter.analysis.selector.FrameUtil; - -import java.util.Set; -import java.util.LinkedHashSet; - -public class ReturnTypeProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - Type cleanType = recipe.clean().getReturnType(); - Type dirtyType = dirty.getReturnType(); - - if (cleanType.equals(dirtyType)) { - return TxResult.PASS; - } - - TypeAdapter adapter = context.getTypeAdapter(cleanType, dirtyType); - if (adapter == null) { - return TxResult.PASS; - } - - MethodNode methodNode = context.methodNode(); - Frame[] frames = FrameUtil.getFrames(methodNode); - Set sources = new LinkedHashSet<>(); - for (AbstractInsnNode ret : MethodAnalyzer.findReturnInsns(methodNode)) { - Frame frame = frames[methodNode.instructions.indexOf(ret)]; - if (frame != null) { - FrameUtil.getOperands(frame, ret).forEach(value -> sources.addAll(value.insns)); - } - } - for (AbstractInsnNode insn : sources) { - if (insn.getOpcode() != Opcodes.ACONST_NULL) { - adapter.apply(methodNode.instructions, insn); - } - } - - methodNode.desc = Type.getMethodDescriptor(dirtyType, Type.getArgumentTypes(methodNode.desc)); - - return TxResult.SUCCESS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/SplitCancellationProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/SplitCancellationProcessor.java deleted file mode 100644 index ce3b156c..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/SplitCancellationProcessor.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sinytra.adapter.patch.processor; - -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.resolver.target.SplitMethodCancellationHelper; - -public class SplitCancellationProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - if (!dirty.getProperty(SpecialKeys.SPLIT_CANCELLATION).orElse(false)) { - return TxResult.PASS; - } - - TargetPair dirtyTarget = recipe.getDirtyTarget(); - if (dirtyTarget == null) { - return TxResult.PASS; - } - - SplitMethodCancellationHelper.handle(context, recipe, dirtyTarget.methodNode()); - return TxResult.SUCCESS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/StaticAccessProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/StaticAccessProcessor.java deleted file mode 100644 index bb1ce34f..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/StaticAccessProcessor.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.sinytra.adapter.patch.processor; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.LocalVariableNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.MethodHelper; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.SingleValueHandle; - -import java.util.List; -import java.util.Objects; - -public class StaticAccessProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - if (!recipe.clean().hasProperty(SpecialKeys.STATIC) || !dirty.hasProperty(SpecialKeys.STATIC)) - return TxResult.PASS; - - boolean cleanStatic = recipe.clean().getProperty(SpecialKeys.STATIC).orElseThrow(); - boolean dirtyStatic = dirty.getProperty(SpecialKeys.STATIC).orElseThrow(); - if (cleanStatic == dirtyStatic) return TxResult.PASS; - - MethodNode method = context.methodNode(); - boolean actuallyStatic = MethodHelper.isStatic(context.methodNode()); - // Add static - if (!actuallyStatic && !cleanStatic && dirtyStatic) { - // Mixin types add the target instance as the first parameter, which replaces usages of "this" - Type[] args = Type.getArgumentTypes(method.desc); - List targetTypes = context.targetTypes(); - boolean hasInstanceParam = args.length > 0 && targetTypes.size() == 1 && args[0].equals(targetTypes.getFirst()); - boolean usesThis = false; - for (AbstractInsnNode insn : method.instructions) { - SingleValueHandle handle = AdapterUtil.handleLocalVarInsnValue(insn); - if (handle != null && handle.get() == 0) { - usesThis = true; - break; - } - } - if (usesThis && !hasInstanceParam) { - return TxResult.FAIL; - } - - context.recordCtxAudit("Made method static"); - method.access |= Opcodes.ACC_STATIC; - - // Remove "this" and shift all other locals down by one slot - if (method.localVariables != null) { - method.localVariables.removeIf(lvn -> lvn.index == 0); - for (LocalVariableNode lvn : method.localVariables) { - lvn.index--; - } - } - AdapterUtil.replaceLVT(method, idx -> idx == 0 ? 0 : idx - 1); - - return TxResult.SUCCESS; - } - // Remove static - else if (actuallyStatic && cleanStatic && !dirtyStatic) { - context.recordCtxAudit("Made method non-static"); - method.access &= ~Opcodes.ACC_STATIC; - - List localVariables = Objects.requireNonNullElseGet(method.localVariables, List::of); - LocalVariableNode firstParam = localVariables.stream().filter(lvn -> lvn.index == 0) - .findFirst() - .orElse(null); - // Offset everything by 1 - for (LocalVariableNode lvn : localVariables) { - lvn.index++; - } - AdapterUtil.replaceLVT(method, idx -> idx + 1); - - // Insert instance local variable - if (firstParam != null) { - method.localVariables.add(new LocalVariableNode("this", Type.getObjectType(context.classNode().name).getDescriptor(), null, firstParam.start, firstParam.end, 0)); - } - - // TODO Must compute frames - return TxResult.SUCCESS; - } - - return TxResult.PASS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/TargetMethodProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/TargetMethodProcessor.java deleted file mode 100644 index 3aa9918a..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/TargetMethodProcessor.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.sinytra.adapter.patch.processor; - -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.locals.LocalVarAnalyzer; -import org.sinytra.adapter.env.ctx.LocalVariable; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; - -import java.util.List; - -public class TargetMethodProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - if (dirty.getTargetMethod() == null) return TxResult.FAIL; - - upgradeCapturedLocals(context.methodNode(), context, recipe); - - return TxResult.SUCCESS; - } - - // TODO Is there a better approach? - private static void upgradeCapturedLocals(MethodNode methodNode, MixinContext context, Recipe recipe) { - LocalVarAnalyzer.CapturedLocals capturedLocals = LocalVarAnalyzer.findCapturedLocals(context, recipe); - if (capturedLocals == null) return; - - List availableLocals = context.methods().getTargetMethodLocals(capturedLocals.target()); - // For now, only handle cases where all locals are part of the method's params, convenient when switching the target to a lambda - if (availableLocals == null || !availableLocals.isEmpty()) return; - - LocalVarAnalyzer.CapturedLocalsTransform transform = LocalVarAnalyzer.analyzeCapturedLocals(capturedLocals, methodNode); - transform.remover().apply(context); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/extract/ExtractMixinProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/extract/ExtractMixinProcessor.java deleted file mode 100644 index 29326a49..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/extract/ExtractMixinProcessor.java +++ /dev/null @@ -1,381 +0,0 @@ -package org.sinytra.adapter.patch.processor.extract; - -import it.unimi.dsi.fastutil.ints.Int2IntMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import org.objectweb.asm.Handle; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.method.MethodAnalyzer.IndyHandle; -import org.sinytra.adapter.env.ctx.LocalVariable; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.processor.Processor; -import org.sinytra.adapter.analysis.locals.LocalVarAnalyzer; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.transform.param.TransformParameters; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.OpcodeUtil; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.util.SingleValueHandle; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.stream.IntStream; - -// TODO Cleanup extraction process + proxy/mirroring -public class ExtractMixinProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - if (recipe.clean().getTargetClass().equals(dirty.getTargetClass())) return TxResult.PASS; - - PatchResult result = apply(dirty.getTargetClass(), context, recipe); - if (result == PatchResult.PASS && dirty.hasProperty(SpecialKeys.EXTRACT_TARGET)) { - MethodInsnNode minsn = dirty.getProperty(SpecialKeys.EXTRACT_TARGET).orElseThrow(); - result = MirrorableExtractMixin.apply(context, recipe, dirty.getTargetClass(), minsn); - } - - // Defer to ProxyExtractMixinSub - if (result == PatchResult.PASS && dirty.hasProperty(SpecialKeys.EXTRACT_ORIGIN_PARAM)) { - return ProxyExtractMixinSub.INSTANCE.process(context, dirty, recipe); - } - - return result == PatchResult.PASS ? TxResult.FAIL : TxResult.SUCCESS; - } - - // TODO Prioritize mirroring over extraction, remove locals recreation - public static PatchResult apply(String targetClassName, MixinContext context, Recipe recipe) { - // Sanity check - boolean isStatic = context.isStatic(); - ClassNode classNode = context.classNode(); - MethodNode methodNode = context.methodNode(); - - TargetPair cleanTarget = recipe.getCleanTarget(); - String owner = Optional.ofNullable(cleanTarget).map(t -> t.classNode().name).orElse(targetClassName); - boolean isInherited = context.environment().inheritanceHandler(context.dirtyLookup()).isClassInherited(targetClassName, owner); - Candidates candidates = findCandidates(classNode, methodNode); - if (!candidates.canMove(classNode, isInherited)) return PatchResult.PASS; - - ClassNode targetClass = context.environment().dirtyClassLookup().getClass(targetClassName).orElse(null); - // Get or generate new mixin class - ClassNode generatedTarget = context.environment().classGenerator().getOrGenerateMixinClass(classNode, targetClassName, targetClass != null ? targetClass.superName : null); - context.environment().refmapHolder().copyEntries(classNode.name, generatedTarget.name); - - // Add mixin methods from original to generated class - for (MethodNode method : candidates.methods()) { - generatedTarget.methods.add(method); - updateOwnerRefereces(method, classNode, targetClassName); - if (!isStatic && method.localVariables != null) { - method.localVariables.stream().filter(l -> l.index == 0).findFirst().ifPresent(lvn -> lvn.desc = Type.getObjectType(generatedTarget.name).getDescriptor()); - } - } - - candidates.handleUpdates().forEach(c -> c.accept(generatedTarget)); - - // Take care of captured locals - PatchResult result = PatchResult.PASS; - boolean capturesLocals = recipe.clean().hasProperty(MixinKeys.LOCALS); - if (capturesLocals) { - result = result.or(recreateLocalVariables(methodNode, context, recipe, generatedTarget)); - } - - context.recordCtxAudit("Extract mixin to target %s", targetClassName); - // Remove original method - context.patchContext().postApply(() -> classNode.methods.removeAll(candidates.methods)); - return result.or(PatchResult.APPLY); - } - - record Candidates(List methods, List> handleUpdates) { - public boolean canMove(ClassNode classNode, boolean isInherited) { - List accessFixes = new ArrayList<>(); - for (MethodNode methodNode : this.methods) { - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof FieldInsnNode finsn && finsn.owner.equals(classNode.name) && !isInheritedField(classNode, finsn, isInherited, accessFixes) - || insn instanceof MethodInsnNode minsn && minsn.owner.equals(classNode.name) && !isInheritedMethod(classNode, minsn, isInherited, accessFixes) - ) { - // We can't move methods that access their class instance - return false; - } - } - } - accessFixes.forEach(Runnable::run); - return true; - } - } - - private static Candidates findCandidates(ClassNode classNode, MethodNode methodNode) { - List methods = new ArrayList<>(); - List> handleUpdates = new ArrayList<>(); - methods.add(methodNode); - for (IndyHandle lambda : MethodAnalyzer.findOwnIndyHandles(classNode, methodNode)) { - Handle handle = lambda.handle(); - if (handle.getName().startsWith(MethodAnalyzer.LAMBDA_PREFIX + methodNode.name)) { - methods.add(lambda.method()); - handleUpdates.add(t -> lambda.indy().bsmArgs[lambda.argIndex()] = new Handle(handle.getTag(), t.name, handle.getName(), handle.getDesc(), handle.isInterface())); - } - } - return new Candidates(methods, handleUpdates); - } - - private static void updateOwnerRefereces(MethodNode methodNode, ClassNode originalClass, String targetClass) { - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof MethodInsnNode minsn && minsn.owner.equals(originalClass.name)) { - minsn.owner = targetClass; - } else if (insn instanceof FieldInsnNode finsn && finsn.owner.equals(originalClass.name)) { - finsn.owner = targetClass; - } - } - } - - private static boolean isInheritedField(ClassNode cls, FieldInsnNode finsn, boolean isTargetInherited, List accessUpdates) { - return cls.fields.stream() - .filter(f -> f.name.equals(finsn.name)) - .findFirst() - .map(f -> isInheritedMember(f.visibleAnnotations, SingleValueHandle.of(() -> f.access, a -> f.access = a), isTargetInherited, accessUpdates)) - .orElse(false); - } - - private static boolean isInheritedMethod(ClassNode cls, MethodInsnNode minsn, boolean isTargetInherited, List accessUpdates) { - return Optional.ofNullable(AdapterUtil.findMethod(cls, minsn.name, minsn.desc)) - .map(m -> isInheritedMember(m.visibleAnnotations, SingleValueHandle.of(() -> m.access, a -> m.access = a), isTargetInherited, accessUpdates)) - .orElse(false); - } - - private static boolean isInheritedMember(@Nullable List annotations, SingleValueHandle access, boolean isTargetInherited, List accessUpdates) { - if (AdapterUtil.hasAnnotation(annotations, MixinAnnotations.SHADOW)) { - return true; - } - if (isTargetInherited) { - accessUpdates.add(() -> access.set(fixAccess(access.get()))); - return true; - } - return false; - } - - private static int fixAccess(int access) { - int visibility = OpcodeUtil.getAccessVisibility(access); - // Lower than protected - if (visibility == Opcodes.ACC_PRIVATE || visibility == 0) { - // Widen to protected - // Add synthetic to avoid mixin complaining about non-private static members being present - // Setting the access to public will prevent the member from being renamed - return OpcodeUtil.setAccessVisibility(access, Opcodes.ACC_PUBLIC) | Opcodes.ACC_SYNTHETIC; - } - return access; - } - - /** - * We can't bring captured locals along when extracting a mixin, so our best bet is to recreate them from scratch. - *

- * Consider the following mixin injector into Gui#renderPlayerHealth, capturing 15 local variables of which only 2 are used. - *

{@code @Inject(
-     *     method = "renderPlayerHealth",
-     *     at = @At(
-     *         value = "INVOKE",
-     *         target = "someInvocation",
-     *     ),
-     *     locals = LocalCapture.CAPTURE_FAILEXCEPTION
-     * )
-     * private void renderArmor(GuiGraphics context, CallbackInfo info, Player player, int i, boolean bl, long l, int j, FoodData foodData, int k, int left, int n, int o, float f, int p, int q, int r, int top) {
-     *     return drawArmor(context, left, top);
-     * }
-     * }
- *

- * We first filter out unused captured locals. - *

{@code @Inject(
-     *     method = "renderPlayerHealth",
-     *     at = @At(
-     *         value = "INVOKE",
-     *         target = "someInvocation",
-     *     ),
-     *     locals = LocalCapture.CAPTURE_FAILEXCEPTION
-     * )
-     * private void renderArmor(GuiGraphics context, CallbackInfo info, int left, int top) {
-     *     return drawArmor(context, left, top);
-     * }
-     * }
- *

- * Next, we create a prefixed copy of the method for overloading. - * The remaining captured locals are also removed from the original, as we provide them ourselves. - *

{@code @Inject(
-     *     method = "renderPlayerHealth",
-     *     at = @At(
-     *         value = "INVOKE",
-     *         target = "someInvocation",
-     *     )
-     * )
-     * private void renderArmor(GuiGraphics context, CallbackInfo info) {
-     *
-     * }
-     *
-     * private void adapter$bridge$renderArmor(GuiGraphics context, CallbackInfo info, int left, int top) {
-     *     return drawArmor(context, left, top);
-     * }}
- *

- * Finally, we load the local variable initializer instructions and call the overloaded method. - * Local variables that are referenced multiple times in the original target's code will be stored as local variables - * in the injector for reuse. - *

{@code @Inject(
-     *     method = "renderPlayerHealth",
-     *     at = @At(
-     *         value = "INVOKE",
-     *         target = "someInvocation",
-     *     )
-     * )
-     * private void renderArmor(GuiGraphics context, CallbackInfo info) {
-     *     int shared = context.someMethod();
-     *     return adapter$bridge$renderArmor(context, shared.getLeft(), shared.getTop());
-     * }
-     *
-     * private void adapter$bridge$renderArmor(GuiGraphics context, CallbackInfo info, int left, int top) {
-     *     return drawArmor(context, left, top);
-     * }
-     * }
- */ - private static PatchResult recreateLocalVariables(MethodNode methodNode, MixinContext context, Recipe recipe, ClassNode extractClass) { - LocalVarAnalyzer.CapturedLocals capturedLocals = LocalVarAnalyzer.findCapturedLocals(context, recipe); - if (capturedLocals == null) { - return PatchResult.PASS; - } - int paramLocalStart = capturedLocals.paramLocalStart(); - - // Mixin requires capturing locals in their original order, so we must filter out unused ones - LocalVarAnalyzer.CapturedLocalsTransform transform = LocalVarAnalyzer.analyzeCapturedLocals(capturedLocals, methodNode); - LocalVarAnalyzer.CapturedLocalsUsage usage = transform.getUsage(capturedLocals, context); - // Used captured locals in parameter order - List used = transform.used().stream().sorted().toList(); - LocalVariableLookup targetTable = usage.targetTable(); - Int2ObjectMap usedLocals = usage.usedLocals(); - Int2ObjectMap varInsnLists = usage.varInsnLists(); - Int2IntMap usageCount = usage.usageCount(); - - // Validate all local variable lookups before the method is modified - if (targetTable == null || !usedLocals.keySet().containsAll(used) - || usageCount.int2IntEntrySet().stream().anyMatch(e -> e.getIntValue() > 1 && targetTable.getByIndexOrNull(e.getIntKey()) == null) - ) { - return PatchResult.PASS; - } - - // Remove unused captured locals, if there are any - if (!transform.remover().isEmpty() && transform.remover().apply(context) == PatchResult.PASS) { - return PatchResult.PASS; - } - - // Create a copy of the method to call - MethodNode copy = new MethodNode(Opcodes.ACC_PRIVATE | (capturedLocals.isStatic() ? Opcodes.ACC_STATIC : 0), "adapter$bridge$" + methodNode.name, methodNode.desc, null, null); - methodNode.accept(copy); - copy.visibleAnnotations = null; - copy.invisibleAnnotations = null; - methodNode.instructions = new InsnList(); - // Remove used locals from the original method, as we'll be providing them ourselves - TransformParameters cleanupPatch = TransformParameters.builder() - .chain(b -> IntStream.range(paramLocalStart, paramLocalStart + used.size()).boxed() - .sorted(Collections.reverseOrder()) - .forEach(b::remove)) - .build(); - if (!cleanupPatch.isEmpty()) { - cleanupPatch.apply(context); - } - - // Determine parameter slots of the remaining method parameters - Type[] remainingParams = Type.getArgumentTypes(methodNode.desc); - int[] paramSlots = new int[remainingParams.length]; - int nextSlot = capturedLocals.isStatic() ? 0 : 1; - for (int i = 0; i < remainingParams.length; i++) { - paramSlots[i] = nextSlot; - nextSlot += remainingParams[i].getSize(); - } - - // Save reused variables. Locals that are only referenced once will be inlined instead - InsnList replacementInsns = new InsnList(); - AtomicInteger nextAvailableIndex = new AtomicInteger(nextSlot); - usageCount.forEach((index, count) -> { - if (count == 1) { - int usages = 0; - for (Int2ObjectMap.Entry entry : varInsnLists.int2ObjectEntrySet()) { - for (AbstractInsnNode insn : entry.getValue()) { - if (insn instanceof VarInsnNode varInsn && varInsn.var == index) { - // Copy the initializers, as the variable may be loaded on its own as well - InsnList varInitializers = varInsnLists.get(varInsn.var); - entry.getValue().insert(varInsn, AdapterUtil.insnList(AdapterUtil.cloneInsns(Arrays.asList(varInitializers.toArray())))); - entry.getValue().remove(varInsn); - usages++; - } - } - } - if (usages > 1) { - throw new IllegalStateException("Expected only one reference to variable " + index); - } - } - }); - LabelNode end = new LabelNode(); - Map newIndices = new HashMap<>(); - usageCount.forEach((index, count) -> { - if (count > 1) { - LocalVariableNode node = targetTable.getByIndexOrNull(index); - Type type = Type.getType(node.desc); - int newIndex = nextAvailableIndex.getAndAdd(type.getSize()); - LabelNode start = new LabelNode(); - methodNode.localVariables.add(new LocalVariableNode(node.name, node.desc, node.signature, start, end, newIndex)); - InsnList insns = varInsnLists.get((int) index); - replacementInsns.add(insns); - replacementInsns.add(new VarInsnNode(type.getOpcode(Opcodes.ISTORE), newIndex)); - replacementInsns.add(start); - // Update initializers to point to the new index instead - varInsnLists.remove((int) index); - varInsnLists.forEach((varIndex, varInsns) -> { - for (AbstractInsnNode insn : varInsns) { - if (insn instanceof VarInsnNode varInsn && varInsn.var == index) { - varInsn.var = newIndex; - } - } - }); - newIndices.put(index, newIndex); - } - }); - - // Load the instance and the parameters preceding the captured locals - if (!capturedLocals.isStatic()) { - replacementInsns.add(new VarInsnNode(Opcodes.ALOAD, 0)); - } - for (int i = 0; i < paramLocalStart && i < remainingParams.length; i++) { - replacementInsns.add(new VarInsnNode(remainingParams[i].getOpcode(Opcodes.ILOAD), paramSlots[i])); - } - // Load recreated locals - for (int ordinal : used) { - LocalVariable local = usedLocals.get(ordinal); - InsnList insns = varInsnLists.get(local.index()); - if (insns != null) { - replacementInsns.add(insns); - } else { - int newIndex = newIndices.getOrDefault(local.index(), -1); - if (newIndex == -1) { - throw new IllegalArgumentException("Missing new index for var " + local.index()); - } - replacementInsns.add(new VarInsnNode(local.type().getOpcode(Opcodes.ILOAD), newIndex)); - } - } - // Load the remaining parameters following the captured locals (MixinExtras sugar parameters) - for (int i = paramLocalStart; i < remainingParams.length; i++) { - replacementInsns.add(new VarInsnNode(remainingParams[i].getOpcode(Opcodes.ILOAD), paramSlots[i])); - } - // Call overloaded method - replacementInsns.add(new MethodInsnNode(capturedLocals.isStatic() ? Opcodes.INVOKESTATIC : Opcodes.INVOKEVIRTUAL, extractClass.name, copy.name, copy.desc)); - replacementInsns.add(new LabelNode()); - replacementInsns.add(new InsnNode(OpcodeUtil.getReturnOpcode(methodNode))); - replacementInsns.add(end); - methodNode.instructions = replacementInsns; - - extractClass.methods.add(copy); - return PatchResult.COMPUTE_FRAMES; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/extract/MirrorableExtractMixin.java b/core/src/main/java/org/sinytra/adapter/patch/processor/extract/MirrorableExtractMixin.java deleted file mode 100644 index b27eecd9..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/extract/MirrorableExtractMixin.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.sinytra.adapter.patch.processor.extract; - -import com.google.common.collect.ImmutableList; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ctx.*; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.env.util.TypeConstants; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.OpcodeUtil; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_METHOD; - -// TODO Cleanup -public class MirrorableExtractMixin { - - public static PatchResult apply(MixinContext context, Recipe recipe, String destinationClass, MethodInsnNode destinationMethodInvocation) { - TargetPair dirtyTarget = recipe.getNewCleanTarget(); - if (dirtyTarget == null) return PatchResult.PASS; - - MethodNode methodNode = context.methodNode(); - - Type selfType = Type.getObjectType(dirtyTarget.classNode().name); - Type[] params = Type.getArgumentTypes(destinationMethodInvocation.desc); - int selfIndex = Stream.of(Stream.iterate(0, i -> i < params.length, i -> i + 1) - .filter(i -> params[i].equals(selfType)) - .toList()) - .filter(list -> list.size() == 1) - .map(List::getFirst) - .findFirst() - .orElse(-1); - if (selfIndex == -1) { - return PatchResult.PASS; - } - - List callInsns = MethodAnalyzer.getMethodCallSrcInsns(dirtyTarget.methodNode(), destinationMethodInvocation); - if (callInsns == null || callInsns.size() <= selfIndex) { - return PatchResult.PASS; - } - AbstractInsnNode selfParamInsn = callInsns.get(selfIndex); - if (!(selfParamInsn instanceof VarInsnNode varInsn) || varInsn.getOpcode() != Opcodes.ALOAD || varInsn.var != 0) { - return PatchResult.PASS; - } - List originalParams = Parameters.getParameterTypes(methodNode.desc); - List newParams = ImmutableList.builder().add(Type.getArgumentTypes(destinationMethodInvocation.desc)).add(TypeConstants.CI_TYPE).build(); - // Make sure we have all required params, matching parameters of the same type in order - int[] argMapping = new int[originalParams.size()]; - boolean[] mappedArgs = new boolean[newParams.size()]; - for (int i = 0; i < originalParams.size(); i++) { - int argIndex = -1; - for (int j = 0; j < newParams.size(); j++) { - if (!mappedArgs[j] && newParams.get(j).equals(originalParams.get(i))) { - argIndex = j; - break; - } - } - if (argIndex == -1) { - return PatchResult.PASS; - } - mappedArgs[argIndex] = true; - argMapping[i] = argIndex; - } - - // Change target. The annotation is carried over to the mirror. - context.methodAnnotation() - .setOrAppendNonNull(AT_METHOD, List.of( - MethodQualifier.create(destinationMethodInvocation).asDescriptor() - )); - - String desc = Type.getMethodDescriptor(Type.VOID_TYPE, newParams.toArray(Type[]::new)); - generateMirror(context, destinationClass, desc, dirtyTarget.classNode(), gen -> gen.loadArg(selfIndex), argMapping); - - context.recordCtxAudit("Extract (mirror) mixin to target %s", destinationClass); - return PatchResult.APPLY; - } - - /** - * Generate a mixin method in the generated mixin class for {@code destinationClass}. It carries the original injector annotation - * and calls the original handler, which is turned into a public {@code @Unique} method. - * - * @param handlerOwner the class the original handler is merged into - * @param loadInstance loads the handler's instance, if the handler isn't static - * @param argIndices the mirror parameter to pass as each handler parameter - * @return the generated mirror method - */ - public static MethodNode generateMirror(MixinContext context, String destinationClass, String desc, ClassNode handlerOwner, Consumer loadInstance, int[] argIndices) { - ClassNode classNode = context.classNode(); - MethodNode methodNode = context.methodNode(); - PatchEnvironment environment = context.environment(); - ClassNode generatedTarget = environment.classGenerator().getOrGenerateMixinClass(classNode, destinationClass, null); - environment.refmapHolder().copyEntries(classNode.name, generatedTarget.name); - - // Generate a method with the same injector annotation - String name = methodNode.name + "$adapter$mirror$" + AdapterUtil.randomString(5); - MethodNode mirror = (MethodNode) generatedTarget.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, name, desc, null, null); - mirror.visibleAnnotations = new ArrayList<>(methodNode.visibleAnnotations); - - // Make original mixin a unique public method - methodNode.access = OpcodeUtil.setAccessVisibility(methodNode.access, Opcodes.ACC_PUBLIC); - methodNode.visibleAnnotations.remove(context.methodAnnotation().unwrap()); - methodNode.visitAnnotation(MixinAnnotations.UNIQUE, true); - - // Now call the original mixin - GeneratorAdapter gen = new GeneratorAdapter(mirror, mirror.access, mirror.name, mirror.desc); - gen.newLabel(); - if (!MethodHelper.isStatic(methodNode)) { - loadInstance.accept(gen); - } - for (int argIndex : argIndices) { - gen.loadArg(argIndex); - } - invokeHandler(gen, methodNode, handlerOwner); - gen.newLabel(); - gen.returnValue(); - gen.newLabel(); - gen.endMethod(); - return mirror; - } - - static void invokeHandler(GeneratorAdapter gen, MethodNode handler, ClassNode handlerOwner) { - boolean isInterface = (handlerOwner.access & Opcodes.ACC_INTERFACE) != 0; - int opcode = MethodHelper.isStatic(handler) ? Opcodes.INVOKESTATIC : isInterface ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL; - gen.visitMethodInsn(opcode, handlerOwner.name, handler.name, handler.desc, isInterface); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/extract/ProxyExtractMixinSub.java b/core/src/main/java/org/sinytra/adapter/patch/processor/extract/ProxyExtractMixinSub.java deleted file mode 100644 index 24112769..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/extract/ProxyExtractMixinSub.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.sinytra.adapter.patch.processor.extract; - -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.processor.Processor; -import org.sinytra.adapter.analysis.locals.LocalVarAnalyzer; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.IntStream; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.PROPERTY_ARGS_ONLY; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.PROPERTY_INDEX; - -// Create an identical mixin in the destination class and call the old one -public class ProxyExtractMixinSub implements Processor { - public static final ProxyExtractMixinSub INSTANCE = new ProxyExtractMixinSub(); - - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - if (recipe.clean().getTargetClass().equals(dirty.getTargetClass())) - return TxResult.PASS; - - if (!dirty.hasProperty(SpecialKeys.EXTRACT_ORIGIN_PARAM) || LocalVarAnalyzer.capturesLocals(context)) - return TxResult.FAIL; - - TargetPair dirtyTarget = recipe.getNewCleanTarget(); - if (dirtyTarget == null) return TxResult.FAIL; - - MethodNode methodNode = context.methodNode(); - - int originParamIndex = dirty.getProperty(SpecialKeys.EXTRACT_ORIGIN_PARAM).orElseThrow(); - if (dirty.getTargetMethod() == null || dirty.getTargetMethod().desc() == null) return TxResult.FAIL; - - List destParams = Parameters.getParameterTypes(dirty.getTargetMethod().desc()); - Type instanceType = destParams.get(originParamIndex); - String destinationClass = dirty.getTargetClass(); - - // Cool, out instance is passed into the method. Now let's inject there and call the old mixin method - List originalParams = Parameters.getParameterTypes(methodNode.desc); - List newParams = new ArrayList<>(originalParams); - int capturedInstanceParamIndex = newParams.size(); - newParams.add(instanceType); - String newDesc = Type.getMethodDescriptor(Type.getReturnType(methodNode.desc), newParams.toArray(Type[]::new)); - - // The original handler is merged into the original target class - Type handlerOwner = Type.getObjectType(dirtyTarget.classNode().name); - MethodNode mirror = MirrorableExtractMixin.generateMirror(context, destinationClass, newDesc, dirtyTarget.classNode(), gen -> { - gen.loadArg(capturedInstanceParamIndex); - if (!instanceType.equals(handlerOwner)) { - gen.checkCast(handlerOwner); - } - }, IntStream.range(0, originalParams.size()).toArray()); - // Add @Local to captured instance param - AnnotationVisitor visitor = mirror.visitParameterAnnotation(capturedInstanceParamIndex, MixinAnnotations.LOCAL, false); - visitor.visit(PROPERTY_ARGS_ONLY, true); - visitor.visit(PROPERTY_INDEX, originParamIndex); - mirror.maxLocals = mirror.maxStack = 999; - - context.recordCtxAudit("Proxy (mirror) mixin to target %s", destinationClass); - return TxResult.SUCCESS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/redirect/DivertRedirectProcessor.java b/core/src/main/java/org/sinytra/adapter/patch/processor/redirect/DivertRedirectProcessor.java deleted file mode 100644 index ecdd9f2c..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/redirect/DivertRedirectProcessor.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.sinytra.adapter.patch.processor.redirect; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.commons.InstructionAdapter; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.processor.Processor; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.analysis.InsnTraversal; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; - -import java.util.function.Consumer; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; - -public class DivertRedirectProcessor implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - Consumer patcher = dirty.getProperty(SpecialKeys.REDIRECT_ADAPTER).orElse(null); - if (patcher == null) return TxResult.PASS; - - if (!recipe.hasInjectionPointValue(AT_VAL_INVOKE)) return TxResult.PASS; - MethodQualifier target = dirty.getAtData().getTargetQualifier().orElse(null); - - if (target != null) { - MethodNode methodNode = context.methodNode(); - boolean applied = false; - for (MethodInsnNode minsn : MethodAnalyzer.findCalls(methodNode, target)) { - AbstractInsnNode previousLabel = InsnTraversal.find(minsn, InsnTraversal.BACKWARD, LabelNode.class::isInstance); - if (previousLabel != null) { - MethodNode dummy = new MethodNode(); - InstructionAdapter adapter = new InstructionAdapter(dummy); - - LabelNode gotoTarget = new LabelNode(); - dummy.instructions.add(gotoTarget); - - patcher.accept(adapter); - - methodNode.instructions.insert(minsn, dummy.instructions); - methodNode.instructions.insert(previousLabel, new JumpInsnNode(Opcodes.GOTO, gotoTarget)); - applied = true; - } - } - if (applied) { - return TxResult.SUCCESS; - } - } - - return TxResult.PASS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpAnalyzer.java b/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpAnalyzer.java deleted file mode 100644 index f898d25a..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpAnalyzer.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.sinytra.adapter.patch.processor.wrapop; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.SourceValue; -import org.sinytra.adapter.analysis.selector.FrameUtil; -import org.sinytra.adapter.util.AdapterUtil; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class WrapOpAnalyzer { - public static List> groupArrayInitializers(MethodNode methodNode, AbstractInsnNode newArrayInsn) { - // 1. Analyze the method frames to track data flow - Frame[] frames = FrameUtil.getStableFrames(methodNode); - - // Map to hold the AASTORE instruction for each array index - Map indexToStoreNode = new HashMap<>(); - - // 2. Scan for AASTORE instructions targeting our array - InsnList instructions = methodNode.instructions; - for (int i = 0; i < instructions.size(); i++) { - AbstractInsnNode insn = instructions.get(i); - - if (insn.getOpcode() == Opcodes.AASTORE) { - Frame frame = frames[i]; - if (frame == null) continue; - - // Stack for AASTORE: [..., arrayRef, index, value] - // arrayRef is at stack size - 3 - if (frame.getStackSize() < 3) continue; - SourceValue arrayRefSource = frame.getStack(frame.getStackSize() - 3); - - // Check if this AASTORE is operating on the array created by newArrayInsn - if (arrayRefSource.insns.contains(newArrayInsn)) { - // Get the index (stack size - 2) - SourceValue indexSource = frame.getStack(frame.getStackSize() - 2); - Integer constantIndex = resolveConstantIndex(indexSource); - - if (constantIndex != null) { - indexToStoreNode.put(constantIndex, insn); - } - } - } - } - - // 3. Group the instructions linearly - // We assume the compiler emits instructions in contiguous blocks for each element - List> groups = new ArrayList<>(); - - // Start looking after the creation of the array - AbstractInsnNode currentStart = newArrayInsn.getNext(); - - // Find the maximum index we encountered to size our list - int maxIndex = indexToStoreNode.keySet().stream().max(Integer::compare).orElse(-1); - - for (int i = 0; i <= maxIndex; i++) { - AbstractInsnNode endNode = indexToStoreNode.get(i); - List currentGroup = new ArrayList<>(); - - if (endNode != null) { - // Collect everything from currentStart up to (and including) the AASTORE - AbstractInsnNode ptr = currentStart; - while (ptr != null) { - currentGroup.add(ptr); - if (ptr == endNode) break; - ptr = ptr.getNext(); - } - // Advance start pointer for the next group - currentStart = endNode.getNext(); - } - - groups.add(currentGroup); - } - - return groups; - } - - // Helper to extract the integer value from a SourceValue - private static Integer resolveConstantIndex(SourceValue source) { - for (AbstractInsnNode insn : source.insns) { - int index = AdapterUtil.getIntConstValue(insn).orElse(-1); - if (index != -1) { - return index; - } - } - // Could not resolve to a static constant - return null; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/CompoundResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/CompoundResolver.java deleted file mode 100644 index d470ac0f..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/CompoundResolver.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.sinytra.adapter.patch.resolver; - -import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; - -import java.util.ArrayList; -import java.util.List; - -public abstract class CompoundResolver implements Resolver { - protected final List subResolvers = new ArrayList<>(); - - public CompoundResolver addSubResolverFirst(SubResolver subResolver) { - this.subResolvers.addFirst(subResolver); - return this; - } - - public CompoundResolver addSubResolver(SubResolver subResolver) { - this.subResolvers.add(subResolver); - return this; - } - - protected abstract boolean canApply(Recipe recipe); - - @Nullable - protected Configuration tryReuse(MixinContext context, Recipe recipe) { - return null; - } - - @Nullable - protected Configuration useFallback(MixinContext context, Recipe recipe) { - return null; - } - - @Override - public ResolutionResult resolve(MixinContext context, Recipe recipe) { - if (!canApply(recipe)) { - return ResolutionResult.pass(); - } - - Configuration resused = tryReuse(context, recipe); - if (resused != null) { - return ResolutionResult.success(resused); - } - - for (SubResolver subResolver : this.subResolvers) { - Configuration result = context.audited(subResolver, () -> subResolver.resolve(context, recipe)); - if (result != null) { - return ResolutionResult.success(result); - } - } - - Configuration fallback = useFallback(context, recipe); - if (fallback != null) { - return ResolutionResult.success(fallback); - } - - return ResolutionResult.fail(); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/SubResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/SubResolver.java deleted file mode 100644 index 0b70d05a..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/SubResolver.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.sinytra.adapter.patch.resolver; - -import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; - -public interface SubResolver { - @Nullable - Configuration resolve(MixinContext context, Recipe recipe); -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ArbitraryInjectionPointSubResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ArbitraryInjectionPointSubResolver.java deleted file mode 100644 index cfcb8269..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ArbitraryInjectionPointSubResolver.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.InsnComparator; -import org.sinytra.adapter.analysis.InstructionMatcher; -import org.sinytra.adapter.analysis.method.MethodInsnMatcher; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.mixin.MixinFlag; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.analysis.InsnTraversal; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.function.UnaryOperator; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; - -public class ArbitraryInjectionPointSubResolver implements SubResolver { - @Nullable - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - String injectionPointTarget = recipe.clean().getAtData().getTarget().orElse(null); - TargetPair dirtyTarget = recipe.getDirtyTarget(); - AbstractInsnNode cleanInsn = recipe.getCleanInjectionInsn(); - if (dirtyTarget == null || cleanInsn == null) return null; - - MethodInsnNode targetMethodCall = resolveTargetCallInsn(dirtyTarget.methodNode(), cleanInsn, context, injectionPointTarget); - if (targetMethodCall == null) return null; - - if (targetMethodCall.owner.equals(dirtyTarget.classNode().name)) { - TargetPair target = context.methods().findOwnMethodPair(context.dirtyLookup(), MethodQualifier.create(targetMethodCall)); - if (target == null) return null; - - // New method is in the same class? It's possible our target injection point was moved there - if (context.methods().hasInjectionTargetInsns(target)) { - return MutableConfiguration.create().setTargetMethod(targetMethodCall); - } - } - - return MutableConfiguration.create().setAtData(AtData.create(AT_VAL_INVOKE, targetMethodCall)); - } - - private MethodInsnNode resolveTargetCallInsn(MethodNode dirtyTargetMethod, AbstractInsnNode cleanInjectionInsn, MixinContext context, @Nullable String injectionPointTarget) { - AbstractInsnNode nextCallCandidate = findCandidates(MethodInsnMatcher.findBackwardsInstructions(cleanInjectionInsn).inverse(), dirtyTargetMethod, List::getLast); - - if (nextCallCandidate != null) { - return findReplacementInjectionPoint(nextCallCandidate, AbstractInsnNode::getNext, context, injectionPointTarget); - } else { - AbstractInsnNode previousCallCandidate = findCandidates(MethodInsnMatcher.findForwardInstructions(cleanInjectionInsn), dirtyTargetMethod, List::getFirst); - if (previousCallCandidate != null) { - return findReplacementInjectionPoint(previousCallCandidate, AbstractInsnNode::getPrevious, context, injectionPointTarget); - } - } - - return null; - } - - private static AbstractInsnNode findCandidates(InstructionMatcher cleanMatcher, MethodNode dirtyTargetMethod, Function, AbstractInsnNode> selector) { - // Find an common instruction in the clean and dirty target methods - if (cleanMatcher.after().isEmpty()) return null; - int firstOpcode = cleanMatcher.after().getFirst().getOpcode(); - List candidates = new ArrayList<>(); - - for (AbstractInsnNode insn : dirtyTargetMethod.instructions) { - if (InsnTraversal.isDebugNode(insn) || insn.getOpcode() != firstOpcode) { - continue; - } - - InstructionMatcher dirtyMatcher = MethodInsnMatcher.findForwardInstructionsDirect(insn); - if (cleanMatcher.test(dirtyMatcher, InsnComparator.IGNORE_VAR_INDEX)) { - // Find first method call past matched instruction - AbstractInsnNode lastInsn = selector.apply(dirtyMatcher.after()); - if (lastInsn != null) { - candidates.add(lastInsn); - } - } - } - - return !candidates.isEmpty() ? candidates.getFirst() : null; - } - - private static MethodInsnNode findReplacementInjectionPoint(AbstractInsnNode lastInsn, UnaryOperator flow, MixinContext context, @Nullable String injectionPointTarget) { - // Require matching return types for ModifyExpressionValue mixins - if (context.hasFlag(MixinFlag.RETURN_TYPE_SENSITIVE) && injectionPointTarget != null) { - Type desiredReturnType = Type.getReturnType(injectionPointTarget); - return (MethodInsnNode) InsnTraversal.find(lastInsn, flow, - v -> v instanceof MethodInsnNode minsn && Type.getReturnType(minsn.desc).equals(desiredReturnType)); - } - return (MethodInsnNode) InsnTraversal.find(lastInsn, flow, v -> v instanceof MethodInsnNode); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/AtVariableAssignStoreSubResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/AtVariableAssignStoreSubResolver.java deleted file mode 100644 index 65abd8f7..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/AtVariableAssignStoreSubResolver.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.mixin.MixinFlag; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.util.OpcodeUtil; -import org.sinytra.adapter.analysis.locals.LocalVarAnalyzer; -import org.sinytra.adapter.analysis.InsnTraversal; -import org.sinytra.adapter.env.ctx.Targets; - -import java.util.List; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; - -/** - * Find our new injection point with relation to variable assignments - */ -public class AtVariableAssignStoreSubResolver implements SubResolver { - @Nullable - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - if (!recipe.hasInjectionPointValue(AT_VAL_INVOKE)) return null; - - Targets targets = recipe.targets(); - if (targets == null) return null; - AbstractInsnNode cleanInsn = recipe.getCleanInjectionInsn(); - if (cleanInsn == null) return null; - - // Check that the following instruction is a store operation - AbstractInsnNode next = findNextUsefulInsn(cleanInsn); - if (!(next instanceof VarInsnNode varInsn) || !OpcodeUtil.isStoreOpcode(varInsn.getOpcode())) { - return null; - } - // Find matching local in dirty target method - LocalVariableLookup cleanTable = recipe.cleanLocalsTable(); - LocalVariableLookup dirtyTable = recipe.dirtyLocalsTable(); - if (cleanTable == null || dirtyTable == null) { - return null; - } - LocalVariableNode cleanLocal = cleanTable.getByIndexOrNull(varInsn.var); - if (cleanLocal == null) { - return null; - } - List cleanLocals = cleanTable.getForType(cleanLocal); - List dirtyLocals = dirtyTable.getForType(cleanLocal); - if (cleanLocals.size() != dirtyLocals.size()) { - return null; - } - LocalVariableNode dirtyLocal = dirtyLocals.get(cleanLocals.indexOf(cleanLocal)); - // Find store insns - List cleanStoreInsns = LocalVarAnalyzer.findStoreInsns(targets.clean().methodNode(), cleanLocal.index); - int cleanStoreInsnIndex = cleanStoreInsns.indexOf(varInsn); - if (cleanStoreInsnIndex == -1) { - return null; - } - List dirtyStoreInsns = LocalVarAnalyzer.findStoreInsns(targets.dirty().methodNode(), dirtyLocal.index); - if (cleanStoreInsns.size() != dirtyStoreInsns.size()) { - return null; - } - AbstractInsnNode dirtyStoreInsn = dirtyStoreInsns.get(cleanStoreInsnIndex); - // Find first method call before dirty store - MethodInsnNode previousMethodCall = (MethodInsnNode) InsnTraversal.find(dirtyStoreInsn, InsnTraversal.BACKWARD, i -> i instanceof MethodInsnNode); - if (previousMethodCall == null) { - return null; - } - - // In case the mixin is call-sensitive, we try to keep the orignal injection point if the method was moved - if (context.hasFlag(MixinFlag.AT_TARGET_SENSITIVE) && !previousMethodCall.owner.equals(targets.dirty().classNode().name)) { - return null; - } - - // All checks have passed, proceed to patch method - return MutableConfiguration.create() - .setAtData(recipe.clean().getAtData().withTarget(previousMethodCall)); - } - - private static AbstractInsnNode findNextUsefulInsn(AbstractInsnNode insn) { - return InsnTraversal.find(insn, InsnTraversal.FORWARD, i -> !(i instanceof TypeInsnNode || i instanceof LabelNode || InsnTraversal.isDebugNode(i))); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ComparingInjectionPointResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ComparingInjectionPointResolver.java deleted file mode 100644 index e87e91ad..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ComparingInjectionPointResolver.java +++ /dev/null @@ -1,253 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import com.google.common.collect.Multimap; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ann.ConstantData; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.MethodParameters.ParamGroup; -import org.sinytra.adapter.env.param.Parameter; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.env.util.TypeConstants; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.*; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.processor.wrapop.WrapOpSurgeon; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.analysis.MethodLabelComparator; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.env.ctx.Targets; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.PROPERTY_ORDINAL; - -public abstract class ComparingInjectionPointResolver implements SubResolver { - - public static class Inject extends ComparingInjectionPointResolver { - @Nullable - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - Targets targets = recipe.targets(); - if (targets == null) return null; - - AbstractInsnNode cleanInsn = recipe.getCleanInjectionInsn(); - MethodLabelComparator.ComparisonResult comparisonResult = MethodLabelComparator.findPatchedLabels(cleanInsn, recipe); - if (comparisonResult == null) return null; - - List> hunkLabels = comparisonResult.patchedLabels(); - - ClassNode cleanTargetClass = targets.clean().classNode(); - for (List insns : hunkLabels) { - for (AbstractInsnNode insn : insns) { - if (insn instanceof MethodInsnNode minsn && minsn.getOpcode() == Opcodes.INVOKESTATIC && !minsn.owner.equals(cleanTargetClass.name)) { - Configuration result = attemptExtractMixin(minsn, context, recipe.clean()); - if (result != null) { - return result; - } - } - } - } - - // If extraction fails, fall back to injecting at the nearest method call in dirty code - for (List insns : hunkLabels) { - for (AbstractInsnNode insn : insns) { - if (insn instanceof MethodInsnNode minsn) { - return MutableConfiguration.create() - .setAtData(AtData.create(AT_VAL_INVOKE, minsn)); - } - } - } - return null; - } - - private static Configuration attemptExtractMixin(MethodInsnNode minsn, MixinContext context, Configuration clean) { - // Looks like some code was moved into a static method outside this class - // Attempt extracting mixin - TargetPair target = context.methods().findMethodPair(context.dirtyLookup(), MethodQualifier.create(minsn)); - if (target == null) return null; - -// MethodUpgrader.adjustInjectorOrdinalForNewMethod(minsn, context.legacy()); FIXME - List newTargetInsns = context.methods().findInjectionTargetInsns(target, true); - if (newTargetInsns.isEmpty()) return null; - - return MutableConfiguration.create() - .setTargetClass(target.classNode().name) - .setTargetMethod(minsn) - .setAtData(clean.getAtData().withOrdinal(null)) // TODO Find new ordinal - .setProperty(SpecialKeys.EXTRACT_TARGET, minsn); - } - } - - public static class WrapOperation extends ComparingInjectionPointResolver { - @Nullable - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - TargetPair dirtyTarget = recipe.getDirtyTarget(); - if (dirtyTarget == null || recipe.getCleanTarget() == null) return null; - - AbstractInsnNode cleanInsn = recipe.getCleanInjectionInsn(); - if (!(cleanInsn instanceof MethodInsnNode minsn)) return null; - - MethodLabelComparator.ComparisonResult comparisonResult = MethodLabelComparator.findPatchedLabels(cleanInsn, recipe); - if (comparisonResult == null) return null; - - List> hunkLabels = comparisonResult.patchedLabels(); - - return handleWrapOperationToInstanceOf(recipe, minsn, comparisonResult.cleanLabel(), hunkLabels, context) - .or(() -> handleWrapOperationAdaptedTarget(hunkLabels)) - .or(() -> handleWrapOperationNewInjectionPoint(minsn, comparisonResult.cleanLabel(), hunkLabels)) - .or(() -> handleTargetModification(hunkLabels, dirtyTarget, context)) - .orElse(null); - } - - private static Optional handleWrapOperationToInstanceOf(Recipe recipe, MethodInsnNode cleanInjectionInsn, List cleanLabel, List> hunkLabels, MixinContext context) { - if (hunkLabels.size() != 1 || !(cleanLabel.getLast() instanceof JumpInsnNode) || cleanLabel.stream().anyMatch(i -> i instanceof TypeInsnNode)) - return Optional.empty(); - - List dirtyLabel = hunkLabels.getFirst(); - if (!(dirtyLabel.getLast() instanceof JumpInsnNode)) return Optional.empty(); - - List instanceOfCalls = dirtyLabel.stream() - .filter(TypeInsnNode.class::isInstance) - .map(TypeInsnNode.class::cast) - .toList(); - if (instanceOfCalls.size() != 1) return Optional.empty(); - TypeInsnNode instanceOfCall = instanceOfCalls.getFirst(); - - MethodNode methodNode = context.methodNode(); - LocalVariableLookup lvt = context.methods().getLVT(methodNode); - if (lvt == null) return Optional.empty(); - - LocalVariableNode instanceLocal = lvt.getByParameterOrdinalOrNull(0); - if (instanceLocal == null) return Optional.empty(); - - Configuration clean = recipe.clean(); - List inheritedParams = clean.getParameters().getTypes(ParamGroup.METHOD_PARAMS); - Multimap usedVars = WrapOpSurgeon.getUsedVars(lvt, inheritedParams, context); - - MutableConfiguration config = recipe.dirty().copyClean() - .removeProperty(MixinKeys.TARGET_AT) - .setProperty(MixinKeys.TARGET_CONSTANT, ConstantData.classValue(Type.getObjectType(instanceOfCall.desc))) - .inheritSignature(); - MethodParameters parameters = config.getParameters(); - Parameter newInstanceParam = Parameter.simple(TypeConstants.OBJECT_TYPE); - parameters.set(ParamGroup.METHOD_PARAMS, List.of(newInstanceParam)); - - if (usedVars.containsKey(instanceLocal.index)) { - TargetPair cleanTarget = recipe.getCleanTarget(); - LocalVariableLookup cleanLocals = recipe.cleanLocalsTable(); - LocalVariableLookup dirtyLocals = recipe.dirtyLocalsTable(); - if (cleanTarget == null || cleanLocals == null || dirtyLocals == null) return Optional.empty(); - - List originalCallArgs = MethodAnalyzer.getMethodCallSrcInsns(cleanTarget.methodNode(), cleanInjectionInsn); - if (originalCallArgs == null || originalCallArgs.isEmpty() || !(originalCallArgs.getFirst() instanceof VarInsnNode originalInstanceInsn)) - return Optional.empty(); - LocalVariableNode originalInstanceLocal = cleanLocals.getByIndexOrNull(originalInstanceInsn.var); - if (originalInstanceLocal == null) return Optional.empty(); - int cleanOrdinal = cleanLocals.getTypedOrdinal(originalInstanceLocal).orElse(-1); - if (cleanOrdinal == -1) return Optional.empty(); - - LocalVariableNode dirtyLocal = dirtyLocals.getByTypedOrdinal(Type.getType(instanceLocal.desc), cleanOrdinal).orElse(null); - if (dirtyLocal == null) return Optional.empty(); - - Parameter replacementInstanceParam = Parameter.builder(dirtyLocal.desc) - .annotate(MixinAnnotations.LOCAL, b -> b - .put(PROPERTY_ORDINAL, cleanOrdinal)) - .build(); - parameters.add(ParamGroup.LOCALS, replacementInstanceParam); - // TODO exclude original.call(...) insns from being mapped - parameters.mapParameter(newInstanceParam, replacementInstanceParam); - } - - // TODO Find a way to get rid of this ugly hack - // Use "state.getBlock()" as the new Block parameter - if (instanceOfCall.getPrevious() instanceof MethodInsnNode m) { - String loadedType = Type.getReturnType(m.desc).getDescriptor(); - for (int paramVar : usedVars.keySet()) { - if (paramVar == instanceLocal.index) { - continue; - } - - LocalVariableNode node = lvt.getByIndexOrNull(paramVar); - if (node == null || !loadedType.equals(node.desc)) return Optional.empty(); - - int paramOrdinal = lvt.getParameterOrdinal(node); - List cleanParams = clean.getParameters().get(ParamGroup.METHOD_PARAMS); - if (paramOrdinal < 0 || paramOrdinal >= cleanParams.size()) return Optional.empty(); - - Parameter oldParam = cleanParams.get(paramOrdinal); - parameters.mapParameter(oldParam, newInstanceParam); - } - } - - return Optional.of(config); - } - - private static Optional handleWrapOperationAdaptedTarget(List> hunkLabels) { - if (hunkLabels.size() != 1) return Optional.empty(); - - List methodCalls = hunkLabels.getFirst().stream() - .filter(MethodInsnNode.class::isInstance) - .map(MethodInsnNode.class::cast) - .toList(); - if (methodCalls.size() != 1) return Optional.empty(); - MethodInsnNode insn = methodCalls.getLast(); - - return Optional.of(MutableConfiguration.create() - .setAtData(AtData.create(AT_VAL_INVOKE, insn))); - } - - private static Optional handleWrapOperationNewInjectionPoint(MethodInsnNode cleanInjectionInsn, List cleanLabel, List> hunkLabels) { - if (hunkLabels.size() != 1) - return Optional.empty(); - - Type cleanReturnType = Type.getReturnType(cleanInjectionInsn.desc); - List dirtyLabel = hunkLabels.getFirst(); - List cleanMethodCalls = cleanLabel.stream() - .filter(i -> i instanceof MethodInsnNode m - && m.owner.equals(cleanInjectionInsn.owner) - && Type.getReturnType(m.desc).equals(cleanReturnType)) - .map(i -> ((MethodInsnNode) i).name) - .toList(); - - List methodCalls = dirtyLabel.stream() - .filter(i -> i instanceof MethodInsnNode m && m.owner.equals(cleanInjectionInsn.owner) - && Type.getReturnType(m.desc).equals(cleanReturnType) - && !cleanMethodCalls.contains(m.name)) - .map(MethodInsnNode.class::cast) - .toList(); - if (methodCalls.size() != 1) return Optional.empty(); - - MethodInsnNode dirtyMinsn = methodCalls.getFirst(); - return Optional.of(MutableConfiguration.create() - .setAtData(AtData.create(AT_VAL_INVOKE, dirtyMinsn))); - } - - private static Optional handleTargetModification(List> hunkLabels, TargetPair dirtyTarget, MixinContext context) { - ClassNode dirtyClass = dirtyTarget.classNode(); - return hunkLabels.stream() - .flatMap(Collection::stream) - .filter(insn -> insn instanceof MethodInsnNode minsn && minsn.owner.equals(dirtyClass.name)) - .map(MethodInsnNode.class::cast) - .filter(minsn -> { - TargetPair pair = context.methods().findMethodPair(context.dirtyLookup(), MethodQualifier.create(minsn)); - return pair != null && context.methods().hasInjectionTargetInsns(pair); - }) - .map(minsn -> MutableConfiguration.create() - .setAtData(AtData.create(AT_VAL_INVOKE, minsn))) - .findFirst(); - } - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InheritedInjectionPointSubResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InheritedInjectionPointSubResolver.java deleted file mode 100644 index 15d1e8e6..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InheritedInjectionPointSubResolver.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.MethodParameters.ParamGroup; -import org.sinytra.adapter.env.param.Parameter; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.mixin.MixinFlag; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.env.ctx.PatchContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.types.BytecodeFixerUpper; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.analysis.InsnTraversal; - -import java.util.List; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; - -public class InheritedInjectionPointSubResolver implements SubResolver { - @Nullable - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - if (!recipe.hasInjectionPointValue(AT_VAL_INVOKE)) return null; - - AtData at = recipe.clean().getAtData(); - MethodQualifier atTarget = at.getTargetQualifier().orElse(null); - if (atTarget == null || atTarget.internalOwnerName() == null) return null; - - TargetPair targetPair = recipe.getDirtyTarget(); - if (targetPair == null) return null; - if (context.methods().hasInjectionTargetInsns(targetPair)) return null; - - String owner = atTarget.internalOwnerName(); - for (AbstractInsnNode insn : targetPair.methodNode().instructions) { - if (insn instanceof MethodInsnNode minsn - && minsn.name.equals(atTarget.name()) && minsn.desc.equals(atTarget.desc()) && !minsn.owner.equals(owner) - && (context.environment().inheritanceHandler(context.dirtyLookup()).isClassInherited(minsn.owner, owner) || isFixedField(minsn, context.patchContext())) - ) { - MutableConfiguration config = MutableConfiguration.create(); - config.setAtData(at.withTarget(minsn)); - - if (context.hasFlag(MixinFlag.AT_TARGET_SENSITIVE) && minsn.getOpcode() != Opcodes.INVOKESTATIC) { - MethodParameters params = recipe.clean().getParameters().copy(); - List callParams = params.get(ParamGroup.METHOD_PARAMS); - if (!callParams.isEmpty()) { - Parameter first = callParams.getFirst().extend() - .annotate(MixinAnnotations.COERCE, b -> b.visible(false)) - .build(); - callParams.set(0, first); - config.setParameters(params); - } - } - - return config; - } - } - - return null; - } - - private boolean isFixedField(AbstractInsnNode insn, PatchContext context) { - BytecodeFixerUpper bfu = context.environment().bytecodeFixerUpper(); - return bfu != null && InsnTraversal.untilLabel(insn, InsnTraversal.BACKWARD).stream() - .filter(FieldInsnNode.class::isInstance) - .map(FieldInsnNode.class::cast) - .findFirst() - .map(finsn -> bfu.getFieldTypeChange(finsn.owner, finsn.name) != null) - .orElse(false); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointResolver.java deleted file mode 100644 index d20851f4..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointResolver.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.resolver.CompoundResolver; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.List; - -public class InjectionPointResolver extends CompoundResolver { - - public InjectionPointResolver() { - addSubResolver(new OverloadedInjectionPointSubResolver()); - addSubResolver(new InheritedInjectionPointSubResolver()); - addSubResolver(InjectionPointSubResolvers.REPLACED_TYPE); - addSubResolver(InjectionPointSubResolvers.EXTRACTED_CALL); - addSubResolver(InjectionPointSubResolvers.EXTRACTED_CODEPATH_CALL); - } - - @Override - protected boolean canApply(Recipe recipe) { - return recipe.dirty().getAtData() == null; - } - - @Nullable - @Override - protected Configuration tryReuse(MixinContext context, Recipe recipe) { - Configuration dirty = recipe.dirty(); - - String targetClass = dirty.getTargetClass(); - if (targetClass == null) return null; - - MethodQualifier targetQualifier = dirty.getTargetMethod(); - if (targetQualifier == null) return null; - - TargetPair dirtyTarget = context.methods().findMethodPair(context.dirtyLookup(), targetQualifier.withOwner(Type.getObjectType(targetClass))); - if (dirtyTarget == null) return null; - - // Try reusing the original - List insns = context.methods().findInjectionTargetInsns(dirtyTarget); - if (!insns.isEmpty()) { - return dirty.copyClean() - .inheritAtData(); - } - - return null; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointSubResolvers.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointSubResolvers.java deleted file mode 100644 index 80248398..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointSubResolvers.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import com.google.common.collect.Multimap; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.InstructionMatcher; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.analysis.method.MethodInsnMatcher; -import org.sinytra.adapter.analysis.tree.CodePaths; -import org.sinytra.adapter.analysis.tree.CodePaths.CodePath; -import org.sinytra.adapter.analysis.tree.CodePaths.CodePathStep; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.env.util.WeighedDisambiguation; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.env.ctx.Targets; - -import java.util.List; -import java.util.Objects; -import java.util.Set; - -public class InjectionPointSubResolvers { - public static final SubResolver REPLACED_TYPE = (MixinContext context, Recipe recipe) -> { - Targets targets = recipe.targets(); - if (targets == null) return null; - - // Find single clean target minsn - List insns = context.methods().findInjectionTargetInsns(targets.clean()); - if (insns.isEmpty() || !(insns.getFirst() instanceof MethodInsnNode cleanInsn)) return null; - - InstructionMatcher cleanMatcher = MethodInsnMatcher.findSurroundingInstructions(cleanInsn); - Multimap dirtyCalls = MethodAnalyzer.getMethodCalls(targets.dirty().methodNode()); - List dirtyMatchers = dirtyCalls.values().stream() - .map(MethodInsnMatcher::findSurroundingInstructions) - .toList(); - - WeighedDisambiguation magicBlackBox = WeighedDisambiguation.builder() - .match(() -> testMatchers(context, cleanInsn, cleanMatcher, dirtyMatchers, false)) - .match(() -> testMatchers(context, cleanInsn, cleanMatcher, dirtyMatchers, true)) - .match(() -> testOverloadedMethods(context, cleanInsn, targets.clean(), targets.dirty())) - .resultsEqual(MethodQualifier::equals) - .build(); - - MethodQualifier replacement = magicBlackBox.findBestMatch(); - if (replacement != null) { - return MutableConfiguration.create() - .setAtData(recipe.clean().getAtData().withTarget(replacement)); - } - - return null; - }; - - public static final SubResolver EXTRACTED_CALL = (MixinContext context, Recipe recipe) -> { - if (context.isStatic()) return null; - - Targets targets = recipe.targets(); - if (targets == null) return null; - - for (MethodInsnNode minsn : MethodAnalyzer.findDirtyOnlyCalls(targets.clean().methodNode(), targets.dirty().methodNode(), true)) { - // We only want static external methods - if (minsn.getOpcode() != Opcodes.INVOKESTATIC || minsn.owner.equals(targets.dirty().classNode().name)) - continue; - - List argInsns = MethodAnalyzer.getMethodCallSrcInsns(targets.dirty().methodNode(), minsn); - if (argInsns == null) continue; - - AbstractInsnNode self = argInsns.stream() - .filter(i -> i instanceof VarInsnNode vinsn && vinsn.var == 0) - .findFirst() - .orElse(null); - if (self == null) continue; - - int selfParamIndex = argInsns.indexOf(self); - MethodQualifier target = MethodQualifier.create(minsn); - - TargetPair pair = context.methods().findMethodPair(context.dirtyLookup(), target); - if (pair != null && context.methods().hasInjectionTargetInsns(pair)) { - return recipe.dirty().copyClean() - .setTargetClass(minsn.owner) - .setTargetMethod(minsn) - .inheritAtData() - .setProperty(SpecialKeys.EXTRACT_ORIGIN_PARAM, selfParamIndex) - .inheritProperyIfAbsent(MixinKeys.INDEX) - .inheritProperyIfAbsent(MixinKeys.ORDINAL); - } - } - - return null; - }; - - public static final SubResolver EXTRACTED_CODEPATH_CALL = (MixinContext context, Recipe recipe) -> { - Targets targets = recipe.targets(); - if (targets == null) return null; - if (!(recipe.getCleanInjectionInsn() instanceof MethodInsnNode minsn)) return null; - - Set newCalls = Set.copyOf(MethodAnalyzer.findDirtyOnlyCalls(targets.clean().methodNode(), targets.dirty().methodNode(), true)); - if (newCalls.isEmpty()) return null; - Set trackLocals = context.isStatic() ? Set.of() : Set.of(0); - - TargetPair to = context.methods().findInheritedMethodPair(context.dirtyLookup(), MethodQualifier.create(minsn)); - CodePath path = CodePaths.findCodePath(targets.dirty(), to, trackLocals, context, newCalls::contains); - if (path == null) return null; - - CodePathStep last = path.steps().getLast(); - return recipe.dirty().copyClean() - .setTargetClass(last.from().classNode()) - .setTargetMethod(last.from().methodNode()) - .inheritAtData(); - }; - - private static List testMatchers(MixinContext context, MethodInsnNode cleanInsn, InstructionMatcher cleanMatcher, List dirtyMatchers, boolean partial) { - return dirtyMatchers.stream() - .map(m -> { - boolean before = cleanMatcher.testBefore(m); - boolean after = cleanMatcher.testAfter(m); - boolean match = partial ? before || after : before && after; - - MethodInsnNode dirtyInsn = (MethodInsnNode) m.insn(); - if (match && matchesMethodCall(context, cleanInsn, dirtyInsn)) { - return dirtyInsn; - } - return null; - }) - .filter(Objects::nonNull) - .map(MethodQualifier::create) - .toList(); - } - - private static List testOverloadedMethods(MixinContext context, MethodInsnNode cleanInsn, TargetPair cleanPair, TargetPair dirtyPair) { - ClassNode dirtyClass = context.dirtyLookup().getClass(cleanInsn.owner).orElse(null); - if (dirtyClass == null) { - return List.of(); - } - - List cleanParams = Parameters.getParameterTypes(cleanInsn.desc); - List methods = dirtyClass.methods.stream() - .filter(m -> { - if (m.name.equals(cleanInsn.name) && AdapterUtil.findMethod(cleanPair.classNode(), m.name, m.desc) == null) { - List dirtyParams = Parameters.getParameterTypes(m.desc); - return dirtyParams.size() > cleanParams.size() && dirtyParams.subList(0, cleanParams.size()).equals(cleanParams); - } - return false; - }) - .filter(m -> !MethodAnalyzer.findCalls(dirtyPair.methodNode(), MethodQualifier.create(m)).isEmpty()) - .toList(); - return methods.size() == 1 ? List.of(MethodQualifier.create(dirtyClass, methods.getFirst())) : List.of(); - } - - private static boolean matchesMethodCall(MixinContext context, MethodInsnNode cleanInsn, MethodInsnNode dirtyInsn) { - return cleanInsn.owner.equals(dirtyInsn.owner) && cleanInsn.name.equals(dirtyInsn.name) - || context.getTypeAdapter(Type.getObjectType(dirtyInsn.owner), Type.getObjectType(cleanInsn.owner)) != null - && Type.getArgumentTypes(cleanInsn.desc).length == Type.getArgumentTypes(dirtyInsn.desc).length; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ModifyVarInjectionPointSubResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ModifyVarInjectionPointSubResolver.java deleted file mode 100644 index a4b0a97d..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/ModifyVarInjectionPointSubResolver.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.LocalVariableNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.analysis.locals.LocalVarAnalyzer; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.Targets; - - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_STORE; -import static org.sinytra.adapter.patch.config.key.MixinKeys.ORDINAL; - -public class ModifyVarInjectionPointSubResolver implements SubResolver { - - @Nullable - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - if (!recipe.clean().getAtData().getValue().equals(AT_VAL_STORE)) return null; - Targets targets = recipe.targets(); - if (targets == null) return null; - - // Find replacement - Integer ordinal = recipe.clean().getProperty(ORDINAL).orElse(null); - if (ordinal == null) return null; - - Type varType = Type.getReturnType(context.methodNode().desc); - - LocalVariableLookup lookup = recipe.cleanLocalsTable(); - if (lookup == null) return null; - - LocalVariableNode desired = lookup.getByTypedOrdinal(varType, ordinal).orElse(null); - if (desired == null) return null; - - // Find variable initializer insns - InsnList desiredInitializerInsns = LocalVarAnalyzer.findInitializerInsns(targets.clean().methodNode(), desired.index); - - // Get all matching variables - for (MethodNode method : targets.dirty().classNode().methods) { - LocalVariableLookup dirtyLookup = context.methods().getLVT(method); - if (dirtyLookup == null) continue; - - LocalVariableNode match = LocalVarAnalyzer.findLocalWithInitializer(method, desired.desc, desiredInitializerInsns).orElse(null); - if (match != null) { - Integer dirtyOrdinal = dirtyLookup.getTypedOrdinal(match).orElse(null); - if (dirtyOrdinal == null) continue; - - return recipe.dirty().copyClean() - .setProperty(ORDINAL, dirtyOrdinal) - .setTargetMethod(method) - .inheritAtData(); - } - } - return null; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/OverloadedInjectionPointSubResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/OverloadedInjectionPointSubResolver.java deleted file mode 100644 index 26ad42a4..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/OverloadedInjectionPointSubResolver.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.sinytra.adapter.patch.resolver.injection; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.env.ctx.Targets; - -import java.util.List; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE_ASSIGN; - -public class OverloadedInjectionPointSubResolver implements SubResolver { - @Nullable - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - AtData at = recipe.clean().getAtData(); - if (at == null || !at.getValue().equals(AT_VAL_INVOKE) && !at.getValue().equals(AT_VAL_INVOKE_ASSIGN)) - return null; - - Targets targets = recipe.targets(); - if (targets == null) return null; - - // Temporarily modify @At data to target INVOKE if originally using INVOKE_ASSIGN - AtData tempAt = getInvokeAtData(at); - AbstractInsnNode cleanInsn = context.methods().findInjectionTargetInsn(targets.clean(), tempAt); - if (!(cleanInsn instanceof MethodInsnNode cleanMinsn)) return null; - MethodQualifier cleanQualifier = MethodQualifier.create(cleanMinsn); - - TargetPair cleanTarget = context.methods().findMethodPair(context.cleanLookup(), cleanQualifier); - if (cleanTarget == null) return null; - - TargetPair newCleanTarget = context.methods().findMethodPair(context.dirtyLookup(), cleanQualifier); - if (newCleanTarget == null) return null; - - // Check if called clean method was marked as deprecated - if (!MethodAnalyzer.isDirtyDeprecatedMethod(cleanTarget.methodNode(), newCleanTarget.methodNode())) { - return null; - } - - // Check that the deprecated method is not called anymore - List cleanCallsInDirty = MethodAnalyzer.findCalls(targets.dirty().methodNode(), cleanQualifier); - if (!cleanCallsInDirty.isEmpty()) return null; - - // Find method calls to potential replacement method - MethodQualifier qualifier = cleanQualifier.ignoreDesc(); - List dirtyCalls = MethodAnalyzer.findCalls(targets.dirty().methodNode(), qualifier); - if (dirtyCalls.isEmpty()) return null; - - // All calls target the same overloaded method - boolean allEqual = dirtyCalls.stream().map(m -> m.desc).distinct().limit(2).count() <= 1; - if (allEqual) { - return MutableConfiguration.create() - .setAtData(recipe.clean().getAtData().withTarget(dirtyCalls.getFirst())); - } - - return null; - } - - private AtData getInvokeAtData(AtData at) { - if (at.getValue().equals(AT_VAL_INVOKE_ASSIGN)) { - return at.withValue(AT_VAL_INVOKE); - } - return at; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/InjectorOrdinalResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/special/InjectorOrdinalResolver.java deleted file mode 100644 index 0155fe5a..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/InjectorOrdinalResolver.java +++ /dev/null @@ -1,429 +0,0 @@ -package org.sinytra.adapter.patch.resolver.special; - -import com.google.common.collect.Multimap; -import com.mojang.datafixers.util.Pair; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.InsnComparator; -import org.sinytra.adapter.analysis.InstructionMatcher; -import org.sinytra.adapter.analysis.locals.LocalVarAnalyzer; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.analysis.method.MethodInsnMatcher; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.mixin.MixinFlag; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.GeneratedVariables; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.SingleValueHandle; -import org.sinytra.adapter.util.OpcodeUtil; -import org.sinytra.adapter.analysis.InsnTraversal; - -import java.util.*; -import java.util.function.Function; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_RETURN; - -public class InjectorOrdinalResolver implements Resolver { - private static final Map OFFSET_HANDLERS = Map.of( - AT_VAL_INVOKE, InvokeOffsetHandler.INSTANCE, - AT_VAL_RETURN, ReturnOffsetHandler.INSTANCE - ); - - @Override - public ResolutionResult resolve(MixinContext context, Recipe recipe) { - Type returnType = recipe.clean().getReturnType(); - if (returnType == null) return ResolutionResult.pass(); - - List> offsetHandlers = getOffsetHandlers(context, recipe, returnType); - if (offsetHandlers.isEmpty()) return ResolutionResult.pass(); - - if (recipe.targets() == null) return ResolutionResult.pass(); - - boolean applied = false; - MutableConfiguration merged = MutableConfiguration.create(); - for (HandlerInstance instance : offsetHandlers) { - Configuration config = instance.apply(context, recipe); - if (config != null) { - applied = true; - merged.mergeFrom(config); - } - } - - return applied ? ResolutionResult.success(merged) : ResolutionResult.pass(); - } - - private static List> getOffsetHandlers(MixinContext context, Recipe recipe, Type returnType) { - Configuration cleanConfig = recipe.clean(); - AtData at = cleanConfig.getAtData(); - List> handlers = new ArrayList<>(); - - at.getProperty(AtData.Keys.ORDINAL).ifPresent(ordinal -> { - String target = at.getTarget().orElse(null); - OffsetUpdateHandler handler = OFFSET_HANDLERS.get(at.getValue()); - if (handler != null && (!handler.requiresTarget() || target != null)) { - OffsetUpdateHandler.Context handleContext = new OffsetUpdateHandler.Context(target, ordinal); - HandlerInstance instance = new HandlerInstance<>(handler, handleContext, i -> - MutableConfiguration.create() - .setAtData(at.withOrdinal(i)) - ); - handlers.add(instance); - } - }); - - if (context.hasFlag(MixinFlag.TARGETS_VARIABLE)) { - LocalVariableLookup cleanTable = recipe.cleanLocalsTable(); - if (cleanTable != null) { - // Handle modified ordinals - cleanConfig.getProperty(MixinKeys.ORDINAL) - .flatMap(ordinal -> cleanTable.getByTypedOrdinal(returnType, ordinal) - .flatMap(lvn -> cleanTable.getTypedOrdinal(lvn).map(o -> new LocalVar(lvn, o, true))) - .map(local -> new HandlerInstance<>(ModifyVariableOffsetHandler.INSTANCE, local, pair -> - MutableConfiguration.create() - .setProperty(MixinKeys.ORDINAL, pair.getFirst().ordinal()) - .mergeFrom(pair.getSecond()) - ))) - // Handle modified indexes - .or(() -> cleanConfig.getProperty(MixinKeys.INDEX) - .flatMap(i -> Optional.ofNullable(cleanTable.getByIndexOrNull(i)) - .flatMap(lvn -> cleanTable.getTypedOrdinal(lvn).map(o -> new LocalVar(lvn, o, false))) - .map(local -> new HandlerInstance<>(ModifyVariableOffsetHandler.INSTANCE, local, pair -> - MutableConfiguration.create() - .setProperty(MixinKeys.INDEX, pair.getFirst().lvn().index) - .mergeFrom(pair.getSecond()) - )) - )) - .ifPresent(handlers::add); - } - } - - return handlers; - } - - private interface UpdateHandler { - Optional apply(MixinContext mixinContext, Recipe recipe, T context); - } - - private interface OffsetUpdateHandler extends UpdateHandler { - record Context(@Nullable String target, int ordinal) { - } - - default boolean requiresTarget() { - return false; - } - } - - private record LocalVar(LocalVariableNode lvn, int ordinal, boolean relative) { - public LocalVar(LocalVariableNode lvn, int ordinal) { - this(lvn, ordinal, false); - } - } - - private record HandlerInstance(UpdateHandler handler, T context, Function applicator) { - @Nullable - public Configuration apply(MixinContext mixinContext, Recipe recipe) { - Optional updatedValue = this.handler.apply(mixinContext, recipe, this.context); - if (updatedValue.isPresent()) { - U value = updatedValue.get(); - mixinContext.recordCtxAudit("Update injection point ordinal from %s to %s", this.context, value); - return this.applicator.apply(value); - } - return null; - } - } - - private static class InvokeOffsetHandler implements OffsetUpdateHandler { - public static final InvokeOffsetHandler INSTANCE = new InvokeOffsetHandler(); - - @Override - public boolean requiresTarget() { - return true; - } - - @Override - public Optional apply(MixinContext mixinContext, Recipe recipe, Context context) { - TargetPair cleanTarget = recipe.getCleanTarget(); - TargetPair dirtyTarget = recipe.getDirtyTarget(); - String target = context.target(); - int ordinal = context.ordinal(); - - Multimap cleanCallsMap = MethodAnalyzer.getMethodCalls(cleanTarget.methodNode()); - Multimap dirtyCallsMap = MethodAnalyzer.getMethodCalls(dirtyTarget.methodNode()); - - String remappedTarget = mixinContext.patchContext().remap(target); - Collection cleanCalls = cleanCallsMap.get(remappedTarget); - Collection dirtyCalls = dirtyCallsMap.get(remappedTarget); - - if (cleanCalls.size() != dirtyCalls.size()) { - List cleanMatchers = cleanCalls.stream().map(MethodInsnMatcher::findSurroundingInstructions).toList(); - List dirtyMatchers = dirtyCalls.stream().map(MethodInsnMatcher::findSurroundingInstructions).toList(); - - if (ordinal >= 0 && ordinal < cleanMatchers.size()) { - InstructionMatcher original = cleanMatchers.get(ordinal); - return AdapterUtil.findOne(dirtyMatchers, original::test).map(dirtyMatchers::indexOf); - } - } - - return Optional.empty(); - } - } - - private static class ReturnOffsetHandler implements OffsetUpdateHandler { - public static final OffsetUpdateHandler INSTANCE = new ReturnOffsetHandler(); - - @Override - public Optional apply(MixinContext mixinContext, Recipe recipe, Context context) { - TargetPair cleanTarget = recipe.getCleanTarget(); - TargetPair dirtyTarget = recipe.getDirtyTarget(); - int ordinal = context.ordinal(); - - List cleanReturnInsns = MethodAnalyzer.findReturnInsns(cleanTarget.methodNode()); - List dirtyReturnInsns = MethodAnalyzer.findReturnInsns(dirtyTarget.methodNode()); - - if (cleanReturnInsns.size() == dirtyReturnInsns.size()) { - return Optional.empty(); - } - - if (ordinal < cleanReturnInsns.size()) { - AbstractInsnNode cleanInsn = cleanReturnInsns.get(ordinal); - InstructionMatcher original = new InstructionMatcher(cleanInsn, findReturnPrecedingInsns(cleanInsn), List.of()); - List dirtyMatchers = dirtyReturnInsns.stream() - .map(i -> new InstructionMatcher(i, findReturnPrecedingInsns(i), List.of())) - .toList(); - Optional match = AdapterUtil.findOne(dirtyMatchers, m -> original.test(m, InsnComparator.IGNORE_VAR_INDEX)); - if (match.isPresent()) { - return match.map(dirtyMatchers::indexOf); - } - } - - return findByMethodCalls(ordinal, cleanTarget, dirtyTarget); - } - - private static Optional findByMethodCalls(int ordinal, TargetPair cleanTarget, TargetPair dirtyTarget) { - List>> cleanCalls = getBlockCalls(cleanTarget.methodNode()); - List>> dirtyCalls = getBlockCalls(dirtyTarget.methodNode()); - - if (ordinal >= cleanCalls.size()) { - return Optional.empty(); - } - - List cleanCall = cleanCalls.get(ordinal).getSecond(); - for (int i = 0; i < dirtyCalls.size(); i++) { - Pair> dirtyCall = dirtyCalls.get(i); - boolean haveCommon = !Collections.disjoint(cleanCall, dirtyCall.getSecond()); - if (haveCommon) { - return Optional.of(i); - } - } - - return Optional.empty(); - } - - private static List>> getBlockCalls(MethodNode methodNode) { - List>> blockCalls = new ArrayList<>(); - - List minsns = new ArrayList<>(); - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof MethodInsnNode minsn) { - minsns.add(MethodQualifier.create(minsn).asDescriptor()); - } - if (OpcodeUtil.isReturnOpcode(insn.getOpcode())) { - blockCalls.add(Pair.of((InsnNode) insn, minsns)); - minsns = new ArrayList<>(); - } - } - - // Remove non-unique calls - List> values = blockCalls.stream() - .map(Pair::getSecond) - .toList(); - Map counts = new HashMap<>(); - for (List list : values) { - for (String s : list) { - counts.merge(s, 1, Integer::sum); - } - } - for (List list : values) { - list.removeIf(s -> counts.getOrDefault(s, 0) > 1); - } - - return blockCalls; - } - - private static List findReturnPrecedingInsns(AbstractInsnNode insn) { - return InsnTraversal.walk(insn.getPrevious(), InsnTraversal.BACKWARD) - .takeWhile(prev -> !OpcodeUtil.isReturnOpcode(prev.getOpcode())) - .filter(prev -> !(prev instanceof LabelNode) && !InsnTraversal.isDebugNode(prev)) - .limit(6) - .toList() - .reversed(); - } - } - - private static class ModifyVariableOffsetHandler implements UpdateHandler> { - private static final ModifyVariableOffsetHandler INSTANCE = new ModifyVariableOffsetHandler(); - - @Override - public Optional> apply(MixinContext mixinContext, Recipe recipe, LocalVar local) { - MethodNode methodNode = mixinContext.methodNode(); - - Type[] args = Type.getArgumentTypes(methodNode.desc); - if (args.length < 1) { - return Optional.empty(); - } - - Type targetType = args[0]; - // Gradually expand supported types over time as necessary - if (!targetType.equals(Type.BOOLEAN_TYPE) && !targetType.equals(Type.INT_TYPE) && !targetType.equals(Type.FLOAT_TYPE)) { - return Optional.empty(); - } - - if (mixinContext.methodAnnotation().getValue("slice").isPresent() && local.relative()) { - return Optional.empty(); - } - - return tryFindUpdatedIndex(targetType, recipe, local) - .>map(var -> Pair.of(var, null)) - .or(() -> tryFindSyntheticVariableIndex(mixinContext, methodNode, recipe, local)); - } - - /** - * Handle situations where a mixin is attempting to modify a variable that is used immediately after its modified. - * However, due to the nature of binary patches, a new variable might have been introduced earlier in the method, which is being used in its place now. - * In these cases, we'll find the new variable and update the mixin's index - *

- * As an example, let's have a look at LivingEntity#actuallyHurt - *

{@code
-         * == Original code ==
-         *    INVOKEVIRTUAL net/minecraft/world/entity/player/Player.getHealth ()F
-         *    FLOAD 2
-         *    FSUB
-         *    INVOKEVIRTUAL net/minecraft/world/entity/player/Player.setHealth (F)V
-         * == Patched code ==
-         * >> INVOKESPECIAL 
-         * >> FSTORE 2
-         *    INVOKEVIRTUAL net/minecraft/world/entity/player/Player.getHealth ()F
-         * != FLOAD 3
-         *    FSUB
-         *    INVOKEVIRTUAL net/minecraft/world/entity/player/Player.setHealth (F)V
-         * == Resulting code ==
-         * >> INVOKESPECIAL 
-         * >> FSTORE 3
-         *    INVOKEVIRTUAL net/minecraft/world/entity/player/Player.getHealth ()F
-         *    FLOAD 3
-         *    FSUB
-         *    INVOKEVIRTUAL net/minecraft/world/entity/player/Player.setHealth (F)V
-         * }
- */ - private static Optional> tryFindSyntheticVariableIndex(MixinContext context, MethodNode methodNode, Recipe recipe, LocalVar local) { - int ordinal = local.ordinal(); - Type variableType = Type.getReturnType(methodNode.desc); - LocalVariableLookup cleanTable = recipe.cleanLocalsTable(); - LocalVariableLookup dirtyTable = recipe.dirtyLocalsTable(); - if (cleanTable == null || dirtyTable == null) { - return Optional.empty(); - } - if (cleanTable.getForType(variableType).size() == dirtyTable.getForType(variableType).size()) { - List available = dirtyTable.getForType(variableType); - if (available.size() > ordinal) { - int variableIndex = available.get(ordinal).index; - AbstractInsnNode cleanInsn = recipe.getCleanInjectionInsn(); - AbstractInsnNode dirtyInsn = context.methods().findInjectionTargetInsn(recipe.getDirtyTarget()); - if (cleanInsn != null && dirtyInsn != null) { - for (AbstractInsnNode insn : InsnTraversal.untilLabel(cleanInsn, InsnTraversal.FORWARD, true)) { - SingleValueHandle handle = AdapterUtil.handleLocalVarInsnValue(insn); - if (handle != null && handle.get() == variableIndex) { - // We found out the variable is used right after our injection point - // Now let's check if it its index remain the same in the dirty target - List> dirtyVars = getUsedVariablesInLabel(dirtyInsn, insn.getOpcode()); - if (dirtyVars.size() == 1) { - int dirtyIndex = dirtyVars.getFirst().get(); - if (dirtyIndex != variableIndex) { - // Find new ordinal by index - LocalVariableNode lvn = dirtyTable.getByIndexOrNull(dirtyIndex); - if (lvn == null) { - return Optional.empty(); - } - return dirtyTable.getTypedOrdinal(lvn) - .map(o -> Pair.of( - new LocalVar(lvn, o), - MutableConfiguration.create() - .setProperty(MixinKeys.ARGS_ONLY, false) - )); - } - } - break; - } - } - } - } - } - return Optional.empty(); - } - - private static List> getUsedVariablesInLabel(AbstractInsnNode start, int opcode) { - List> list = new ArrayList<>(); - for (AbstractInsnNode insn : InsnTraversal.untilLabel(start, InsnTraversal.FORWARD, true)) { - if (insn.getOpcode() == opcode) { - SingleValueHandle handle = AdapterUtil.handleLocalVarInsnValue(insn); - if (handle != null) { - list.add(handle); - } - } - } - return list; - } - - private static Optional tryFindUpdatedIndex(Type targetType, Recipe recipe, LocalVar local) { - int ordinal = local.ordinal(); - LocalVariableLookup cleanVarLookup = recipe.cleanLocalsTable(); - LocalVariableLookup dirtyVarLookup = recipe.dirtyLocalsTable(); - if (cleanVarLookup == null || dirtyVarLookup == null) { - return Optional.empty(); - } - List cleanLocals = cleanVarLookup.getForType(targetType); - if (cleanLocals.size() <= ordinal) { - return Optional.empty(); - } - LocalVariableNode cleanLocal = cleanLocals.get(ordinal); - if (!GeneratedVariables.isGeneratedVariableName(cleanLocal.name, Type.getType(cleanLocal.desc))) { - return Optional.empty(); - } - - List dirtyLocals = dirtyVarLookup.getForType(targetType); - if (cleanLocals.size() != dirtyLocals.size() || dirtyLocals.size() <= ordinal) { - return findReplacementLocal(recipe.getCleanTarget().methodNode(), recipe.getDirtyTarget().methodNode(), cleanLocal) - .flatMap(var -> dirtyVarLookup.getTypedOrdinal(var).map(o -> new LocalVar(var, o))); - } - LocalVariableNode dirtyLocal = dirtyLocals.get(ordinal); - if (!local.relative() && dirtyLocal.index == local.lvn().index) { - return Optional.empty(); - } - OptionalInt dirtyNameOrdinal = GeneratedVariables.getGeneratedVariableOrdinal(dirtyLocal.name, Type.getType(dirtyLocal.desc)); - if (dirtyNameOrdinal.isEmpty() || local.relative() && ordinal == dirtyNameOrdinal.getAsInt()) { - return Optional.empty(); - } - - if (cleanLocal.index != dirtyLocal.index && !local.relative()) { - return Optional.of(new LocalVar(dirtyLocal, dirtyLocals.indexOf(dirtyLocal))); - } - - return Optional.empty(); - } - - private static Optional findReplacementLocal(MethodNode cleanMethod, MethodNode dirtyMethod, LocalVariableNode desired) { - // Find variable initializer insns - InsnList desiredInitializerInsns = LocalVarAnalyzer.findInitializerInsns(cleanMethod, desired.index); - return LocalVarAnalyzer.findLocalWithInitializer(dirtyMethod, desired.desc, desiredInitializerInsns); - } - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ModifyVarAtReturnResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ModifyVarAtReturnResolver.java deleted file mode 100644 index 81c131c4..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ModifyVarAtReturnResolver.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.sinytra.adapter.patch.resolver.special; - -import com.mojang.datafixers.util.Pair; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.VarInsnNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.MockMixinRuntime; -import org.sinytra.adapter.util.MethodQualifier; -import org.spongepowered.asm.mixin.injection.modify.LocalVariableDiscriminator; -import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; -import org.spongepowered.asm.mixin.injection.struct.Target; -import org.sinytra.adapter.env.ctx.Targets; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; - -import java.util.List; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_RETURN; - -/** - * Original mixin: - * - *
{@code @ModifyVariable(
- *     method = "exampleMethod",
- *     at = @At("RETURN")
- * )
- * private void someMethodMixin(int original) {
- *     return original * 2;
- * }
- * }
- *

- * Original target: - * - *

{@code
- * public int exampleMethod() {
- *     int i = 10;
- *     // ...
- * <<< return i;
- * >>> return localvar$zfk000$someMethodMixin(i);
- * }
- * }
- *

- * Patched target: - *

{@code
- * public int exampleMethod() {
- *     int i = 10;
- *     // ...
- * <<< return EventHooks.wrapVariable(i);
- * >>> return EventHooks.wrapVariable(modify$zfk000$someMethodMixin(i));
- * }
- * }
- *

- * Patched mixin: - * - *

{@code @ModifyArg(
- *     method = "exampleMethod",
- *     at = @At(
- *         value = "INVOKE",
- *         target="Lcom/example/EventHooks;wrapVariable(I)I"
- *     ),
- *     index = 0
- * )
- * private void someMethodMixin(int original) {
- *     return original * 2;
- * }
- * }
- */ -public class ModifyVarAtReturnResolver implements Resolver { - @Override - public ResolutionResult resolve(MixinContext context, Recipe recipe) { - AtData at = recipe.clean().getAtData(); - if (!AT_VAL_RETURN.equals(at.getValue())) return ResolutionResult.pass(); - - // Find injection targets - Targets targets = recipe.targets(); - if (targets == null) return ResolutionResult.pass(); - - int ordinal = at.getOrdinal().orElse(-1); - Pair cleanTargetPair = getTargetPair(recipe.clean(), context, targets.clean(), ordinal); - // In CLEAN, previous insn is VarInsn for the modified variable - if (cleanTargetPair == null || !(cleanTargetPair.getFirst() instanceof VarInsnNode cleanVarInsn) || cleanVarInsn.var != cleanTargetPair.getSecond()) { - return ResolutionResult.pass(); - } - // In DIRTY, previous insn (as in, before the RETURN insn) is a method call - Pair dirtyTargetPair = getTargetPair(recipe.clean(), context, targets.dirty(), ordinal); - // In CLEAN, previous insn is VarInsn for the modified variable - if (dirtyTargetPair == null || !(dirtyTargetPair.getFirst() instanceof MethodInsnNode dirtyMinsn)) { - return ResolutionResult.pass(); - } - // Get method call argument instructions - List args = MethodAnalyzer.getMethodCallSrcInsns(targets.dirty().methodNode(), dirtyMinsn); - if (args == null) { - return ResolutionResult.pass(); - } - - for (int i = 0; i < args.size(); i++) { - AbstractInsnNode insn = args.get(i); - if (insn instanceof VarInsnNode varInsn && varInsn.var == cleanTargetPair.getSecond()) { - Configuration config = recipe.dirty().copyClean() - .setMixinType(MixinAnnotations.MODIFY_ARG) - .inheritTargetClass() - .inheritTargetMethod() - .setAtData(AtData.create(AT_VAL_INVOKE, dirtyMinsn)) - .inheritSignature() - .setProperty(MixinKeys.INDEX, i); - - String qualifier = MethodQualifier.create(dirtyMinsn).asDescriptor(); - context.recordCtxAudit("Redirect RETURN variable modifier to parameter %s of method call to %s", i, qualifier); - return ResolutionResult.replace(config); - } - } - - return ResolutionResult.pass(); - } - - private static Pair getTargetPair(Configuration clean, MixinContext context, TargetPair injectionTarget, int ordinal) { - // Find injection point insn - List targetInsns = context.methods().findInjectionTargetInsns(injectionTarget); - if (targetInsns.isEmpty()) return null; - - int index = ordinal == -1 ? targetInsns.size() - 1 : ordinal; - if (index >= targetInsns.size()) return null; - - AbstractInsnNode targetInsn = targetInsns.get(index); - // Find modified variable - LocalVariableDiscriminator discriminator = LocalVariableDiscriminator.parse(context.methodAnnotation().unwrap()); - InjectionInfo injectionInfo = MockMixinRuntime.forInjectionInfo(context.classNode().name, injectionTarget.classNode().name, context.environment()); - - Type returnType = clean.getReturnType(); - Target target = MockMixinRuntime.createMixinTarget(injectionTarget); - LocalVariableDiscriminator.Context ctx = new LocalVariableDiscriminator.Context(injectionInfo, returnType, discriminator.isArgsOnly(), target, targetInsn); - - int local = discriminator.findLocal(ctx); - return Pair.of(targetInsn, local); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ModifyVarUpgradeResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ModifyVarUpgradeResolver.java deleted file mode 100644 index 88eb6bad..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ModifyVarUpgradeResolver.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.sinytra.adapter.patch.resolver.special; - -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.spongepowered.asm.mixin.injection.At; -import org.sinytra.adapter.patch.config.Configurations; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE_ASSIGN; - -/** - * Upgrade a ModifyVar to a ModifyExpressionVal - * Original mixin: - * - *
{@code @ModifyVariable(
- *     method = "exampleMethod",
- *     at = @At(
- *         value = "INVOKE",
- *         target = "someOtherMethod()I",
- *         shift = At.Shift.BY,
- *         by = 2
- *     )
- * )
- * private void someMethodMixin(int original) {
- *     return original * 2;
- * }
- * }
- *

- * Original target: - * - *

{@code
- * public int exampleMethod() {
- *     int i = someOtherMethod();
- * >>> i = localvar$zfk000$someMethodMixin(i);
- *     // ...
- * }
- * }
- *

- * Patched target: - *

{@code
- * public int exampleMethod() {
- * <<< int i = someOtherMethod();
- * >>> int i = modifyExpressionValue$zfk000$someMethodMixin(someOtherMethod());
- *     // ...
- * }
- * }
- *

- * Patched mixin: - * - *

{@code @ModifyExpressionValue(
- *     method = "exampleMethod",
- *     at = @At(
- *         value = "INVOKE",
- *         target="someOtherMethod()I"
- *     )
- * )
- * private void someMethodMixin(int original) {
- *     return original * 2;
- * }
- * }
- */ -public class ModifyVarUpgradeResolver implements Resolver { - @Override - public ResolutionResult resolve(MixinContext context, Recipe recipe) { - if (!recipe.hasInjectionPointValue(AT_VAL_INVOKE) && !recipe.hasInjectionPointValue(AT_VAL_INVOKE_ASSIGN)) - return ResolutionResult.pass(); - - // Conditions must be met: - // - The injection target value is INVOKE or INVOKE_ASSIGN - // - The target is shifted BY 2 - AtData at = recipe.clean().getAtData(); - if (at == null || at.getTarget().isEmpty()) return ResolutionResult.pass(); - - At.Shift shift = at.getProperty(AtData.Keys.SHIFT).orElse(null); - if (shift != At.Shift.BY) return ResolutionResult.pass(); - - boolean byTwo = at.getProperty(AtData.Keys.BY).map(i -> i == 2).orElse(false); - if (!byTwo) return ResolutionResult.pass(); - - String target = at.getTargetOrThrow(); - return ResolutionResult.replace(Configurations.changeMixinType(recipe.dirty(), MixinAnnotations.MODIFY_EXPR_VAL, AtData.create(AT_VAL_INVOKE, target))); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ResolverSyntheticInstanceof.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ResolverSyntheticInstanceof.java deleted file mode 100644 index d08a7bd7..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/ResolverSyntheticInstanceof.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.sinytra.adapter.patch.resolver.special; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.patch.config.Configurations; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.sinytra.adapter.analysis.InsnComparator; -import org.sinytra.adapter.analysis.InstructionMatcher; -import org.sinytra.adapter.analysis.method.MethodInsnMatcher; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.analysis.InsnTraversal; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_SINYTRA_INSTANCEOF; - -/** - *

- * Handle targeting redirectors to injections points that are being replaced with instanceof checks. - *

- * Sometimes, mods include injectors that widen item comparison if-statements from comparing specific item instances to an instanceof call - * that applies to all items of a given type. While this is very nice of them, Forge already takes care of it for us, eliminating the needs for such a mixin. - *

- * Reference: stack.isOf(Items.CROSSBOW) -> stack.getItem() instanceof CrossbowItem in HeldItemRenderer#renderFirstPersonItem - */ -public record ResolverSyntheticInstanceof(boolean skipInsnComparison) implements Resolver { - - @Override - public ResolutionResult resolve(MixinContext context, Recipe recipe) { - if (!recipe.hasInjectionPointValue(AT_VAL_INVOKE)) - return ResolutionResult.pass(); - - TargetPair dirtyTarget = recipe.getDirtyTarget(); - AbstractInsnNode targetInsn = recipe.getCleanInjectionInsn(); - if (dirtyTarget == null || targetInsn == null) return ResolutionResult.pass(); - - List labelInsns = findLabelInsns(targetInsn); - // Ensure label contain an if statement - if (labelInsns.isEmpty() || !(labelInsns.getLast() instanceof JumpInsnNode)) return ResolutionResult.pass(); - - InstructionMatcher cleanMatcher = MethodInsnMatcher.findForwardInstructions(targetInsn); - if (cleanMatcher.after().isEmpty()) return ResolutionResult.pass(); - int firstOp = cleanMatcher.after().getFirst().getOpcode(); - // Find equivalent dirty code point - InsnList dirtyInsns = dirtyTarget.methodNode().instructions; - for (AbstractInsnNode insn : dirtyInsns) { - if (insn.getOpcode() == firstOp) { - AbstractInsnNode nextLabel = InsnTraversal.find(insn, InsnTraversal.FORWARD, LabelNode.class::isInstance); - InstructionMatcher dirtyMatcher = MethodInsnMatcher.findForwardInstructions(nextLabel); - if (cleanMatcher.test(dirtyMatcher)) { - // ModifyExpressionValue doesn't include the original instanceof call, so we can skip comparing instructions - if (this.skipInsnComparison) { - // Only convert to @ModifyInstanceofValue if the handler returns boolean. - // If it returns a non-boolean type (e.g. Block), the mixin is modifying the - // value fed into the instanceof check, not the boolean result of it. - // Converting in that case produces an invalid (Block)Block handler where (Z)Z is expected. - Type handlerReturnType = Type.getReturnType(context.methodNode().desc); - if (!handlerReturnType.equals(Type.BOOLEAN_TYPE)) { - return ResolutionResult.pass(); - } - - TypeInsnNode instanceOfInsn = (TypeInsnNode) findLabelInsns(insn).stream().filter(i -> i.getOpcode() == Opcodes.INSTANCEOF).findFirst().orElse(null); - if (instanceOfInsn == null) { - return ResolutionResult.pass(); - } - - int ordinal = getInstanceofOrdinal(dirtyInsns, instanceOfInsn); - Configuration config = Configurations.changeMixinType(recipe.dirty(), MixinAnnotations.MODIFY_INSTANCEOF_VAL, - AtData.builder(AT_VAL_SINYTRA_INSTANCEOF) - .target(instanceOfInsn.desc) - .ordinal(ordinal != 0 ? ordinal : null) - .build()); - - return ResolutionResult.replace(config); - } - - // Found the code point, now determine the contents of the updated if statement - List dirtyLabelInsns = findLabelInsns(insn); - - // Create a normalized method body insn list - List modLabelInsns = new ArrayList<>(); - for (AbstractInsnNode ins : context.methodNode().instructions) { - if (InsnTraversal.isDebugNode(ins)) { - continue; - } - modLabelInsns.add(ins.clone(Map.of())); - } - if (modLabelInsns.size() < 3 || dirtyLabelInsns.isEmpty()) { - continue; - } - // Remove first and last label insns - modLabelInsns.removeFirst(); - modLabelInsns.removeLast(); - - // Remove consumer insns (jump / return) - dirtyLabelInsns.removeLast(); - modLabelInsns.removeLast(); - - // Test whether the mixin method's body is functionally equal to the patched if statement - InstructionMatcher finalCleanMatcher = new InstructionMatcher(null, dirtyLabelInsns, List.of()); - InstructionMatcher finalDirtyMatcher = new InstructionMatcher(null, modLabelInsns, List.of()); - - // Disable mixin. Goodbye. - if (finalCleanMatcher.test(finalDirtyMatcher, InsnComparator.IGNORE_VAR_INDEX)) { - return ResolutionResult.replace(Configurations.DELETE); - } - } - } - } - - return ResolutionResult.pass(); - } - - // The instructions around insn within its label - private static List findLabelInsns(AbstractInsnNode insn) { - List list = new ArrayList<>(); - if (!(insn instanceof LabelNode)) { - list.addAll(InsnTraversal.untilLabel(insn, InsnTraversal.BACKWARD)); - } - list.addAll(InsnTraversal.untilLabel(insn, InsnTraversal.FORWARD)); - return list; - } - - private static int getInstanceofOrdinal(InsnList insns, TypeInsnNode insn) { - List instanceOfInsns = new ArrayList<>(); - for (AbstractInsnNode node : insns) { - if (node instanceof TypeInsnNode typeInsn && typeInsn.getOpcode() == Opcodes.INSTANCEOF && typeInsn.desc.equals(insn.desc)) { - instanceOfInsns.add(node); - } - } - return instanceOfInsns.indexOf(insn); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/SliceBoundaryResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/special/SliceBoundaryResolver.java deleted file mode 100644 index a34fbdc4..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/special/SliceBoundaryResolver.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.sinytra.adapter.patch.resolver.special; - -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ann.SliceData; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.AT_VAL_INVOKE; -import static org.sinytra.adapter.patch.config.key.MixinKeys.SLICE; -import static org.sinytra.adapter.patch.config.key.MixinKeys.SLICES; - -public class SliceBoundaryResolver implements Resolver { - @Override - public ResolutionResult resolve(MixinContext context, Recipe recipe) { - TargetPair dirtyTarget = recipe.getDirtyTarget(); - if (dirtyTarget == null) return ResolutionResult.pass(); - - SliceData slice = recipe.clean().getProperty(SLICE).orElse(null); - if (slice != null) { - if (passesSliceCheck(slice, context, dirtyTarget)) - return ResolutionResult.pass(); - - SliceData fixedSlice = fixSliceData(slice, dirtyTarget.methodNode()); - if (fixedSlice == null) - return ResolutionResult.pass(); - - return ResolutionResult.success(MutableConfiguration.create().setProperty(SLICE, fixedSlice)); - } - - List slices = recipe.clean().getProperty(SLICES).orElse(null); - if (slices == null || slices.isEmpty()) - return ResolutionResult.pass(); - - List fixedSlices = new ArrayList<>(); - boolean changed = false; - for (SliceData s : slices) { - if (passesSliceCheck(s, context, dirtyTarget)) { - fixedSlices.add(s); - continue; - } - SliceData fixedSlice = fixSliceData(s, dirtyTarget.methodNode()); - if (fixedSlice == null) - return ResolutionResult.pass(); - fixedSlices.add(fixedSlice); - changed = true; - } - if (!changed) - return ResolutionResult.pass(); - - return ResolutionResult.success(MutableConfiguration.create().setProperty(SLICES, fixedSlices)); - } - - @Nullable - private static SliceData fixSliceData(SliceData slice, MethodNode dirtyMethod) { - AtData from = slice.from(); - if (from != null) { - from = fixSlideInjectionPoint(from, dirtyMethod).orElse(null); - if (from == null) return null; - } - - AtData to = slice.to(); - if (to != null) { - to = fixSlideInjectionPoint(to, dirtyMethod).orElse(null); - if (to == null) return null; - } - - return new SliceData(slice.id(), from, to); - } - - private static Optional fixSlideInjectionPoint(AtData boundary, MethodNode dirtyMethod) { - // Require only INVOKE values - if (!AT_VAL_INVOKE.equals(boundary.getValue())) - return Optional.empty(); - - // Find original target invocation qualifier - MethodQualifier target = boundary.getTargetQualifier().orElse(null); - if (target == null) return Optional.empty(); - - // Find method invocations with a matching name - List candidates = new ArrayList<>(); - for (AbstractInsnNode insn : dirtyMethod.instructions) { - if (insn instanceof MethodInsnNode minsn && minsn.name.equals(target.name())) { - candidates.add(minsn); - } - } - if (candidates.size() != 1) return Optional.empty(); - - MethodInsnNode insn = candidates.getFirst(); - return Optional.of(boundary.withTarget(insn)); - } - - private static boolean passesSliceCheck(SliceData slice, MixinContext context, TargetPair dirtyTarget) { - InsnList insns = context.methods().computeSlice(dirtyTarget, slice.toAnnotationNode()); - return insns.size() != dirtyTarget.methodNode().instructions.size(); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitMethodCancellationHelper.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitMethodCancellationHelper.java deleted file mode 100644 index 84333fc2..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitMethodCancellationHelper.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.sinytra.adapter.patch.resolver.target; - -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.Label; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; -import org.objectweb.asm.commons.Method; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.env.ctx.MixinClassGenerator; -import org.sinytra.adapter.env.util.TypeConstants; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.List; -import java.util.stream.IntStream; - -public final class SplitMethodCancellationHelper { - - public static void handle(MixinContext context, Recipe recipe, MethodNode newTarget) { - TargetPair cleanTarget = recipe.getCleanTarget(); - if (cleanTarget == null) return; - TargetPair newCleanTarget = recipe.getNewCleanTarget(); - if (newCleanTarget == null) return; - - ClassNode originalClassTarget = newCleanTarget.classNode(); - MethodNode originalMethodTarget = newCleanTarget.methodNode(); - - if (!MethodAnalyzer.isDirtyDeprecatedMethod(cleanTarget.methodNode(), originalMethodTarget) || Type.getReturnType(originalMethodTarget.desc) != Type.VOID_TYPE) { - return; - } - - List invocations = MethodAnalyzer.getSelfTopTierMethodCalls(newCleanTarget); - if (invocations == null) { - return; - } - int index = IntStream.range(0, invocations.size()) - .filter(i -> invocations.get(i).name.equals(newTarget.name) && invocations.get(i).desc.equals(newTarget.desc)) - .findFirst() - .orElse(-1); - if (index == -1) { - return; - } - - MixinClassGenerator generator = context.patchContext().environment().classGenerator(); - ClassNode generatedTarget = generator.getOrGenerateMixinClass(context.classNode(), originalClassTarget.name, null); - - String fieldName = "adapter$canceller$" + originalMethodTarget.name + "$" + AdapterUtil.randomString(5); - int fieldAccess = Opcodes.ACC_PRIVATE | (context.isStatic() ? Opcodes.ACC_STATIC : 0); - FieldNode trackerField = (FieldNode) generatedTarget.visitField(fieldAccess, fieldName, Type.BOOLEAN_TYPE.getDescriptor(), null, null); - - for (int i = index + 1; i < invocations.size(); i++) { - generateCancellerMethod(generatedTarget, trackerField, originalClassTarget, invocations.get(i), context, i == invocations.size() - 1); - } - - context.recordCtxAudit("Generate cancellation handler mixin"); - } - - private static void generateCancellerMethod(ClassNode generatedTarget, FieldNode trackerField, ClassNode originalClassTarget, MethodNode newTarget, MixinContext context, boolean reset) { - String name = context.methodNode().name + "$adapter$canceller$" + AdapterUtil.randomString(5); - String desc = Type.getMethodDescriptor(Type.VOID_TYPE, TypeConstants.CI_TYPE); - MethodNode invokerMixinMethod = (MethodNode) generatedTarget.visitMethod(Opcodes.ACC_PRIVATE | (context.isStatic() ? Opcodes.ACC_STATIC : 0), name, desc, null, null); - { - AnnotationVisitor injectAnn = invokerMixinMethod.visitAnnotation(MixinAnnotations.INJECT, true); - { - AnnotationVisitor methodValue = injectAnn.visitArray("method"); - methodValue.visit(null, newTarget.name + newTarget.desc); - methodValue.visitEnd(); - } - { - AnnotationVisitor atValue = injectAnn.visitArray("at"); - { - AnnotationVisitor atAnn = atValue.visitAnnotation(null, MixinAnnotations.AT); - atAnn.visit("value", "HEAD"); - atAnn.visitEnd(); - } - atValue.visitEnd(); - } - injectAnn.visit("cancellable", Boolean.TRUE); - injectAnn.visitEnd(); - } - // Generate logic - boolean isStatic = context.isStatic(); - Type generatedType = Type.getObjectType(generatedTarget.name); - GeneratorAdapter gen = new GeneratorAdapter(invokerMixinMethod, invokerMixinMethod.access, invokerMixinMethod.name, invokerMixinMethod.desc); - Label endLabel = new Label(); - gen.newLabel(); - if (isStatic) { - gen.getStatic(generatedType, trackerField.name, Type.BOOLEAN_TYPE); - } else { - gen.loadThis(); - gen.getField(generatedType, trackerField.name, Type.BOOLEAN_TYPE); - } - gen.visitJumpInsn(Opcodes.IFEQ, endLabel); - { - if (reset) { - gen.newLabel(); - if (isStatic) { - gen.visitInsn(Opcodes.ICONST_0); - gen.putStatic(generatedType, trackerField.name, Type.BOOLEAN_TYPE); - } else { - gen.loadThis(); - gen.visitInsn(Opcodes.ICONST_0); - gen.putField(generatedType, trackerField.name, Type.BOOLEAN_TYPE); - } - } - gen.newLabel(); - gen.loadArg(0); - gen.invokeVirtual(TypeConstants.CI_TYPE, new Method("cancel", "()V")); - } - gen.visitLabel(endLabel); - gen.returnValue(); - gen.newLabel(); - gen.endMethod(); - // Modify mixin to set the field value - MethodQualifier qualifier = new MethodQualifier(TypeConstants.CI_TYPE.getDescriptor(), "cancel", "()V"); - InsnList methodInsns = context.methodNode().instructions; - for (MethodInsnNode minsn : MethodAnalyzer.findCalls(context.methodNode(), qualifier)) { - InsnList list = new InsnList(); - if (isStatic) { - list.add(new InsnNode(Opcodes.ICONST_1)); - list.add(new FieldInsnNode(Opcodes.PUTSTATIC, originalClassTarget.name, trackerField.name, trackerField.desc)); - } else { - list.add(new VarInsnNode(Opcodes.ALOAD, 0)); - list.add(new InsnNode(Opcodes.ICONST_1)); - list.add(new FieldInsnNode(Opcodes.PUTFIELD, originalClassTarget.name, trackerField.name, trackerField.desc)); - } - methodInsns.insertBefore(minsn, list); - } - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitTargetMethodSubResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitTargetMethodSubResolver.java deleted file mode 100644 index b7e8e396..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/SplitTargetMethodSubResolver.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.sinytra.adapter.patch.resolver.target; - -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.method.MethodAnalyzer.IndyHandle; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.analysis.InsnComparator; -import org.sinytra.adapter.analysis.InstructionMatcher; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.analysis.method.MethodInsnMatcher; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.List; -import java.util.Objects; - -/** - * Handle cases where a single method is split into multiple smaller pieces. - * For an example, see net.minecraft.client.gui.Gui#renderPlayerHealth - */ -public class SplitTargetMethodSubResolver implements SubResolver { - @Override - public Configuration resolve(MixinContext context, Recipe recipe) { - TargetPair cleanTarget = recipe.getCleanTarget(); - TargetPair dirtyTarget = recipe.getNewCleanTarget(); - if (cleanTarget == null || dirtyTarget == null) return null; - - List candidates = disambiguate(locateCandidates(context, cleanTarget, dirtyTarget), context, cleanTarget); - - if (candidates.size() == 1) { - MethodNode method = candidates.getFirst().method(); - context.recordCtxAudit("Adjusting split method target to %s", MethodQualifier.create(method).asDescriptor()); - - MutableConfiguration config = MutableConfiguration.create() - .setTargetMethod(method); - if (recipe.clean().isCancellable()) { - config.setProperty(SpecialKeys.SPLIT_CANCELLATION, true); - } - return config; - } - - return null; - } - - private static List locateCandidates(MixinContext context, TargetPair cleanTarget, TargetPair dirtyTarget) { - MethodNode cleanTargetMethod = cleanTarget.methodNode(); - ClassNode dirtyTargetClass = dirtyTarget.classNode(); - MethodNode dirtyTargetMethod = dirtyTarget.methodNode(); - - // Check that a Deprecated annotation was added to the dirty method - if (!MethodAnalyzer.isDirtyDeprecatedMethod(cleanTargetMethod, dirtyTargetMethod)) { - return tryFindPartialCandidates(cleanTargetMethod, dirtyTargetClass, dirtyTargetMethod, context); - } - - List invocations = MethodAnalyzer.getSelfTopTierMethodCalls(dirtyTarget); - if (invocations == null) { - return List.of(); - } - - List candidates = findInsnsCalls(invocations.stream() - .map(m -> new TargetPair(dirtyTargetClass, m)).toList(), context); - - // Attempt to find matching insns in lambdas - if (candidates.isEmpty()) { - List nestedLambdas = invocations.stream() - .flatMap(m -> MethodAnalyzer.findOwnIndyHandles(dirtyTargetClass, m).stream()) - .map(IndyHandle::method) - .filter(MethodAnalyzer::isLambda) - .map(m -> new TargetPair(dirtyTargetClass, m)) - .toList(); - return findInsnsCalls(nestedLambdas, context); - } - - return candidates; - } - - // Handle cases where only part of the method is moved away - private static List tryFindPartialCandidates(MethodNode cleanTargetMethod, ClassNode dirtyTargetClass, MethodNode dirtyTargetMethod, MixinContext context) { - List dirtyOnlyCalls = MethodAnalyzer.findDirtyOnlyCalls(cleanTargetMethod, dirtyTargetMethod, false).stream() - .filter(i -> i.owner.equals(dirtyTargetClass.name)) - .map(i -> context.methods().findOwnMethodPair(context.dirtyLookup(), MethodQualifier.create(i))) - .filter(Objects::nonNull) - .toList(); - - return findInsnsCalls(dirtyOnlyCalls, context); - } - - // If multiple candidates have been found, try comparing the surrounding method instructions to find one match - private static List disambiguate(List candidates, MixinContext context, TargetPair cleanTarget) { - if (candidates.size() <= 1) { - return candidates; - } - - List cleanInsns = context.methods().findInjectionTargetInsns(cleanTarget); - if (cleanInsns.size() != 1) { - return candidates; - } - - InstructionMatcher cleanMatcher = MethodInsnMatcher.findSurroundingInstructions(cleanInsns.getFirst(), 5); - - List matchingCandidates = candidates.stream() - .filter(method -> method.insns().size() == 1) - .filter(method -> { - InstructionMatcher matcher = MethodInsnMatcher.findSurroundingInstructions(method.insns().getFirst(), 5); - return cleanMatcher.test(matcher, InsnComparator.IGNORE_VAR_INDEX); - }) - .toList(); - - if (matchingCandidates.size() == 1) { - return matchingCandidates; - } - - return candidates; - } - - private static List findInsnsCalls(List methods, MixinContext context) { - return methods.stream() - .map(pair -> { - List insns = context.methods().findInjectionTargetInsns(pair); - return !insns.isEmpty() ? new CandidateMethod(pair.methodNode(), insns) : null; - }) - .filter(Objects::nonNull) - .toList(); - } - - private record CandidateMethod(MethodNode method, List insns) { - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/TargetMethodResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/target/TargetMethodResolver.java deleted file mode 100644 index 0d3efb43..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/TargetMethodResolver.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.sinytra.adapter.patch.resolver.target; - -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.resolver.CompoundResolver; - -public class TargetMethodResolver extends CompoundResolver { - public TargetMethodResolver() { - addSubResolver(new SplitTargetMethodSubResolver()); - addSubResolver(TargetMethodSubResolvers.CHANGED_LAMBDA_INDEX); - addSubResolver(TargetMethodSubResolvers.CHANGED_METHOD_PARAMS); - addSubResolver(TargetMethodSubResolvers.MOVED_INTO_LAMBDA); - } - - @Override - protected boolean canApply(Recipe recipe) { - return recipe.dirty().getTargetMethod() == null; - } - - @Override - protected Configuration tryReuse(MixinContext context, Recipe recipe) { - TargetPair target = recipe.getNewCleanTarget(); - if (target != null && context.methods().hasInjectionTargetInsns(target)) { - return recipe.dirty().copyClean() - .inheritTargetMethod(); - } - return null; - } - - @Override - protected Configuration useFallback(MixinContext context, Recipe recipe) { - TargetPair target = recipe.getNewCleanTarget(); - if (target != null) { - return recipe.dirty().copyClean() - .inheritTargetMethod(); - } - return null; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/TargetMethodSubResolvers.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/target/TargetMethodSubResolvers.java deleted file mode 100644 index 2891c298..00000000 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/target/TargetMethodSubResolvers.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.sinytra.adapter.patch.resolver.target; - -import com.mojang.datafixers.util.Pair; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; -import org.sinytra.adapter.analysis.method.MethodAnalyzer.IndyHandle; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.sinytra.adapter.patch.resolver.SubResolver; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.util.MethodQualifier; - -import java.util.Comparator; -import java.util.List; - -public class TargetMethodSubResolvers { - /** - * For example: - * CLEAN: net/minecraft/server/MinecraftServer.lambda$reloadResources$28(Lcom/google/common/collect/ImmutableList;)Ljava/util/concurrent/CompletionStage; - * DIRTY: net/minecraft/server/MinecraftServer.lambda$reloadResources$29(Lcom/google/common/collect/ImmutableList;)Ljava/util/concurrent/CompletionStage; - */ - public static final SubResolver CHANGED_LAMBDA_INDEX = (MixinContext context, Recipe recipe) -> { - MethodQualifier cleanQualifier = recipe.clean().getTargetMethod(); - if (!MethodAnalyzer.isLambda(cleanQualifier)) return null; - - ClassNode targetClass = context.dirtyLookup().getClass(recipe.clean().getTargetClass()).orElse(null); - if (targetClass == null) return null; - - List candidateMethods = targetClass.methods.stream() - .filter(m -> MethodAnalyzer.isLambda(m) && m.desc.equals(cleanQualifier.desc())) - .toList(); - if (candidateMethods.size() != 1) return null; - - MethodNode newTarget = candidateMethods.getFirst(); - return MutableConfiguration.create() - .setTargetMethod(newTarget); - }; - - /** - * Handle cases where the target method's parameters have changed - *

- * For example: - * CLEAN: Lnet/minecraft/server/level/ServerEntity;sendPairingData(Lnet/minecraft/server/level/ServerPlayer;Ljava/util/function/Consumer;)V - *
- * DIRTY: Lnet/minecraft/server/level/ServerEntity;sendPairingData(Lnet/minecraft/server/level/ServerPlayer;Lnet/neoforged/neoforge/network/bundle/PacketAndPayloadAcceptor;)V - */ - public static final SubResolver CHANGED_METHOD_PARAMS = (MixinContext context, Recipe recipe) -> { - Configuration overloaded = resolveOverloadedReplacement(context, recipe); - if (overloaded != null) return overloaded; - - MethodQualifier cleanQualifier = recipe.clean().getTargetMethod(); - - Pair> candidates = context.methods().findOwnMethodsByName(context.dirtyLookup(), cleanQualifier); - if (candidates == null) return null; - - // Find single matching candidate - Configuration resolved = resolveReplacementCandidate(context, recipe, candidates.getSecond()); - if (resolved == null) return null; - - // Only apply single candidate change when the target desc has changed - if (resolved.getTargetMethod() != null && !resolved.getTargetMethod().matches(cleanQualifier)) { - return resolved; - } - - return null; - }; - - /** - * Handle cases where the target instructions have been moved into a lambda inside the target method - */ - public static final SubResolver MOVED_INTO_LAMBDA = (MixinContext context, Recipe recipe) -> { - TargetPair target = recipe.getNewCleanTarget(); - if (target == null) return null; - - // Find lambda invocations and search for target insns inside the lambda - for (IndyHandle handle : MethodAnalyzer.findOwnIndyHandles(target.classNode(), target.methodNode())) { - TargetPair lambda = new TargetPair(target.classNode(), handle.method()); - if (!context.methods().findInjectionTargetInsns(lambda).isEmpty()) { - return MutableConfiguration.create() - .setTargetMethod(lambda.methodNode()); - } - } - - return null; - }; - - // Resolve overloaded method by call for when the name doesn't match. Original method must be marked @Deprecated - // Example: BoneMealItem#growCrop -> applyBonemeal - @Nullable - private static Configuration resolveOverloadedReplacement(MixinContext context, Recipe recipe) { - TargetPair dirtyTarget = recipe.getNewCleanTarget(); - if (dirtyTarget == null || !MethodAnalyzer.isDeprecated(dirtyTarget.methodNode())) return null; - - List invocations = MethodAnalyzer.getSelfTopTierMethodCalls(dirtyTarget); - for (MethodNode invocation : invocations) { - if (context.methods().hasInjectionTargetInsns(new TargetPair(dirtyTarget.classNode(), invocation))) { - return MutableConfiguration.create() - .setTargetMethod(invocation); - } - } - - return null; - } - - @Nullable - private static Configuration resolveReplacementCandidate(MixinContext context, Recipe recipe, List methods) { - if (methods.size() == 1) { - return MutableConfiguration.create() - .setTargetMethod(methods.getFirst()); - } - - Resolver resolver = recipe.resolvers().get(InjectionPointResolver.class); - - List> valid = methods.stream() - .sorted(Comparator.comparingInt(m -> Parameters.getParameterTypes(m.desc).size()).reversed()) - .>flatMap(m -> { - Configuration dirtyCopy = recipe.dirty().copy().setTargetMethod(m); - Resolver.ResolutionResult result = context.audited(resolver, () -> resolver.resolve(context, recipe.withDirtyConfig(dirtyCopy))); - return result - .maybePatch() - .stream() - .map(c -> Pair.of(m, c.copyClean().setTargetMethod(m))); - }) - .toList(); - if (valid.size() == 1) { - return valid.getFirst().getSecond(); - } - - List nonDeprecated = valid.stream() - .map(Pair::getFirst) - .filter(m -> !isDirtyDeprecatedMethod(context, m)) - .toList(); - if (nonDeprecated.size() == 1) { - return MutableConfiguration.create() - .setTargetMethod(nonDeprecated.getFirst()); - } - - // Best effort: Handle cases where the target method is overloaded, but the mixin does not specify the target descriptor, - // resulting in ambigous targets. So we just pick the one with the most parameters; - if (!valid.isEmpty()) { - return valid.getFirst().getSecond(); - } - - return null; - } - - public static boolean isDirtyDeprecatedMethod(MixinContext context, MethodNode dirty) { - TargetPair pair = context.methods().findOwnMethodPair(context.cleanLookup(), MethodQualifier.create(dirty)); - return pair != null ? MethodAnalyzer.isDirtyDeprecatedMethod(pair.methodNode(), dirty) : MethodAnalyzer.isDeprecated(dirty); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/AppliedLocalsCheck.java b/core/src/main/java/org/sinytra/adapter/pipeline/AppliedLocalsCheck.java new file mode 100644 index 00000000..e5f756e2 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/AppliedLocalsCheck.java @@ -0,0 +1,61 @@ +package org.sinytra.adapter.pipeline; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.injector.MixinFlag; +import org.sinytra.adapter.pipeline.locate.LocalLocator; +import org.sinytra.adapter.pipeline.processor.SugarLocalProcessor; +import org.sinytra.adapter.util.MixinBridge; + +public final class AppliedLocalsCheck { + public static void check(Object actor, MixinContext context, TargetPair cleanTarget, TargetPair dirtyTarget) { + AbstractInsnNode cleanInsn = context.injections().resolveFirst(cleanTarget); + AbstractInsnNode dirtyInsn = context.injections().resolveFirst(dirtyTarget); + if (cleanInsn == null || dirtyInsn == null) { + return; + } + + MethodNode handler = context.methodNode(); + if (context.hasFlag(MixinFlag.TARGETS_VARIABLE)) { + Type type = Type.getReturnType(handler.desc); + int cleanSlot = context.locals().resolveSelectedLocal(cleanTarget, cleanInsn, type); + int slot = context.locals().resolveSelectedLocal(dirtyTarget, dirtyInsn, type); + report(actor, context, "The variable selector", cleanTarget, cleanSlot, cleanInsn, dirtyTarget, slot); + } + + Type[] params = Type.getArgumentTypes(handler.desc); + MixinBridge.Session cleanSession = null; + MixinBridge.Session dirtySession = null; + for (int i = 0; i < params.length; i++) { + AnnotationNode local = SugarLocalProcessor.getLocalAnnotation(handler, i); + if (local == null) { + continue; + } + if (cleanSession == null) { + cleanSession = MixinBridge.session(context.classNode().name, handler, cleanTarget.classNode(), cleanTarget.methodNode(), context.environment()); + dirtySession = MixinBridge.session(context.classNode().name, handler, dirtyTarget.classNode(), dirtyTarget.methodNode(), context.environment()); + } + int cleanSlot = cleanSession.findLocal(local, params[i], cleanInsn); + int slot = dirtySession.findLocal(local, params[i], dirtyInsn); + report(actor, context, "@Local parameter " + i, cleanTarget, cleanSlot, cleanInsn, dirtyTarget, slot); + } + } + + private static void report(Object actor, MixinContext context, String selector, TargetPair cleanTarget, int cleanSlot, AbstractInsnNode cleanInsn, + TargetPair dirtyTarget, int slot) { + if (cleanSlot == -1 || slot == -1) { + return; + } + int expected = LocalLocator.resolve(context, cleanTarget, dirtyTarget, cleanSlot, cleanInsn, dirtyTarget); + if (expected != -1 && expected != slot) { + context.recordAudit(actor, "Warning: %s selects local %s, but local %s may now hold what clean local %s held", selector, slot, expected, cleanSlot); + } + } + + private AppliedLocalsCheck() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/HandlerValidator.java b/core/src/main/java/org/sinytra/adapter/pipeline/HandlerValidator.java new file mode 100644 index 00000000..45ec7f1c --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/HandlerValidator.java @@ -0,0 +1,123 @@ +package org.sinytra.adapter.pipeline; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.constant.TypeConstants; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.model.param.RedirectedInsn; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public final class HandlerValidator { + @Nullable + public static String validate(MixinContext context, Configuration dirty, Recipe recipe) { + if (dirty.getProperty(ControlKeys.DELETE).orElse(false) + || !Objects.equals(recipe.clean().getProperty(ControlKeys.MIXIN_TYPE), dirty.getProperty(ControlKeys.MIXIN_TYPE)) + || dirty.getTargetMethod() == null) { + return null; + } + + TargetPair target = recipe.getDirtyTarget(); + if (target == null) { + return "dirty target method %s not found".formatted(dirty.getTargetMethod().asDescriptor()); + } + + AtData at = dirty.getAtData(); + List insns; + if (at != null) { + insns = context.injections().find(target, InjectionQuery.of(at)); + } else if (dirty.hasProperty(MixinKeys.TARGET_CONSTANT)) { + insns = context.injections().find(target, InjectionQuery.constant()); + } else { + return null; + } + if (insns.isEmpty()) { + return "the injection point selects nothing in %s".formatted(target.methodNode().name + target.methodNode().desc); + } + + return validateSignature(context, context.getMixinType().signature(), dirty, target, insns.getFirst()); + } + + @Nullable + private static String validateSignature(MixinContext context, HandlerSignature signature, Configuration dirty, TargetPair target, AbstractInsnNode insn) { + MethodParameters params = dirty.getParameters(); + if (params == null) { + return null; + } + + Type[] handlerArgs = Type.getArgumentTypes(context.methodNode().desc); + if (!Arrays.asList(handlerArgs).equals(params.mergeTypes())) { + return "handler parameters %s don't match the expected %s".formatted(Arrays.toString(handlerArgs), params.mergeTypes()); + } + Type handlerReturn = Type.getReturnType(context.methodNode().desc); + if (!handlerReturn.equals(dirty.getReturnType())) { + return "handler returns %s instead of the expected %s".formatted(handlerReturn, dirty.getReturnType()); + } + + RedirectedInsn redirected = RedirectedInsn.of(insn); + List targetArgs = Parameters.getParameterTypes(target.methodNode().desc); + Type targetReturn = Type.getReturnType(target.methodNode().desc); + + Type expectedReturn = switch (signature.returns()) { + case VOID -> Type.VOID_TYPE; + case CONDITION -> Type.BOOLEAN_TYPE; + case CALL_RETURN -> redirected != null ? redirected.result() : null; + case TARGET_RETURN -> targetReturn; + case CALL_ARG, VARIABLE, CONSTANT -> null; + }; + if (expectedReturn != null && !expectedReturn.equals(handlerReturn)) { + return "handler returns %s but the injector expects %s".formatted(handlerReturn, expectedReturn); + } + + for (HandlerSignature.Slot slot : signature.slots()) { + if (!params.has(slot.group())) continue; + List group = params.get(slot.group()); + String problem = switch (slot.source()) { + case CALL_OPERANDS -> redirected == null ? null : checkTypes("call operand", group, redirected.operands()); + case OPERATION -> checkTypes("operation", group, List.of(TypeConstants.OPERATION_TYPE)); + case CALLBACK_INFO -> group.isEmpty() ? null + : checkTypes("callback info", group, List.of(targetReturn.equals(Type.VOID_TYPE) ? TypeConstants.CI_TYPE : TypeConstants.CIR_TYPE)); + case TARGET_ARGS -> group.isEmpty() ? null : checkTypes("target argument", group, targetArgs); + case CAPTURED_TARGET_ARGS -> group.size() > targetArgs.size() ? "handler captures %d target arguments, but the target has %d".formatted(group.size(), targetArgs.size()) + : checkTypes("captured target argument", group, targetArgs.subList(0, group.size())); + case VALUE -> expectedReturn == null ? null : checkTypes("value", group, List.of(expectedReturn)); + case KEEP -> null; + }; + if (problem != null) { + return problem; + } + } + return null; + } + + @Nullable + private static String checkTypes(String kind, List group, List expected) { + if (group.size() != expected.size()) { + return "handler has %d %s parameters, but the injector expects %d (%s)".formatted(group.size(), kind, expected.size(), expected); + } + for (int i = 0; i < group.size(); i++) { + Parameter param = group.get(i); + if (!param.type().equals(expected.get(i)) && !param.hasAnnotation(MixinAnnotations.COERCE)) { + return "%s parameter %d is %s, but the injector expects %s".formatted(kind, i, param.type(), expected.get(i)); + } + } + return null; + } + + private HandlerValidator() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/MixinParser.java b/core/src/main/java/org/sinytra/adapter/pipeline/MixinParser.java similarity index 79% rename from core/src/main/java/org/sinytra/adapter/patch/MixinParser.java rename to core/src/main/java/org/sinytra/adapter/pipeline/MixinParser.java index 2db8a10c..6b1eaed3 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/MixinParser.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/MixinParser.java @@ -1,30 +1,28 @@ -package org.sinytra.adapter.patch; +package org.sinytra.adapter.pipeline; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.env.ctx.MethodHelper; -import org.sinytra.adapter.env.ctx.PatchEnvironment; -import org.sinytra.adapter.env.ctx.RefMapper; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.config.MutablePropertyContainer; -import org.sinytra.adapter.patch.config.PropertyContainer; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.config.key.ControlKeys; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.patch.mixin.MixinType; -import org.sinytra.adapter.patch.mixin.MixinTypes; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.provider.ClassLookup; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.ClassTarget; +import org.sinytra.adapter.model.ann.RefMapper; +import org.sinytra.adapter.model.config.MutablePropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainer; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.pipeline.injector.MixinType; +import org.sinytra.adapter.pipeline.injector.MixinTypes; import java.util.ArrayList; import java.util.List; -import java.util.Objects; public class MixinParser { @@ -64,10 +62,10 @@ public static MixinMethodHandle parseMixin(ClassTarget cls, MethodNode method, R MutablePropertyContainer properties = MutablePropertyContainer.parse(handle, template, mapper); properties.setProperty(ControlKeys.MIXIN_TYPE, annotation.desc); - String targetClass = Objects.requireNonNullElseGet(cls.getSingle(), () -> cls.getTypes().getFirst()).getInternalName(); + String targetClass = cls.getTypes().getFirst().getInternalName(); properties.setProperty(ControlKeys.TARGET_CLASS, targetClass); properties.setProperty(ControlKeys.RETURN_TYPE, Type.getReturnType(method.desc)); - properties.setProperty(SpecialKeys.STATIC, MethodHelper.isStatic(method)); + properties.setProperty(ControlKeys.STATIC, MethodAnalyzer.isStatic(method)); resolveTargetDescriptor(properties, environment); return new MixinMethodHandle(mixinType, method, handle, properties); diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/MultiTargetSplitter.java b/core/src/main/java/org/sinytra/adapter/pipeline/MultiTargetSplitter.java new file mode 100644 index 00000000..c06f6b26 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/MultiTargetSplitter.java @@ -0,0 +1,128 @@ +package org.sinytra.adapter.pipeline; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.ClassTarget; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.MixinBridge; + +import java.util.ArrayList; +import java.util.List; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_METHOD; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_AT; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_SLICE; + +public final class MultiTargetSplitter { + private static final MultiTargetSplitter INSTANCE = new MultiTargetSplitter(); + + public static List split(ClassNode classNode, ClassTarget classTarget, MixinParser.MixinMethodHandle mixin, PatchEnvironment environment) { + if (mixin.properties().hasProperty(MixinKeys.TARGET_METHOD)) { + return List.of(mixin); + } + String targetClass = mixin.properties().getProperty(ControlKeys.TARGET_CLASS).orElse(null); + ClassNode clean = targetClass != null ? environment.cleanClassLookup().getClass(targetClass).orElse(null) : null; + List targets = clean != null ? findTargets(classNode, mixin.methodAnnotation(), clean, environment) : List.of(); + if (targets.size() < 2 || targets.stream().noneMatch(target -> needsPatching(classNode, mixin, targetClass, target, environment))) { + return List.of(mixin); + } + + MethodNode handler = mixin.methodNode(); + List methods = new ArrayList<>(); + methods.add(handler); + int position = classNode.methods.indexOf(handler); + for (int i = 1; i < targets.size(); i++) { + MethodNode copy = AdapterUtil.copyMethod(handler); + String name = handler.name + "$adapter$target$" + i; + for (int suffix = 1; AdapterUtil.findMethod(classNode, name, copy.desc) != null; suffix++) { + name = handler.name + "$adapter$target$" + i + "$" + suffix; + } + copy.name = name; + AnnotationHandle annotation = findAnnotation(copy, mixin.methodAnnotation().getDesc()); + annotation.setOrAppendNonNull(AT_METHOD, List.of(asSelector(targets.get(i)))); + annotation.setOrAppendNonNull("require", 0); + classNode.methods.add(position + i, copy); + methods.add(copy); + } + mixin.methodAnnotation().setOrAppendNonNull(AT_METHOD, List.of(asSelector(targets.getFirst()))); + environment.auditTrail().recordAudit(INSTANCE, classNode, "Split %s into one handler per target: %s", handler.name, + targets.stream().map(MultiTargetSplitter::asSelector).toList()); + + List handles = new ArrayList<>(); + for (MethodNode method : methods) { + MixinParser.MixinMethodHandle handle = MixinParser.parseMixin(classTarget, method, ref -> environment.refmapHolder().remap(classNode.name, ref), environment); + if (handle != null) { + handles.add(handle); + } + } + return handles; + } + + private static List findTargets(ClassNode mixinClass, AnnotationHandle annotation, ClassNode clean, PatchEnvironment environment) { + Object value = annotation.getValue(AT_METHOD).map(v -> v.get()).orElse(null); + List selectors = value instanceof List list ? list : value != null ? List.of(value) : List.of(); + List targets = new ArrayList<>(); + for (Object selector : selectors) { + if (!(selector instanceof String string)) { + return List.of(); + } + MixinBridge.MemberSelector member = MixinBridge.parseMemberSelector(environment.refmapHolder().remap(mixinClass.name, string)); + if (member == null || member.field() || member.name() == null && member.desc() == null) { + return List.of(); + } + MethodQualifier qualifier = new MethodQualifier(null, member.name(), member.desc()); + int matches = 0; + for (MethodNode method : clean.methods) { + if (matches < member.maxMatches() && qualifier.matches(null, method.name, method.desc)) { + matches++; + if (!targets.contains(method)) { + targets.add(method); + } + } + } + } + return targets; + } + + private static boolean needsPatching(ClassNode mixinClass, MixinParser.MixinMethodHandle mixin, String targetClass, MethodNode target, PatchEnvironment environment) { + ClassNode dirty = environment.dirtyClassLookup().getClass(targetClass).orElse(null); + MethodNode dirtyTarget = dirty != null ? AdapterUtil.findMethod(dirty, target.name, target.desc) : null; + if (dirtyTarget == null) { + return true; + } + AnnotationHandle annotation = mixin.methodAnnotation(); + AnnotationNode at = annotation.getNested(PROPERTY_AT).map(AnnotationHandle::unwrap).orElse(null); + if (at == null) { + return false; + } + AnnotationNode slice = annotation.getValue(PROPERTY_SLICE) + .map(handle -> handle.get() instanceof List list ? (list.isEmpty() ? null : (AnnotationNode) list.getFirst()) : (AnnotationNode) handle.get()) + .orElse(null); + return MixinBridge.session(mixinClass.name, mixin.methodNode(), dirty, dirtyTarget, environment) + .findInjectionPoint(annotation.unwrap(), at, slice) + .isEmpty(); + } + + @Nullable + private static AnnotationHandle findAnnotation(MethodNode method, String desc) { + return method.visibleAnnotations.stream() + .filter(annotation -> annotation.desc.equals(desc)) + .findFirst() + .map(AnnotationHandle::new) + .orElse(null); + } + + private static String asSelector(MethodNode method) { + return method.name + method.desc; + } + + private MultiTargetSplitter() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/Patcher.java b/core/src/main/java/org/sinytra/adapter/pipeline/PatcherImpl.java similarity index 59% rename from core/src/main/java/org/sinytra/adapter/patch/Patcher.java rename to core/src/main/java/org/sinytra/adapter/pipeline/PatcherImpl.java index a73d3bb1..6e237838 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/Patcher.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/PatcherImpl.java @@ -1,24 +1,23 @@ -package org.sinytra.adapter.patch; +package org.sinytra.adapter.pipeline; -import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.mojang.logging.LogUtils; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.MixinGroups; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.env.ctx.*; -import org.sinytra.adapter.env.util.MixinAnnotationConstants; -import org.sinytra.adapter.patch.MixinParser.MixinMethodHandle; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.mixin.MixinFlag; -import org.sinytra.adapter.patch.mixin.MixinType; -import org.sinytra.adapter.transform.ClassTransformer; -import org.sinytra.adapter.transform.MethodTransformer; -import org.sinytra.adapter.util.MethodQualifier; +import org.sinytra.adapter.api.*; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.ClassTarget; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.MixinAnnotationConstants; +import org.sinytra.adapter.pipeline.MixinParser.MixinMethodHandle; +import org.sinytra.adapter.pipeline.audit.MixinGroups; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.ctx.PatchContextImpl; +import org.sinytra.adapter.pipeline.injector.MixinFlag; +import org.sinytra.adapter.pipeline.injector.MixinType; import org.slf4j.Logger; import java.util.ArrayList; @@ -27,7 +26,7 @@ import static org.sinytra.adapter.util.AdapterUtil.MIXINPATCH; -public class Patcher { +public class PatcherImpl implements Patcher { private static final Logger LOGGER = LogUtils.getLogger(); // TODO Support all known mixin types from PatchInstance (incl. interface mixins) @@ -35,23 +34,24 @@ public class Patcher { private final List classPatches; private final Multimap methodTransformers; - private Patcher(PatchEnvironment environment, List classPatches, Multimap methodTransformers) { + public PatcherImpl(PatchEnvironment environment, List classPatches, Multimap methodTransformers) { this.environment = environment; this.classPatches = classPatches; this.methodTransformers = methodTransformers; } + @Override public PatchResult process(ClassNode classNode) { // Parse class data ClassTarget classTarget = MixinParser.prepareMixinClass(classNode, this.environment); if (classTarget == null || classTarget.getTypes().isEmpty()) return PatchResult.PASS; PatchResult result = PatchResult.PASS; - PatchContextImpl context = new PatchContextImpl(classNode, classTarget.getTypes(), this.environment); + PatchContextImpl context = new PatchContextImpl(classNode, classTarget, classTarget.getTypes(), this.environment); // Class-level transformations for (ClassTransformer patch : this.classPatches) { - result = result.or(patch.apply(classNode, classTarget, context)); + result = result.or(patch.apply(classNode, context)); } // Parse mixin methods @@ -59,14 +59,18 @@ public PatchResult process(ClassNode classNode) { MixinParser.MixinClassHandle mixinClass = MixinParser.parseMixins(classTarget, classNode, this.environment); if (mixinClass == null) return result; - // Mixin-level transformations + // Mixin-level transformations with one handler per target method + List mixins = new ArrayList<>(); for (MixinParser.MixinMethodHandle mixin : mixinClass.mixins()) { + mixins.addAll(MultiTargetSplitter.split(classNode, classTarget, mixin, this.environment)); + } + for (MixinParser.MixinMethodHandle mixin : mixins) { PatchResult subResult = processMixin(classNode, classTarget, context, mixin); result = result.or(subResult); } context.run(); - MixinGroups groups = MixinGroups.create(mixinClass.mixins().stream().map(MixinMethodHandle::methodNode).toList()); + MixinGroups groups = MixinGroups.create(mixins.stream().map(MixinMethodHandle::methodNode).toList()); this.environment.auditTrail().processGroups(classNode, groups); return result; @@ -80,31 +84,42 @@ private PatchResult processMixin(ClassNode classNode, ClassTarget classTarget, P AnnotationHandle atHandle = mixin.methodAnnotation().getNested(MixinAnnotationConstants.PROPERTY_AT).orElse(null); MixinType mixinType = mixin.mixinType(); Set flags = mixinType.getFlags(); - MixinContext mixinContext = new MixinContext(mixinType, patchContext, classTarget, classNode, mixin.methodNode(), mixin.methodAnnotation(), atHandle, flags); - String mixinId = mixinContext.getMixinId(); - // Build base config PropertyContainerTemplate template = mixin.mixinType().getConfigurationTemplate(); MutableConfiguration configuration = MutableConfiguration.create(template); configuration.mergeFrom(mixin.properties()); + MixinContext mixinContext = new MixinContext(mixinType, patchContext, classTarget, classNode, mixin.methodNode(), mixin.methodAnnotation(), atHandle, flags, configuration); + String mixinId = mixinContext.getMixinId(); + this.environment.auditTrail().prepareMethod(mixinContext); - int checkpoint = patchContext.checkpoint(); + PatchContextImpl.Checkpoint checkpoint = patchContext.checkpoint(); + PatchResult result; try { - return applyPatches(mixinContext, configuration); + result = applyPatches(mixinContext, configuration); } catch (Exception e) { LOGGER.error(MIXINPATCH, "Error patching mixin {}, reverting changes", mixinId, e); - - int index = classNode.methods.indexOf(mixinContext.methodNode()); - if (index != -1) { - classNode.methods.set(index, mixinContext.unmodifiedMethodNode()); - } - patchContext.rollback(checkpoint); - + rollback(classNode, patchContext, checkpoint, mixinContext); this.environment.auditTrail().recordResult(mixinContext, configuration, AuditTrail.Match.NONE); return PatchResult.PASS; } + + if (mixinContext.getRejection() != null) { + LOGGER.debug(MIXINPATCH, "Rejected patch of mixin {}, reverting changes: {}", mixinId, mixinContext.getRejection()); + rollback(classNode, patchContext, checkpoint, mixinContext); + this.environment.auditTrail().recordRejection(mixinContext, configuration); + return PatchResult.PASS; + } + return result; + } + + private static void rollback(ClassNode classNode, PatchContextImpl patchContext, PatchContextImpl.Checkpoint checkpoint, MixinContext mixinContext) { + int index = classNode.methods.indexOf(mixinContext.methodNode()); + if (index != -1) { + classNode.methods.set(index, mixinContext.unmodifiedMethodNode()); + } + patchContext.rollback(checkpoint); } private PatchResult applyPatches(MixinContext mixinContext, MutableConfiguration configuration) { @@ -112,17 +127,17 @@ private PatchResult applyPatches(MixinContext mixinContext, MutableConfiguration String mixinId = mixinContext.getMixinId(); // << RUN EARLY PHASE - PatchResult result = runPhase(TxPhase.EARLY, mixinContext, configuration); + PatchResult result = runPhase(TxPhase.EARLY, mixinContext); // Complete clean config - TxResult preResult = mixinType.preProcess(mixinContext, configuration, mixinContext.getResolvers(), mixinContext.getProcessors()); + TxResult preResult = mixinType.preProcess(mixinContext, configuration, mixinContext.getResolvers()); if (preResult == TxResult.FAIL) { LOGGER.debug(MIXINPATCH, "Skipping mixin {} due to failed preProcess", mixinId); return PatchResult.PASS; } // << RUN LOADED PHASE - result = result.or(runPhase(TxPhase.LOADED, mixinContext, configuration)); + result = result.or(runPhase(TxPhase.LOADED, mixinContext)); // Validate clean config if (!configuration.validate()) { @@ -135,52 +150,14 @@ private PatchResult applyPatches(MixinContext mixinContext, MutableConfiguration MethodNode methodNode = mixinContext.methodNode(); methodNode.maxLocals = methodNode.maxStack = 999; - return result.or(runPhase(TxPhase.VALIDATED, mixinContext, configuration)); + return result.or(runPhase(TxPhase.VALIDATED, mixinContext)); } - private PatchResult runPhase(TxPhase phase, MixinContext mixinContext, MutableConfiguration configuration) { + private PatchResult runPhase(TxPhase phase, MixinContext mixinContext) { PatchResult result = PatchResult.PASS; for (MethodTransformer transformer : this.methodTransformers.get(phase)) { - result = result.or(mixinContext.audited(transformer, () -> transformer.apply(mixinContext, configuration))); + result = result.or(mixinContext.audited(transformer, () -> transformer.apply(mixinContext))); } return result; } - - public static Builder builder(PatchEnvironment environment) { - return new Builder(environment); - } - - public static class Builder { - private final PatchEnvironment environment; - private final List classTransformers = new ArrayList<>(); - private final Multimap methodTransformers = HashMultimap.create(); - - public Builder(PatchEnvironment environment) { - this.environment = environment; - } - - public Builder classTransformer(ClassTransformer transformer) { - this.classTransformers.add(transformer); - return this; - } - - public Builder classTransformers(List transformers) { - this.classTransformers.addAll(transformers); - return this; - } - - public Builder methodTransformer(TxPhase phase, MethodTransformer transformer) { - this.methodTransformers.put(phase, transformer); - return this; - } - - public Builder methodTransformers(Multimap transformers) { - this.methodTransformers.putAll(transformers); - return this; - } - - public Patcher build() { - return new Patcher(this.environment, this.classTransformers, this.methodTransformers); - } - } } diff --git a/core/src/main/java/org/sinytra/adapter/transform/PipelineMethodTransformer.java b/core/src/main/java/org/sinytra/adapter/pipeline/PipelineMethodTransformer.java similarity index 59% rename from core/src/main/java/org/sinytra/adapter/transform/PipelineMethodTransformer.java rename to core/src/main/java/org/sinytra/adapter/pipeline/PipelineMethodTransformer.java index e9e98271..b2c893ff 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/PipelineMethodTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/PipelineMethodTransformer.java @@ -1,33 +1,33 @@ -package org.sinytra.adapter.transform; +package org.sinytra.adapter.pipeline; import com.mojang.logging.LogUtils; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ann.SliceData; -import org.sinytra.adapter.env.ctx.AuditTrail; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.ControlKeys; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.mixin.MixinType; -import org.sinytra.adapter.patch.mixin.MixinTypes; -import org.sinytra.adapter.patch.processor.Processor; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.transform.patch.MethodPatch; -import org.sinytra.adapter.transform.patch.MethodPatchResolver; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.api.AuditTrail; +import org.sinytra.adapter.api.MethodPatch; +import org.sinytra.adapter.api.MethodPatchContext; +import org.sinytra.adapter.api.MethodTransformer; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.ann.SliceData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.injector.MixinType; +import org.sinytra.adapter.pipeline.injector.MixinTypes; +import org.sinytra.adapter.pipeline.manual.MethodPatchResolver; +import org.sinytra.adapter.pipeline.processor.Processor; +import org.sinytra.adapter.pipeline.processor.Processors; +import org.sinytra.adapter.pipeline.resolver.InjectionPointResolver; +import org.sinytra.adapter.pipeline.resolver.Resolver; +import org.sinytra.adapter.pipeline.resolver.Resolvers; import org.slf4j.Logger; -import org.spongepowered.asm.mixin.injection.InjectionPoint; -import org.spongepowered.asm.mixin.injection.points.BeforeConstant; import java.util.Arrays; import java.util.List; @@ -47,13 +47,28 @@ public PipelineMethodTransformer(List methodPatches, boolean patche } @Override + public PatchResult apply(MethodPatchContext patchContext) { + MixinContext context = (MixinContext) patchContext; + return apply(context, context.configuration()); + } + public PatchResult apply(MixinContext context, Configuration config) { - TargetPair cleanTarget = context.methods().findOwnMethodPair(context.cleanLookup(), config.getTargetMethod()); + TargetPair cleanTarget = context.cleanMethods().resolve(config.getTargetMethod()); if (cleanTarget == null) return PatchResult.PASS; - TargetPair dirtyTarget = context.methods().findOwnMethodPair(context.dirtyLookup(), config.getTargetMethod()); - if (!this.patchResolver.matches(config) && !failsDirtyInjectionCheck(context, config, dirtyTarget) && hasValidSlice(context, config, dirtyTarget)) + TargetPair dirtyTarget = context.dirtyMethods().resolve(config.getTargetMethod()); + if (!this.patchResolver.matches(config) && !failsDirtyInjectionCheck(context, config, dirtyTarget) && hasValidSlice(context, config, dirtyTarget) + && !selectsShiftedInsn(context, config, cleanTarget, dirtyTarget) + ) { + if (dirtyTarget != null && !this.patchesOnly) { + try { + AppliedLocalsCheck.check(this, context, cleanTarget, dirtyTarget); + } catch (Exception e) { + LOGGER.debug(MIXINPATCH, "Failed to check the local selectors of applying mixin {}", context.getMixinId(), e); + } + } return PatchResult.PASS; + } LOGGER.debug(MIXINPATCH, "Considering method {}", context.getMixinId()); @@ -113,6 +128,9 @@ private PatchResult execute(MixinContext context, Configuration config) { String type = dirtyConfig.getMixinType(); MixinType lateMixinType = MixinTypes.getMixinType(Type.getType(type).getInternalName()); if (lateMixinType != null) { + if (lateMixinType != context.getMixinType()) { + cleanConfig.setParameters(lateMixinType.signature().parse(context.methodNode())); + } TxResult postResult = lateMixinType.postProcess(context, cleanConfig, dirtyConfig, recipe); if (postResult == TxResult.FAIL) { LOGGER.debug(MIXINPATCH, "Skipping mixin {} due to failed postProcess", mixinId); @@ -135,7 +153,16 @@ private PatchResult execute(MixinContext context, Configuration config) { break; } if (res == TxResult.FAIL) { - LOGGER.debug(MIXINPATCH, "Skipping mixin {} due to failed PROCESSOR {}", mixinId, processor.getClass().getSimpleName()); + context.reject(processor, "processor %s failed".formatted(processor.getClass().getSimpleName())); + return PatchResult.PASS; + } + } + + // 5. Check that the patched mixin will apply + if (!this.patchesOnly && !this.patchResolver.matches(config)) { + String problem = HandlerValidator.validate(context, recipe.dirty(), recipe); + if (problem != null) { + context.reject(this, "validation failed: " + problem); return PatchResult.PASS; } } @@ -151,21 +178,30 @@ private PatchResult execute(MixinContext context, Configuration config) { return PatchResult.APPLY; } + private boolean selectsShiftedInsn(MixinContext context, Configuration config, TargetPair cleanTarget, @Nullable TargetPair dirtyTarget) { + AtData at = config.getAtData(); + if (this.patchesOnly || dirtyTarget == null || at == null || at.getOrdinal().isEmpty()) { + return false; + } + try { + List insns = context.injections().find(dirtyTarget); + if (insns.isEmpty() || InjectionPointResolver.selectsLocatedInsns(context, cleanTarget, dirtyTarget, insns)) { + return false; + } + context.recordAudit(this, "The @At still applies, but ordinal %s now selects another instruction than the clean one became", + at.getOrdinal().get()); + return true; + } catch (Exception e) { + LOGGER.debug(MIXINPATCH, "Failed to check the injection point of applying mixin {}", context.getMixinId(), e); + return false; + } + } + public boolean failsDirtyInjectionCheck(MixinContext context, Configuration config, TargetPair dirtyTarget) { return !context.getMixinType().canInject(context, config) || dirtyTarget == null - || !context.methods().hasInjectionTargetInsns(dirtyTarget) - && computeConstantTargetInsns(context, dirtyTarget).isEmpty(); - } - - // TODO Clean up - public List computeConstantTargetInsns(MixinContext context, @Nullable TargetPair target) { - return context.methods().computeInjectionTargetInsns( - target, - () -> context.methodAnnotation().getNested("constant").orElse(null), - (ctx, h) -> new BeforeConstant(ctx, h.unwrap(), Type.getReturnType(context.methodNode().desc).getDescriptor()), - false - ); + || !context.injections().matches(dirtyTarget) + && !context.injections().matches(dirtyTarget, InjectionQuery.constant()); } public boolean hasValidSlice(MixinContext context, Configuration config, @Nullable TargetPair target) { @@ -178,24 +214,13 @@ public boolean hasValidSlice(MixinContext context, Configuration config, @Nullab .orElse(List.of()); for (SliceData slice : slices) { AtData from = slice.from(); - if (from != null && !validateAtNode(context, from, target)) + if (from != null && !context.injections().matches(target, InjectionQuery.sliceBoundary(from))) return false; AtData to = slice.to(); - if (to != null && !validateAtNode(context, to, target)) + if (to != null && !context.injections().matches(target, InjectionQuery.sliceBoundary(to))) return false; } return true; } - - private boolean validateAtNode(MixinContext context, AtData at, TargetPair target) { - List insns = context.methods().computeInjectionTargetInsns( - target, - context::injectionPointAnnotation, - (ctx, h) -> InjectionPoint.parse(ctx, context.methodNode(), context.methodAnnotation().unwrap(), at.toAnnotationNode()), - false, - true - ); - return !insns.isEmpty(); - } } diff --git a/core/src/main/java/org/sinytra/adapter/patch/Recipe.java b/core/src/main/java/org/sinytra/adapter/pipeline/Recipe.java similarity index 72% rename from core/src/main/java/org/sinytra/adapter/patch/Recipe.java rename to core/src/main/java/org/sinytra/adapter/pipeline/Recipe.java index f7752164..fd44f2b8 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/Recipe.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/Recipe.java @@ -1,18 +1,19 @@ -package org.sinytra.adapter.patch; +package org.sinytra.adapter.pipeline; import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; +import org.sinytra.adapter.analysis.method.MethodLocator; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.processor.Processors; +import org.sinytra.adapter.pipeline.resolver.Resolvers; import org.sinytra.adapter.util.Cached; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.util.provider.ClassLookup; -import org.sinytra.adapter.env.ctx.Targets; -import org.objectweb.asm.tree.AbstractInsnNode; import java.util.Objects; @@ -53,23 +54,33 @@ private Recipe(Configuration clean, Configuration dirty, Resolvers resolvers, Pr this.newCleanTarget = original.newCleanTarget; this.cleanLocalsTable = original.cleanLocalsTable; } else { - this.cleanTarget = Cached.of(() -> findTarget(context, context.cleanLookup(), clean.getTargetMethod())); - this.newCleanTarget = Cached.of(() -> findTarget(context, context.dirtyLookup(), clean.getTargetMethod())); - this.cleanLocalsTable = Cached.of(() -> getLocalsTable(context, getCleanTarget())); + this.cleanTarget = Cached.of(() -> findTarget(context.cleanMethods(), clean.getTargetMethod())); + this.newCleanTarget = Cached.of(() -> findTarget(context.dirtyMethods(), clean.getTargetMethod())); + this.cleanLocalsTable = Cached.of(() -> getLocalsTable(getCleanTarget())); } - this.dirtyTarget = Cached.keyed(dirty::getTargetMethod, target -> findTarget(context, context.dirtyLookup(), target)); - this.dirtyLocalsTable = Cached.keyed(this::getDirtyTarget, target -> getLocalsTable(context, target)); + this.dirtyTarget = Cached.keyed(this::getDirtyTargetQualifier, target -> findTarget(context.dirtyMethods(), target)); + this.dirtyLocalsTable = Cached.keyed(this::getDirtyTarget, target -> getLocalsTable(target)); + } + + @Nullable + private MethodQualifier getDirtyTargetQualifier() { + MethodQualifier target = this.dirty.getTargetMethod(); + String targetClass = this.dirty.getTargetClass(); + if (target != null && target.owner() == null && targetClass != null && !targetClass.equals(this.clean.getTargetClass())) { + return target.withOwner(Type.getObjectType(targetClass)); + } + return target; } @Nullable - private static TargetPair findTarget(MixinContext context, ClassLookup lookup, @Nullable MethodQualifier target) { - return target != null ? context.methods().findOwnMethodPair(lookup, target) : null; + private static TargetPair findTarget(MethodLocator locator, @Nullable MethodQualifier target) { + return target != null ? locator.resolve(target) : null; } @Nullable - private static LocalVariableLookup getLocalsTable(MixinContext context, @Nullable TargetPair target) { - return target != null ? context.methods().getLVT(target.methodNode()) : null; + private static LocalVariableLookup getLocalsTable(@Nullable TargetPair target) { + return target != null ? LocalVariableLookup.of(target.methodNode()) : null; } @Nullable @@ -107,7 +118,7 @@ public Targets targets() { @Nullable public AbstractInsnNode getCleanInjectionInsn() { - return this.context.methods().findInjectionTargetInsn(getCleanTarget()); + return this.context.injections().resolve(getCleanTarget()); } public boolean hasInjectionPointValue(String value) { diff --git a/core/src/main/java/org/sinytra/adapter/patch/TxResult.java b/core/src/main/java/org/sinytra/adapter/pipeline/TxResult.java similarity index 65% rename from core/src/main/java/org/sinytra/adapter/patch/TxResult.java rename to core/src/main/java/org/sinytra/adapter/pipeline/TxResult.java index 3429d7b0..9997eaac 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/TxResult.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/TxResult.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.patch; +package org.sinytra.adapter.pipeline; public enum TxResult { SUCCESS, diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/AuditTrailImpl.java b/core/src/main/java/org/sinytra/adapter/pipeline/audit/AuditTrailImpl.java similarity index 92% rename from core/src/main/java/org/sinytra/adapter/env/ctx/AuditTrailImpl.java rename to core/src/main/java/org/sinytra/adapter/pipeline/audit/AuditTrailImpl.java index 09e1287b..3e2a33b4 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/AuditTrailImpl.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/audit/AuditTrailImpl.java @@ -1,14 +1,15 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.pipeline.audit; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.Pair; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.MixinGroups; -import org.sinytra.adapter.analysis.MixinGroups.GroupInfo; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.MixinKeys; +import org.sinytra.adapter.api.AuditTrail; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.pipeline.audit.MixinGroups.GroupInfo; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.slf4j.Logger; import java.text.DecimalFormat; @@ -70,6 +71,12 @@ public void recordResult(MixinContext context, Configuration configuration, Matc }); } + @Override + public void recordRejection(MixinContext context, Configuration configuration) { + boolean isNotRequired = configuration.getProperty(MixinKeys.REQUIRE).map(i -> i == 0).orElse(false); + this.candidates.put(new Candidate(context.classNode(), context.methodNode()), isNotRequired ? Match.IGNORED : Match.NONE); + } + public String getCompleteReport() { StringBuilder builder = new StringBuilder(); diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/Auditor.java b/core/src/main/java/org/sinytra/adapter/pipeline/audit/Auditor.java similarity index 69% rename from core/src/main/java/org/sinytra/adapter/env/ctx/Auditor.java rename to core/src/main/java/org/sinytra/adapter/pipeline/audit/Auditor.java index 318d2d7c..fe8639fa 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/Auditor.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/audit/Auditor.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.pipeline.audit; public interface Auditor { void recordAudit(Object actor, String message, Object... args); diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/audit/MixinGroups.java b/core/src/main/java/org/sinytra/adapter/pipeline/audit/MixinGroups.java new file mode 100644 index 00000000..f61f2f73 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/audit/MixinGroups.java @@ -0,0 +1,35 @@ +package org.sinytra.adapter.pipeline.audit; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.util.MixinBridge; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MixinGroups { + private final Map groups; + + private MixinGroups(Map groups) { + this.groups = Map.copyOf(groups); + } + + @Nullable + public GroupInfo getGroupInfo(MethodNode methodNode) { + String name = MixinBridge.getGroupName(methodNode); + return name != null ? this.groups.get(name) : null; + } + + public static MixinGroups create(List methods) { + Map groups = new HashMap<>(); + for (MixinBridge.InjectorGroup group : MixinBridge.findInjectorGroups(methods)) { + groups.put(group.name(), new GroupInfo(group.members(), group.minRequired())); + } + return new MixinGroups(groups); + } + + public record GroupInfo(Collection members, int minRequired) { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/MixinContext.java b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/MixinContext.java similarity index 50% rename from core/src/main/java/org/sinytra/adapter/env/ctx/MixinContext.java rename to core/src/main/java/org/sinytra/adapter/pipeline/ctx/MixinContext.java index 25c5373a..65331cc5 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/MixinContext.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/MixinContext.java @@ -1,26 +1,43 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.pipeline.ctx; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.patch.mixin.MixinFlag; -import org.sinytra.adapter.patch.mixin.MixinType; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.types.BytecodeFixerUpper; -import org.sinytra.adapter.types.TypeAdapter; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.analysis.injection.InjectionLocator; +import org.sinytra.adapter.analysis.locals.LocalsAnalyzer; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.analysis.method.MethodLocator; +import org.sinytra.adapter.analysis.params.TypeRelation; +import org.sinytra.adapter.api.BytecodeFixerUpper; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.MethodPatchContext; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.ClassTarget; +import org.sinytra.adapter.model.ann.RefMapper; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.pipeline.audit.Auditor; +import org.sinytra.adapter.pipeline.injector.MixinFlag; +import org.sinytra.adapter.pipeline.injector.MixinType; +import org.sinytra.adapter.pipeline.processor.Processors; +import org.sinytra.adapter.pipeline.resolver.Resolvers; import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.provider.ClassLookup; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Supplier; -public class MixinContext implements RefMapper, Auditor { +public class MixinContext implements MethodPatchContext, RefMapper, Auditor { private final MixinType mixinType; private final PatchContext patchContext; private final ClassTarget classTarget; @@ -28,17 +45,25 @@ public class MixinContext implements RefMapper, Auditor { private final MethodNode methodNode; private final MethodNode originalMethodNode; private final Set flags; + private final MutableConfiguration configuration; + @Nullable + private String rejection; private final AnnotationHandle methodAnnotation; private final AnnotationHandle injectionPointAnnotation; private final List auditContext = new ArrayList<>(); private final String mixinId; - private final MethodHelper methodHelper; + private final MethodLocator cleanMethods; + private final MethodLocator dirtyMethods; + private final InjectionLocator injections; + private final LocalsAnalyzer locals; + + private final Map targetFlows; private final Resolvers resolvers = new Resolvers(); private final Processors processors = new Processors(); - public MixinContext(MixinType mixinType, PatchContext patchContext, ClassTarget classTarget, ClassNode classNode, MethodNode methodNode, AnnotationHandle methodAnnotation, AnnotationHandle injectionPointAnnotation, Set flags) { + public MixinContext(MixinType mixinType, PatchContext patchContext, ClassTarget classTarget, ClassNode classNode, MethodNode methodNode, AnnotationHandle methodAnnotation, AnnotationHandle injectionPointAnnotation, Set flags, MutableConfiguration configuration) { this.mixinType = mixinType; this.patchContext = patchContext; this.classTarget = classTarget; @@ -47,11 +72,18 @@ public MixinContext(MixinType mixinType, PatchContext patchContext, ClassTarget this.methodAnnotation = methodAnnotation; this.injectionPointAnnotation = injectionPointAnnotation; this.flags = flags; + this.configuration = configuration; - this.methodHelper = new MethodHelper(this, classTarget.getTypes()); + this.injections = new InjectionLocator(this); + this.locals = new LocalsAnalyzer(this); + String defaultOwner = classTarget.getTypes().getFirst().getInternalName(); + this.cleanMethods = new MethodLocator(patchContext.environment(), patchContext.environment().cleanClassLookup(), defaultOwner); + this.dirtyMethods = new MethodLocator(patchContext.environment(), patchContext.environment().dirtyClassLookup(), defaultOwner); this.originalMethodNode = AdapterUtil.copyMethod(this.methodNode); this.mixinId = classNode.name + "#" + methodNode.name + methodNode.desc; + + this.targetFlows = new HashMap<>(); } public MixinType getMixinType() { @@ -79,24 +111,53 @@ public ClassTarget classTarget() { return this.classTarget; } + @Override public ClassNode classNode() { return this.classNode; } + @Override public MethodNode methodNode() { return this.methodNode; } + public MutableConfiguration configuration() { + return this.configuration; + } + public MethodNode unmodifiedMethodNode() { return this.originalMethodNode; } - public MethodHelper methods() { - return this.methodHelper; + public MethodLocator cleanMethods() { + return this.cleanMethods; + } + + public TypeRelation typeRelation() { + return TypeRelation.inheritance(this.patchContext.environment().inheritanceHandler(dirtyLookup())); + } + + public MethodLocator dirtyMethods() { + return this.dirtyMethods; + } + + public InjectionLocator injections() { + return this.injections; + } + + public LocalsAnalyzer locals() { + return this.locals; + } + + public DataFlow dataFlow(TargetPair target) { + return this.targetFlows.computeIfAbsent(target.methodNode(), m -> DataFlow.of(target)); } - public ClassLookup cleanLookup() { - return this.patchContext.environment().cleanClassLookup(); + @Nullable + public Targets targets(MethodQualifier qualifier) { + TargetPair clean = this.cleanMethods.resolve(qualifier); + TargetPair dirty = this.dirtyMethods.resolve(qualifier); + return clean != null && dirty != null ? new Targets(clean, dirty) : null; } public ClassLookup dirtyLookup() { @@ -107,6 +168,11 @@ public AnnotationHandle methodAnnotation() { return this.methodAnnotation; } + @Override + public AnnotationNode injectorAnnotation() { + return this.methodAnnotation.unwrap(); + } + @Nullable public AnnotationHandle injectionPointAnnotation() { return this.injectionPointAnnotation; @@ -128,13 +194,14 @@ public PatchContext patchContext() { } public boolean isStatic() { - return MethodHelper.isStatic(this.methodNode); + return MethodAnalyzer.isStatic(this.methodNode); } public List targetTypes() { return this.classTarget.getTypes(); } + @Override public PatchEnvironment environment() { return patchContext().environment(); } @@ -148,6 +215,18 @@ public T audited(Object actor, Supplier action) { } } + public void reject(Object actor, String reason) { + if (this.rejection == null) { + this.rejection = reason; + } + recordAudit(actor, "Rejected: %s", reason); + } + + @Nullable + public String getRejection() { + return this.rejection; + } + public void recordCtxAudit(String message, Object... args) { if (this.auditContext.isEmpty()) { throw new RuntimeException("Missing audit context object"); diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchContext.java b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchContext.java new file mode 100644 index 00000000..f848cec8 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchContext.java @@ -0,0 +1,15 @@ +package org.sinytra.adapter.pipeline.ctx; + +import org.jetbrains.annotations.Nullable; +import org.sinytra.adapter.api.ClassPatchContext; +import org.sinytra.adapter.model.ann.ClassTarget; +import org.sinytra.adapter.model.ann.RefMapper; + +public interface PatchContext extends ClassPatchContext, RefMapper { + @Nullable + ClassTarget classTarget(); + + void postApply(Runnable consumer); + + void onRollback(Runnable undo); +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchContextImpl.java b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchContextImpl.java new file mode 100644 index 00000000..0c97ca26 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchContextImpl.java @@ -0,0 +1,81 @@ +package org.sinytra.adapter.pipeline.ctx; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.model.ann.ClassTarget; + +import java.util.ArrayList; +import java.util.List; + +public class PatchContextImpl implements PatchContext { + private final ClassNode classNode; + @Nullable + private final ClassTarget classTarget; + private final List targetTypes; + private final PatchEnvironment environment; + private final List postApply = new ArrayList<>(); + private final List undo = new ArrayList<>(); + + public record Checkpoint(int postApply, int undo) { + } + + public PatchContextImpl(ClassNode classNode, @Nullable ClassTarget classTarget, List targetTypes, PatchEnvironment environment) { + this.classNode = classNode; + this.classTarget = classTarget; + this.targetTypes = targetTypes; + this.environment = environment; + } + + @Override + public ClassNode classNode() { + return this.classNode; + } + + @Override + @Nullable + public ClassTarget classTarget() { + return this.classTarget; + } + + @Override + public List targetTypes() { + return this.classTarget != null ? this.classTarget.getTypes() : this.targetTypes; + } + + @Override + public PatchEnvironment environment() { + return this.environment; + } + + @Override + public String remap(String reference) { + return this.environment.refmapHolder().remap(this.classNode.name, reference); + } + + @Override + public void postApply(Runnable consumer) { + this.postApply.add(consumer); + } + + @Override + public void onRollback(Runnable undo) { + this.undo.add(undo); + } + + public Checkpoint checkpoint() { + return new Checkpoint(this.postApply.size(), this.undo.size()); + } + + public void rollback(Checkpoint checkpoint) { + this.postApply.subList(checkpoint.postApply(), this.postApply.size()).clear(); + List undone = this.undo.subList(checkpoint.undo(), this.undo.size()); + undone.reversed().forEach(Runnable::run); + undone.clear(); + } + + public void run() { + this.postApply.forEach(Runnable::run); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchEnvironmentImpl.java b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchEnvironmentImpl.java similarity index 86% rename from core/src/main/java/org/sinytra/adapter/env/ctx/PatchEnvironmentImpl.java rename to core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchEnvironmentImpl.java index e355f22e..a96d1eb9 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ctx/PatchEnvironmentImpl.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/ctx/PatchEnvironmentImpl.java @@ -1,11 +1,17 @@ -package org.sinytra.adapter.env.ctx; +package org.sinytra.adapter.pipeline.ctx; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.analysis.InheritanceHandler; -import org.sinytra.adapter.types.BytecodeFixerUpper; +import org.sinytra.adapter.analysis.method.InheritanceHandler; +import org.sinytra.adapter.api.AuditTrail; +import org.sinytra.adapter.api.BytecodeFixerUpper; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.MixinClassGenerator; +import org.sinytra.adapter.api.MixinClassLookup; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.api.RefmapHolder; +import org.sinytra.adapter.edit.MixinClassGeneratorImpl; +import org.sinytra.adapter.pipeline.audit.AuditTrailImpl; import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.provider.ClassLookup; -import org.sinytra.adapter.util.provider.MixinClassLookup; import java.util.Collection; import java.util.Map; diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/AbstractMixinType.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/AbstractMixinType.java similarity index 87% rename from core/src/main/java/org/sinytra/adapter/patch/mixin/AbstractMixinType.java rename to core/src/main/java/org/sinytra/adapter/pipeline/injector/AbstractMixinType.java index 9a06246f..3de70ce4 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/AbstractMixinType.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/AbstractMixinType.java @@ -1,6 +1,6 @@ -package org.sinytra.adapter.patch.mixin; +package org.sinytra.adapter.pipeline.injector; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; import java.util.Arrays; import java.util.Collections; diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/HandlerSignatures.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/HandlerSignatures.java new file mode 100644 index 00000000..c58eac00 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/HandlerSignatures.java @@ -0,0 +1,117 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.constant.MixinAnnotationConstants; +import org.sinytra.adapter.model.constant.TypeConstants; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.model.param.RedirectedInsn; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; + +public final class HandlerSignatures { + + public static TxResult apply(MixinContext context, HandlerSignature signature, Configuration clean, MutableConfiguration dirty, Recipe recipe, @Nullable Type value) { + Type valueType = switch (signature.returns()) { + case VOID -> Type.VOID_TYPE; + case CONDITION -> Type.BOOLEAN_TYPE; + case CALL_RETURN -> { + RedirectedInsn redirected = isInvoke(dirty) ? null : findRedirected(context, recipe, dirty); + MethodQualifier call = getCallQualifier(dirty); + yield redirected != null ? redirected.result() : call != null ? Type.getReturnType(call.desc()) : null; + } + case TARGET_RETURN -> dirty.getTargetMethod() != null && dirty.getTargetMethod().desc() != null + ? Type.getReturnType(dirty.getTargetMethod().desc()) : null; + case CALL_ARG, VARIABLE, CONSTANT -> value; + }; + if (valueType == null) { + return TxResult.FAIL; + } + + MethodParameters.Builder params = clean.getParameters().mutableCopy(); + for (HandlerSignature.Slot slot : signature.slots()) { + switch (slot.source()) { + case TARGET_ARGS -> { + if (dirty.getTargetMethod() == null || dirty.getTargetMethod().desc() == null) return TxResult.FAIL; + + params.putTypes(slot.group(), Parameters.getParameterTypes(dirty.getTargetMethod().desc())); + } + case CALL_OPERANDS -> { + RedirectedInsn redirected = isInvoke(dirty) ? null : findRedirected(context, recipe, dirty); + List operands = redirected != null ? redirected.operands() : findCallOperands(context, recipe, dirty); + if (operands == null) return TxResult.FAIL; + + params.putTypes(slot.group(), operands); + } + case OPERATION -> params.putType(slot.group(), TypeConstants.OPERATION_TYPE); + case VALUE -> params.putType(slot.group(), valueType); + case CAPTURED_TARGET_ARGS -> params.putTypes(slot.group(), context.locals().findDirtyCapturedParams(recipe.clean(), recipe.dirty())); + default -> { + } + } + } + + dirty.setParameters(params.build()); + dirty.setReturnType(valueType); + return TxResult.SUCCESS; + } + + @Nullable + private static MethodQualifier getCallQualifier(Configuration dirty) { + AtData at = dirty.getAtData(); + MethodQualifier call = at != null ? at.getTargetQualifier().orElse(null) : null; + return call != null && call.desc() != null ? call : null; + } + + private static boolean isInvoke(Configuration dirty) { + AtData at = dirty.getAtData(); + return at == null || MixinAnnotationConstants.AT_VAL_INVOKE.equals(at.getValue()) || MixinAnnotationConstants.AT_VAL_INVOKE_ASSIGN.equals(at.getValue()); + } + + @Nullable + private static RedirectedInsn findRedirected(MixinContext context, Recipe recipe, Configuration dirty) { + AbstractInsnNode insn = dirty.getAtData() != null ? context.injections().resolveFirst(recipe.getDirtyTarget(), InjectionQuery.of(dirty.getAtData())) : null; + return insn != null ? RedirectedInsn.of(insn) : null; + } + + @Nullable + private static List findCallOperands(MixinContext context, Recipe recipe, Configuration dirty) { + MethodQualifier call = getCallQualifier(dirty); + if (call == null) return null; + + TargetPair dirtyTarget = recipe.getDirtyTarget(); + AbstractInsnNode insn = context.injections().resolveFirst(dirtyTarget, InjectionQuery.of(dirty.getAtData())); + + boolean isStatic; + String owner; + if (insn instanceof MethodInsnNode minsn) { + isStatic = minsn.getOpcode() == Opcodes.INVOKESTATIC; + owner = minsn.owner; + } else { + TargetPair method = call.isFull() ? context.dirtyMethods().resolveInherited(call) : context.dirtyMethods().resolve(call); + if (method == null) return null; + isStatic = MethodAnalyzer.isStatic(method.methodNode()); + owner = call.internalOwnerName() != null ? call.internalOwnerName() : method.classNode().name; + } + + return Parameters.getCallOperandTypes(isStatic ? null : Type.getObjectType(owner), call.desc()); + } + + private HandlerSignatures() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/InjectMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/InjectMixin.java new file mode 100644 index 00000000..86559396 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/InjectMixin.java @@ -0,0 +1,113 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.objectweb.asm.Type; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.locate.LocatorChain; +import org.sinytra.adapter.pipeline.locate.NearestCallLocator; +import org.sinytra.adapter.pipeline.locate.ScopeSearch; +import org.sinytra.adapter.pipeline.resolver.InjectionPointResolver; +import org.sinytra.adapter.pipeline.resolver.Resolvers; + +import java.util.List; +import java.util.Objects; + +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.*; + +public class InjectMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() + .keys(MixinKeys.SLICES, MixinKeys.CANCELLABLE) + .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.VOID, + slot(METHOD_PARAMS, TARGET_ARGS), + slot(CI_CIR, CALLBACK_INFO), + slot(LOCALS, KEEP) + ); + + public InjectMixin() { + super(TEMPLATE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return RelocationPlan.Passthrough.SKIP; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + // An @Inject only needs a position, so it can fall back to the nearest call + resolvers.getOrThrow(InjectionPointResolver.class) + .addChain(new LocatorChain(List.of(new NearestCallLocator()), ScopeSearch.Reach.TARGET)); + + clean.setParameters(SIGNATURE.parse(context.methodNode())); + + // TODO Handle slices +// mixin.getProperty(Configuration.Keys.SLICES) +// .ifPresent(p -> clean.setProperty(Configuration.Keys.SLICES, p)); + + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + dirty.setReturnType(Type.VOID_TYPE); + if (dirty.getTargetMethod() == null || dirty.getTargetMethod().desc() == null) { + return TxResult.SUCCESS; + } + + if (sameTarget(recipe) || clean.getParameters().getTypes(METHOD_PARAMS).isEmpty()) { + dirty.inheritParameters(); + } else if (HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, null) == TxResult.FAIL) { + return TxResult.FAIL; + } + + // Process static modifier + if (!clean.getProperty(ControlKeys.STATIC).orElse(false) && dirty.getProperty(ControlKeys.STATIC).orElse(false)) { + List types = context.targetTypes(); + if (types.size() != 1) { + return TxResult.FAIL; + } + MethodParameters params = dirty.getParameters(); + List methodParams = params.get(METHOD_PARAMS); + Type targetType = types.getFirst(); + methodParams.addFirst(Parameter.simple(targetType)); + } + + return TxResult.SUCCESS; + } + + private boolean sameTarget(Recipe recipe) { + MethodQualifier cleanQ = recipe.clean().getTargetMethod(); + MethodQualifier dirtyQ = recipe.dirty().getTargetMethod(); + if (cleanQ != null && dirtyQ != null && Objects.equals(cleanQ.name(), dirtyQ.name()) && Objects.equals(cleanQ.desc(), dirtyQ.desc())) { + return true; + } + + Targets targets = recipe.targets(); + return targets != null && MethodQualifier.create(targets.clean().methodNode()).matches(MethodQualifier.create(targets.dirty().methodNode())); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinFlag.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinFlag.java similarity index 89% rename from core/src/main/java/org/sinytra/adapter/patch/mixin/MixinFlag.java rename to core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinFlag.java index 64a798b5..7decede0 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinFlag.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinFlag.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.patch.mixin; +package org.sinytra.adapter.pipeline.injector; public enum MixinFlag { /** diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinType.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinType.java new file mode 100644 index 00000000..0815cc14 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinType.java @@ -0,0 +1,41 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.jetbrains.annotations.Nullable; +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolvers; + +import java.util.EnumSet; +import java.util.Set; + +/** + * Handles configuration and behavior specific to a Mixin type + */ +public interface MixinType { + PropertyContainerTemplate getConfigurationTemplate(); + + HandlerSignature signature(); + + default Set getFlags() { + return EnumSet.noneOf(MixinFlag.class); + } + + @Nullable + default RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return null; + } + + default boolean canInject(MixinContext context, Configuration config) { + return true; + } + + TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers); + + TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe); +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinTypes.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinTypes.java similarity index 72% rename from core/src/main/java/org/sinytra/adapter/patch/mixin/MixinTypes.java rename to core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinTypes.java index 840f9fa6..1c5c7060 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinTypes.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/MixinTypes.java @@ -1,9 +1,11 @@ -package org.sinytra.adapter.patch.mixin; +package org.sinytra.adapter.pipeline.injector; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.env.util.MixinAnnotations; +import org.sinytra.adapter.model.constant.MixinAnnotations; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.Redirect; @@ -14,25 +16,31 @@ public class MixinTypes { private static final Map MIXIN_TYPES = new HashMap<>(); // TODO: - // MixinConstants.MODIFY_ARGS, MixinConstants.MODIFY_CONST, MixinConstants.WRAP_WITH_CONDITION, // MixinConstants.OVERWRITE (dont forget special modifyTarget handling) // MixinConstants.ACCESSOR public static final MixinType INJECT = new InjectMixin(); public static final MixinType MODIFY_VAR = new ModifyVariableMixin(); public static final MixinType MODIFY_ARG = new ModifyArgMixin(); + public static final MixinType MODIFY_ARGS = new ModifyArgsMixin(); public static final MixinType MODIFY_RET = new ModifyReturnValueMixin(); public static final MixinType REDIRECT = new RedirectMixin(); public static final MixinType WRAP_OP = new WrapOperationMixin(); public static final MixinType MODIFY_EXPR_VAL = new ModifyExpressionValueMixin(); + public static final MixinType MODIFY_CONST = new ModifyConstantMixin(); + public static final MixinType WRAP_WITH_CONDITION = new WrapWithConditionMixin(); static { registerMixinType(Inject.class, INJECT); registerMixinType(ModifyVariable.class, MODIFY_VAR); registerMixinType(ModifyArg.class, MODIFY_ARG); + registerMixinType(ModifyArgs.class, MODIFY_ARGS); + registerMixinType(ModifyConstant.class, MODIFY_CONST); registerMixinType(MixinAnnotations.MODIFY_RETURN_VAL_INTERNAL_NAME, MODIFY_RET); registerMixinType(Redirect.class, REDIRECT); registerMixinType(MixinAnnotations.WRAP_OPERATION_INTERNAL_NAME, WRAP_OP); registerMixinType(MixinAnnotations.MODIFY_EXPR_VAL_INTERNAL_NAME, MODIFY_EXPR_VAL); + registerMixinType(MixinAnnotations.WRAP_WITH_CONDITION_INTERNAL_NAME, WRAP_WITH_CONDITION); + registerMixinType(MixinAnnotations.WRAP_WITH_CONDITION_V2_INTERNAL_NAME, WRAP_WITH_CONDITION); } @Nullable diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyArgMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyArgMixin.java similarity index 54% rename from core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyArgMixin.java rename to core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyArgMixin.java index 850f4cd2..373b97ca 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyArgMixin.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyArgMixin.java @@ -1,30 +1,31 @@ -package org.sinytra.adapter.patch.mixin; +package org.sinytra.adapter.pipeline.injector; +import org.sinytra.adapter.model.RelocationPlan; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Type; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameter; -import org.sinytra.adapter.env.param.Parameters; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.ConfigurationTemplates; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.PropertyContainerTemplate; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.processor.Processors; -import org.sinytra.adapter.patch.resolver.Resolvers; -import org.sinytra.adapter.patch.resolver.injection.ArbitraryInjectionPointSubResolver; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.types.TypeAdapter; -import org.sinytra.adapter.util.MethodQualifier; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolvers; import java.util.List; -import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.SINGLE_ANY; -import static org.sinytra.adapter.patch.config.key.MixinKeys.INDEX; +import static org.sinytra.adapter.model.config.key.MixinKeys.INDEX; +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.SINGLE_ANY; public class ModifyArgMixin extends AbstractMixinType { private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() @@ -32,16 +33,37 @@ public class ModifyArgMixin extends AbstractMixinType { .pluralKeys(MixinKeys.TARGET_METHOD) .build(); + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.CALL_ARG, + slot(SINGLE_ANY, VALUE) + ); + public ModifyArgMixin() { super(TEMPLATE); } @Override - public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { - resolvers.getOrThrow(InjectionPointResolver.class) - .addSubResolver(new ArbitraryInjectionPointSubResolver()); + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + AtData at = config.getAtData(); + String desc = at != null ? at.getTargetQualifier().map(MethodQualifier::desc).orElse(null) : null; + if (desc == null) { + return null; + } + + List callArgs = Parameters.getParameterTypes(desc); + List handlerArgs = Parameters.getParameterTypes(context.methodNode().desc); + boolean allArgs = callArgs.size() > 1 && handlerArgs.size() >= callArgs.size() && handlerArgs.subList(0, callArgs.size()).equals(callArgs); + return allArgs ? null : RelocationPlan.Passthrough.KEEP_VALUE; + } - clean.setParameters(MethodParameters.create(context.methodNode(), List.of(SINGLE_ANY))); + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + clean.setParameters(SIGNATURE.parse(context.methodNode())); return TxResult.SUCCESS; } @@ -61,10 +83,7 @@ public TxResult postProcess(MixinContext context, Configuration clean, MutableCo Type type = findArgType(context, recipe.clean().getAtData(), dirty.getAtData(), clean, dirty); if (type != null) { - MethodParameters parameters = MethodParameters.create(context.methodNode(), List.of(SINGLE_ANY)); - parameters.set(SINGLE_ANY, Parameter.simple(type)); - dirty.setParameters(parameters); - dirty.setReturnType(type); + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, type); } return TxResult.SUCCESS; diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyArgsMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyArgsMixin.java new file mode 100644 index 00000000..4e973f6b --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyArgsMixin.java @@ -0,0 +1,73 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolvers; + +import java.util.Objects; + +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.CAPTURED_TARGET_ARGS; +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.KEEP; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.SINGLE_ANY; + +public class ModifyArgsMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() + .pluralKeys(MixinKeys.TARGET_METHOD) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.VOID, + slot(SINGLE_ANY, KEEP), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS) + ); + + public ModifyArgsMixin() { + super(TEMPLATE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return RelocationPlan.Passthrough.SKIP; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + clean.setParameters(SIGNATURE.parse(context.methodNode())); + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + dirty.inheritProperyIfAbsent(MixinKeys.ORDINAL); + AtData cleanAt = clean.getAtData(); + AtData dirtyAt = dirty.getAtData(); + if (cleanAt == null || dirtyAt == null) { + return TxResult.FAIL; + } + String cleanDesc = cleanAt.getTargetQualifier().map(MethodQualifier::desc).orElse(null); + String dirtyDesc = dirtyAt.getTargetQualifier().map(MethodQualifier::desc).orElse(null); + if (cleanDesc == null || !Objects.equals(cleanDesc, dirtyDesc)) { + return TxResult.FAIL; + } + dirty.inheritSignature(); + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyConstantMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyConstantMixin.java new file mode 100644 index 00000000..058a07e5 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyConstantMixin.java @@ -0,0 +1,64 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolvers; + +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.CAPTURED_TARGET_ARGS; +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.VALUE; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.SINGLE_ANY; + +public class ModifyConstantMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_BASE.extend() + .require(MixinKeys.TARGET_CONSTANT) + .keys(MixinKeys.SLICES) + .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_CONSTANT) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.CONSTANT, + slot(SINGLE_ANY, VALUE), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS) + ); + + public ModifyConstantMixin() { + super(TEMPLATE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return RelocationPlan.Passthrough.KEEP_VALUE; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + clean.setParameters(SIGNATURE.parse(context.methodNode())); + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + dirty.inheritProperyIfAbsent(MixinKeys.TARGET_CONSTANT); + if (dirty.getTargetMethod() == null || !dirty.hasProperty(MixinKeys.TARGET_CONSTANT)) { + return TxResult.FAIL; + } + dirty.inheritSignature(); + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyExpressionValueMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyExpressionValueMixin.java new file mode 100644 index 00000000..19835dd9 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyExpressionValueMixin.java @@ -0,0 +1,85 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.InjectionPointResolver; +import org.sinytra.adapter.pipeline.resolver.Resolvers; +import org.sinytra.adapter.pipeline.resolver.rewrite.InstanceofToExpression; +import org.sinytra.adapter.pipeline.resolver.rewrite.RewriteResolver; + +import java.util.List; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_INVOKE; +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.LOCALS; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.SINGLE_ANY; + +public class ModifyExpressionValueMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() + .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.CALL_RETURN, + slot(SINGLE_ANY, VALUE), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS), + slot(LOCALS, KEEP) + ); + + public ModifyExpressionValueMixin() { + super(TEMPLATE, MixinFlag.RETURN_TYPE_SENSITIVE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return RelocationPlan.Passthrough.KEEP_VALUE; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + resolvers + .addBefore(InjectionPointResolver.class, new RewriteResolver(List.of(new InstanceofToExpression()))); + + clean.setParameters(SIGNATURE.parse(context.methodNode())); + + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + if (dirty.getTargetMethod() == null || dirty.getAtData() == null) { + return TxResult.FAIL; + } + // Best effort for non-INVOKE + if (!dirty.getAtData().getValue().equals(AT_VAL_INVOKE)) { + dirty.inheritSignature(); + return TxResult.SUCCESS; + } + + MethodQualifier targetDesc = dirty.getAtData().getTargetQualifier().orElse(null); + if (targetDesc == null || targetDesc.desc() == null) { + // Best effort + dirty.inheritSignature(); + return TxResult.SUCCESS; + } + + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, null); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyReturnValueMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyReturnValueMixin.java new file mode 100644 index 00000000..b66513c4 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyReturnValueMixin.java @@ -0,0 +1,81 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolvers; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_RETURN; +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.*; + +public class ModifyReturnValueMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() + .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.TARGET_RETURN, + slot(SINGLE_ANY, VALUE), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS), + slot(LOCALS, KEEP) + ); + + public ModifyReturnValueMixin() { + super(TEMPLATE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return RelocationPlan.Passthrough.KEEP_VALUE; + } + + @Override + public boolean canInject(MixinContext context, Configuration config) { + AtData at = config.getAtData(); + MethodQualifier target = config.getTargetMethod(); + if (at == null || !AT_VAL_RETURN.equals(at.getValue()) || at.getProperty(AtData.Keys.ORDINAL).isEmpty() || target == null) { + return true; + } + + Targets targets = context.targets(target); + return targets == null || context.dataFlow(targets.clean()).findReturns().size() == context.dataFlow(targets.dirty()).findReturns().size(); + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + clean.setParameters(SIGNATURE.parse(context.methodNode())); + + return TxResult.SUCCESS; + } + + // TODO Cleanup parameter processing in mixin types + // 1. Method params vs Captured params + reconstruction + // 2. Locals + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + MethodQualifier target = dirty.getTargetMethod(); + if (target == null || target.desc() == null) { + return TxResult.FAIL; + } + + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, null); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyVariableMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyVariableMixin.java new file mode 100644 index 00000000..fa7fea0a --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/ModifyVariableMixin.java @@ -0,0 +1,132 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.InjectionPointResolver; +import org.sinytra.adapter.pipeline.resolver.LocalResolver; +import org.sinytra.adapter.pipeline.resolver.Resolvers; +import org.sinytra.adapter.pipeline.resolver.rewrite.RewriteResolver; +import org.sinytra.adapter.pipeline.resolver.rewrite.ShiftedVariableToExpression; + +import java.util.List; + +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.LOCALS; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.SINGLE_ANY; + +public class ModifyVariableMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() + .keys(MixinKeys.ARGS_ONLY, MixinKeys.ORDINAL, MixinKeys.INDEX, MixinKeys.SLICE) + .pluralKeys(MixinKeys.TARGET_METHOD) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.VARIABLE, + slot(SINGLE_ANY, VALUE), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS), + slot(LOCALS, KEEP) + ); + + public ModifyVariableMixin() { + super(TEMPLATE, MixinFlag.TARGETS_VARIABLE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return RelocationPlan.Passthrough.KEEP_VALUE; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + resolvers + .addBefore(InjectionPointResolver.class, new RewriteResolver(List.of(new ShiftedVariableToExpression()))) + .addAfter(InjectionPointResolver.class, new LocalResolver()); + + clean.setParameters(SIGNATURE.parse(context.methodNode())); + + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + if (dirty.getTargetMethod() == null || dirty.getTargetMethod().desc() == null) { + return TxResult.FAIL; + } + + dirty.inheritProperyIfAbsent(MixinKeys.SLICE); + + boolean argsOnly = clean.getProperty(MixinKeys.ARGS_ONLY).orElse(false); + String cleanDesc = clean.getTargetMethod().desc(); + boolean targetChanged = cleanDesc != null && !dirty.getTargetMethod().desc().equals(cleanDesc); + Type cleanVarType = clean.getParameters().getTypes(SINGLE_ANY).getFirst(); + if (argsOnly && targetChanged) { + List cleanTargetMethodParams = Parameters.getParameterTypes(cleanDesc); + int cleanIndex = cleanTargetMethodParams.indexOf(cleanVarType); + + List dirtyTargetMethodParams = Parameters.getParameterTypes(dirty.getTargetMethod().desc()); + // The modified argument can't be located in the new target method + if (cleanIndex == -1 || cleanIndex >= dirtyTargetMethodParams.size()) { + return TxResult.FAIL; + } + Type dirtyVarType = dirtyTargetMethodParams.get(cleanIndex); + + TypeAdapter adapter = context.getTypeAdapter(cleanVarType, dirtyVarType); + if (adapter != null) { + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, dirtyVarType); + } + } + + if (targetChanged && !clean.getParameters().getTypes(CAPTURED_PARAMS).isEmpty()) { + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, cleanVarType); + } + + dirty.inheritSignature(); + + return TxResult.SUCCESS; + } + + @Override + public boolean canInject(MixinContext context, Configuration config) { + // Ensure variable at index has the correct type + if (config.hasProperty(MixinKeys.INDEX)) { + TargetPair targetPair = context.dirtyMethods().resolve(config.getTargetMethod()); + if (targetPair == null) return false; + MethodNode target = targetPair.methodNode(); + + Integer index = config.getProperty(MixinKeys.INDEX).orElseThrow(); + LocalVariableLookup lookup = LocalVariableLookup.of(target); + if (lookup == null) return false; + + LocalVariableNode node = lookup.getByIndexOrNull(index); + if (node == null) return false; + + Type expectedType = config.getReturnType(); + Type actualType = Type.getType(node.desc); + return expectedType.equals(actualType); + } + return true; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/RedirectMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/RedirectMixin.java new file mode 100644 index 00000000..e933cec0 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/RedirectMixin.java @@ -0,0 +1,115 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.MixinAnnotationConstants; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.MethodParameters.ParamGroup; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.model.param.RedirectedInsn; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.InjectionPointResolver; +import org.sinytra.adapter.pipeline.resolver.Resolvers; +import org.sinytra.adapter.pipeline.resolver.rewrite.InstanceofMakesRedirectRedundant; +import org.sinytra.adapter.pipeline.resolver.rewrite.RewriteResolver; + +import java.util.List; + +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.*; + +public class RedirectMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() + .pluralKeys(MixinKeys.TARGET_METHOD) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.CALL_RETURN, + slot(METHOD_PARAMS, CALL_OPERANDS), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS), + slot(LOCALS, KEEP) + ); + + public RedirectMixin() { + super(TEMPLATE, MixinFlag.AT_TARGET_SENSITIVE, MixinFlag.ACCEPTS_INSTANCE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + resolvers. + addBefore(InjectionPointResolver.class, new RewriteResolver(List.of(new InstanceofMakesRedirectRedundant()))); + + if (clean.getAtData() == null) + return TxResult.FAIL; + + List callTypes; + if (MixinAnnotationConstants.AT_VAL_INVOKE.equals(clean.getAtData().getValue())) { + MethodQualifier targetDesc = clean.getAtData().getTargetQualifier().orElse(null); + if (targetDesc == null || targetDesc.desc() == null) + return TxResult.FAIL; + + List methodParams = Parameters.getParameterTypes(context.methodNode().desc); + boolean isStatic = isStaticRedirect(context, targetDesc, methodParams, Parameters.getParameterTypes(targetDesc.desc())); + if (!isStatic && methodParams.isEmpty()) + return TxResult.FAIL; + callTypes = Parameters.getCallOperandTypes(isStatic ? null : methodParams.getFirst(), targetDesc.desc()); + } else { + TargetPair cleanTarget = clean.getTargetMethod() != null ? context.cleanMethods().resolve(clean.getTargetMethod()) : null; + AbstractInsnNode insn = context.injections().resolveFirst(cleanTarget); + RedirectedInsn redirected = insn != null ? RedirectedInsn.of(insn) : null; + if (redirected == null) + return TxResult.FAIL; + callTypes = redirected.operands(); + } + + MethodParameters capturedMethodParams = MethodParameters.create( + Parameters.parse(context.methodNode(), callTypes.size(), -1), + List.of(ParamGroup.CAPTURED_PARAMS, ParamGroup.LOCALS) + ); + + MethodParameters params = MethodParameters.builder() + .putTypes(METHOD_PARAMS, callTypes) + .putTypes(CAPTURED_PARAMS, capturedMethodParams.getTypes(CAPTURED_PARAMS)) + .putTypes(LOCALS, capturedMethodParams.getTypes(LOCALS)) + .putRemaining(capturedMethodParams) + .build(); + + clean.setParameters(params); + + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + if (dirty.getTargetMethod() == null || dirty.getAtData() == null) + return TxResult.FAIL; + + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, null); + } + + private boolean isStaticRedirect(MixinContext context, MethodQualifier targetDesc, List methodParams, List callTypes) { + TargetPair cleanTarget = context.cleanMethods().resolve(targetDesc); + if (cleanTarget != null) { + return MethodAnalyzer.isStatic(cleanTarget.methodNode()); + } + return methodParams.size() >= callTypes.size() && methodParams.subList(0, callTypes.size()).equals(callTypes); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/WrapOperationMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/WrapOperationMixin.java new file mode 100644 index 00000000..7eeb8370 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/WrapOperationMixin.java @@ -0,0 +1,75 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.TypeConstants; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ParamSource; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.InjectionPointResolver; +import org.sinytra.adapter.pipeline.resolver.Resolvers; +import org.sinytra.adapter.pipeline.resolver.rewrite.InstanceofToConstant; +import org.sinytra.adapter.pipeline.resolver.rewrite.RewriteResolver; + +import java.util.List; + + +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.*; + +public class WrapOperationMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_BASE.extend() + .requireOne(MixinKeys.TARGET_AT, MixinKeys.TARGET_CONSTANT) + .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT, MixinKeys.TARGET_CONSTANT) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.CALL_RETURN, + slot(METHOD_PARAMS, CALL_OPERANDS), + slot(MethodParameters.ParamGroup.OPERATION, ParamSource.OPERATION), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS), + slot(LOCALS, KEEP) + ); + + public WrapOperationMixin() { + super(TEMPLATE, MixinFlag.AT_TARGET_SENSITIVE, MixinFlag.ACCEPTS_INSTANCE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + boolean hasOperation = Parameters.getParameterTypes(context.methodNode().desc).contains(TypeConstants.OPERATION_TYPE); + return hasOperation ? RelocationPlan.Passthrough.CALL_ORIGINAL : null; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + resolvers.addBefore(InjectionPointResolver.class, new RewriteResolver(List.of(new InstanceofToConstant()))); + + clean.setParameters(SIGNATURE.parse(context.methodNode())); + + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + if (dirty.getTargetMethod() == null) return TxResult.FAIL; + if (dirty.getAtData() == null) return TxResult.PASS; + + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, null); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/injector/WrapWithConditionMixin.java b/core/src/main/java/org/sinytra/adapter/pipeline/injector/WrapWithConditionMixin.java new file mode 100644 index 00000000..6119905b --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/injector/WrapWithConditionMixin.java @@ -0,0 +1,80 @@ +package org.sinytra.adapter.pipeline.injector; + +import org.sinytra.adapter.model.RelocationPlan; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.ConfigurationTemplates; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyContainerTemplate; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ValueSource; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.model.param.RedirectedInsn; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolvers; + +import java.util.List; + +import static org.sinytra.adapter.model.param.HandlerSignature.ParamSource.*; +import static org.sinytra.adapter.model.param.HandlerSignature.slot; +import static org.sinytra.adapter.model.param.MethodParameters.ParamGroup.*; + +public class WrapWithConditionMixin extends AbstractMixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_AT.extend() + .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT) + .build(); + + private static final HandlerSignature SIGNATURE = HandlerSignature.of( + ValueSource.CONDITION, + slot(METHOD_PARAMS, CALL_OPERANDS), + slot(CAPTURED_PARAMS, CAPTURED_TARGET_ARGS), + slot(LOCALS, KEEP) + ); + + public WrapWithConditionMixin() { + super(TEMPLATE, MixinFlag.AT_TARGET_SENSITIVE, MixinFlag.ACCEPTS_INSTANCE); + } + + @Override + public HandlerSignature signature() { + return SIGNATURE; + } + + @Override + public RelocationPlan.Passthrough passthrough(MixinContext context, Configuration config) { + return RelocationPlan.Passthrough.PROCEED; + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers) { + TargetPair cleanTarget = clean.getTargetMethod() != null ? context.cleanMethods().resolve(clean.getTargetMethod()) : null; + AbstractInsnNode insn = context.injections().resolveFirst(cleanTarget); + RedirectedInsn wrapped = insn != null ? RedirectedInsn.of(insn) : null; + List params = Parameters.parse(context.methodNode()); + if (wrapped == null || wrapped.operands().size() > params.size()) { + return TxResult.FAIL; + } + int operands = wrapped.operands().size(); + MethodParameters rest = MethodParameters.create(params.subList(operands, params.size()), List.of(CAPTURED_PARAMS, LOCALS)); + clean.setParameters(MethodParameters.builder() + .put(METHOD_PARAMS, params.subList(0, operands)) + .put(CAPTURED_PARAMS, rest.get(CAPTURED_PARAMS)) + .put(LOCALS, rest.get(LOCALS)) + .putRemaining(rest) + .build()); + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + if (dirty.getTargetMethod() == null || dirty.getAtData() == null) return TxResult.FAIL; + + return HandlerSignatures.apply(context, SIGNATURE, clean, dirty, recipe, null); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/Candidate.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Candidate.java new file mode 100644 index 00000000..29331abb --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Candidate.java @@ -0,0 +1,13 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.model.Relocation; +import org.sinytra.adapter.model.TargetPair; + +public record Candidate(TargetPair target, AbstractInsnNode insn, MethodAlignment.Confidence confidence, @Nullable Relocation relocation) { + public Candidate(TargetPair target, AbstractInsnNode insn, MethodAlignment.Confidence confidence) { + this(target, insn, confidence, null); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/Describer.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Describer.java new file mode 100644 index 00000000..4626a236 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Describer.java @@ -0,0 +1,271 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.sinytra.adapter.analysis.InsnTraversal; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.ann.ConstantData; +import org.sinytra.adapter.model.ann.SliceData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.key.IntentKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.param.RedirectedInsn; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.injector.MixinFlag; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.MixinBridge; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_INVOKE; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_INVOKE_ASSIGN; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_LOAD; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_NEW; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_STORE; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_ARGS_ONLY; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_INDEX; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_ORDINAL; + +public final class Describer { + @Nullable + public static Configuration describe(MixinContext context, Recipe recipe, Candidate candidate) { + AtData cleanAt = recipe.clean().getAtData(); + ConstantData cleanConstant = recipe.clean().getProperty(MixinKeys.TARGET_CONSTANT).orElse(null); + TargetPair origin = recipe.getDirtyTarget(); + boolean otherMethod = origin == null || !candidate.target().isSameMethod(origin); + Function query = otherMethod ? at -> InjectionQuery.of(at).ignoringSlice() : InjectionQuery::of; + MutableConfiguration config = MutableConfiguration.create(); + AtData described = null; + if (cleanAt != null) { + described = describeAt(context, cleanAt, candidate, query); + if (described == null) { + return null; + } + config.setAtData(described); + } else if (cleanConstant != null) { + ConstantData constant = describeConstant(context, cleanConstant, candidate, otherMethod); + if (constant == null) { + return null; + } + config.setProperty(MixinKeys.TARGET_CONSTANT, constant); + } else { + return null; + } + if (otherMethod && recipe.clean().getProperty(MixinKeys.SLICES).map(slices -> !slices.isEmpty()).orElse(false)) { + config.setProperty(MixinKeys.SLICES, List.of()); + } + if (otherMethod && recipe.clean().hasProperty(MixinKeys.SLICE)) { + config.setProperty(MixinKeys.SLICE, SliceData.empty()); + } + + if (described != null && isLocalAt(context, described) && candidate.insn() instanceof VarInsnNode var) { + AbstractInsnNode position = OpcodeUtil.isStoreOpcode(var.getOpcode()) ? var.getNext() : var; + MutableConfiguration local = describeLocal(context, recipe, candidate.target(), position, var.var); + if (local == null) { + return null; + } + config.mergeFrom(local); + } + + if (otherMethod) { + config.setTargetMethod(candidate.target().methodNode()); + String targetClass = candidate.target().classNode().name; + if (!targetClass.equals(recipe.dirty().getTargetClass())) { + config.setTargetClass(targetClass); + if (candidate.relocation() != null) { + config.setProperty(IntentKeys.RELOCATION, candidate.relocation()); + } + } + } + return config; + } + + @Nullable + private static ConstantData describeConstant(MixinContext context, ConstantData cleanConstant, Candidate candidate, boolean otherMethod) { + ConstantData constant = otherMethod ? cleanConstant.withProperty(ConstantData.Keys.SLICE, null) : cleanConstant; + InjectionQuery query = InjectionQuery.of(constant.withOrdinal(null)); + List selected = context.injections().find(candidate.target(), otherMethod ? query.ignoringSlice() : query); + int ordinal = selected.indexOf(candidate.insn()); + if (ordinal == -1) { + return null; + } + return constant.getOrdinal().isPresent() ? constant.withOrdinal(ordinal) : constant; + } + + @Nullable + public static AtData describeAt(MixinContext context, AtData cleanAt, Candidate candidate, Function query) { + if (isLocalAt(context, cleanAt)) { + if (!(candidate.insn() instanceof VarInsnNode var)) { + return null; + } + List uses = new ArrayList<>(); + for (AbstractInsnNode insn : candidate.target().methodNode().instructions) { + if (insn instanceof VarInsnNode use && use.getOpcode() == var.getOpcode() && use.var == var.var) { + uses.add(use); + } + } + return cleanAt.getOrdinal().isPresent() ? cleanAt.withOrdinal(uses.indexOf(var)) : cleanAt; + } + + boolean changesKind = candidate.insn() instanceof MethodInsnNode && !AT_VAL_INVOKE.equals(cleanAt.getValue()) && !AT_VAL_INVOKE_ASSIGN.equals(cleanAt.getValue()); + AtData at; + if (changesKind) { + at = AtData.create(AT_VAL_INVOKE, (MethodInsnNode) candidate.insn()); + } else if (candidate.insn() instanceof TypeInsnNode creation && AT_VAL_NEW.equals(cleanAt.getValue()) && cleanAt.getTarget().isPresent()) { + at = retargetCreation(cleanAt, creation); + if (at == null) { + return null; + } + } else { + at = candidate.insn() instanceof MethodInsnNode call && cleanAt.getTarget().isPresent() ? cleanAt.withTarget(call) : cleanAt; + } + List selected = context.injections().find(candidate.target(), query.apply(at.withOrdinal(null))); + int ordinal = selected.indexOf(candidate.insn()); + if (ordinal == -1) { + return null; + } + boolean keepsOrdinal = changesKind ? selected.size() > 1 : cleanAt.getOrdinal().isPresent(); + return keepsOrdinal ? at.withOrdinal(ordinal) : at.withOrdinal(null); + } + + @Nullable + public static MutableConfiguration describeLocal(MixinContext context, Recipe recipe, TargetPair target, AbstractInsnNode insn, int slot) { + Configuration clean = recipe.clean(); + Type type = Type.getReturnType(context.methodNode().desc); + MutableConfiguration config = MutableConfiguration.create(); + + boolean argsOnly = clean.getProperty(MixinKeys.ARGS_ONLY).orElse(false); + if (argsOnly && !isParameter(target.methodNode(), slot)) { + argsOnly = false; + config.setProperty(MixinKeys.ARGS_ONLY, false); + } + + if (clean.hasProperty(MixinKeys.INDEX)) { + return config.setProperty(MixinKeys.INDEX, slot); + } + if (clean.hasProperty(MixinKeys.ORDINAL)) { + int ordinal = argsOnly ? getParameterOrdinal(target.methodNode(), slot, type) : context.locals().resolveLocalOrdinal(target, insn, type, slot); + return ordinal != -1 ? config.setProperty(MixinKeys.ORDINAL, ordinal) : null; + } + return config; + } + + @Nullable + private static AtData retargetCreation(AtData cleanAt, TypeInsnNode creation) { + String target = cleanAt.getTargetOrThrow(); + boolean descriptor = target.startsWith("("); + if (!descriptor && !target.contains("")) { + return cleanAt; + } + MethodInsnNode constructor = RedirectedInsn.findConstructorCall(creation); + if (constructor == null) { + return null; + } + Type owner = Type.getObjectType(creation.desc); + return cleanAt.withTarget(descriptor + ? Type.getMethodDescriptor(owner, Type.getArgumentTypes(constructor.desc)) + : owner.getDescriptor() + "" + constructor.desc); + } + + public static List findSelectable(MixinContext context, TargetPair target, AtData at, Function query) { + if (!isLocalAt(context, at)) { + return context.injections().find(target, query.apply(at.withOrdinal(null))); + } + int opcode = Type.getReturnType(context.methodNode().desc).getOpcode(AT_VAL_STORE.equals(at.getValue()) ? Opcodes.ISTORE : Opcodes.ILOAD); + List uses = new ArrayList<>(); + for (AbstractInsnNode insn : target.methodNode().instructions) { + if (insn.getOpcode() == opcode) { + uses.add(insn); + } + } + return uses; + } + + public static AbstractInsnNode toLocatable(MixinContext context, AtData at, AbstractInsnNode selected) { + if (isLocalAt(context, at) && AT_VAL_STORE.equals(at.getValue()) && !OpcodeUtil.isStoreOpcode(selected.getOpcode())) { + AbstractInsnNode store = InsnTraversal.find(selected, InsnTraversal.BACKWARD, insn -> insn.getOpcode() >= 0); + if (store != null && OpcodeUtil.isStoreOpcode(store.getOpcode())) { + return store; + } + } + return selected; + } + + public static boolean isSame(Configuration a, Configuration b) { + return Objects.equals(a.getAtData(), b.getAtData()) + && Objects.equals(a.getTargetMethod(), b.getTargetMethod()) + && Objects.equals(a.getTargetClass(), b.getTargetClass()) + && Objects.equals(a.getProperty(MixinKeys.ORDINAL), b.getProperty(MixinKeys.ORDINAL)) + && Objects.equals(a.getProperty(MixinKeys.INDEX), b.getProperty(MixinKeys.INDEX)); + } + + public static boolean isLocalAt(MixinContext context, AtData at) { + return context.hasFlag(MixinFlag.TARGETS_VARIABLE) && (AT_VAL_STORE.equals(at.getValue()) || AT_VAL_LOAD.equals(at.getValue())); + } + + @Nullable + public static AnnotationNode describeLocalSelector(MixinContext context, MixinBridge.Session session, AnnotationNode selector, TargetPair target, + AbstractInsnNode insn, int slot, Type type) { + boolean argsOnly = Boolean.TRUE.equals(AdapterUtil.getAnnotationValue(selector, PROPERTY_ARGS_ONLY)) && isParameter(target.methodNode(), slot); + AnnotationNode adjusted = new AnnotationNode(selector.desc); + if (argsOnly) { + adjusted.visit(PROPERTY_ARGS_ONLY, true); + } + if (AdapterUtil.getAnnotationValue(selector, PROPERTY_INDEX) != null) { + adjusted.visit(PROPERTY_INDEX, slot); + return session.findLocal(adjusted, type, insn) == slot ? adjusted : null; + } + if (AdapterUtil.getAnnotationValue(selector, PROPERTY_ORDINAL) == null && session.findLocal(adjusted, type, insn) == slot) { + return adjusted; + } + int ordinal = argsOnly + ? getParameterOrdinal(target.methodNode(), slot, type) + : context.locals().resolveLocalOrdinal(target, insn, type, slot); + if (ordinal != -1) { + adjusted.visit(PROPERTY_ORDINAL, ordinal); + if (session.findLocal(adjusted, type, insn) == slot) { + return adjusted; + } + } + return null; + } + + public static boolean isParameter(MethodNode method, int slot) { + return slot < (MethodAnalyzer.isStatic(method) ? 0 : 1) + Arrays.stream(Type.getArgumentTypes(method.desc)).mapToInt(Type::getSize).sum(); + } + + public static int getParameterOrdinal(MethodNode method, int slot, Type type) { + int current = MethodAnalyzer.isStatic(method) ? 0 : 1; + int ordinal = 0; + for (Type arg : Type.getArgumentTypes(method.desc)) { + if (current == slot) { + return arg.equals(type) ? ordinal : -1; + } + if (arg.equals(type)) { + ordinal++; + } + current += arg.getSize(); + } + return -1; + } + + private Describer() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/LocalLocator.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/LocalLocator.java new file mode 100644 index 00000000..b53c803a --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/LocalLocator.java @@ -0,0 +1,52 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LocalVariableNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.analysis.locals.LocalAlignment; +import org.sinytra.adapter.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.analysis.locals.LocalsAnalyzer; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +public final class LocalLocator { + public static int resolve(MixinContext context, Recipe recipe, TargetPair cleanTarget, int cleanSlot, AbstractInsnNode cleanInsn, TargetPair dirtyTarget) { + return resolve(context, cleanTarget, recipe.getNewCleanTarget(), cleanSlot, cleanInsn, dirtyTarget); + } + + public static int resolve(MixinContext context, TargetPair cleanTarget, @Nullable TargetPair origin, int cleanSlot, AbstractInsnNode cleanInsn, TargetPair dirtyTarget) { + int originSlot = origin != null + ? LocalAlignment.of(MethodAlignment.align(cleanTarget.methodNode(), origin.methodNode())).resolve(cleanSlot, cleanInsn) + : -1; + if (originSlot == -1 && origin != null && origin.methodNode().desc.equals(cleanTarget.methodNode().desc) + && Describer.isParameter(cleanTarget.methodNode(), cleanSlot)) { + originSlot = cleanSlot; + } + if (origin != null && origin.isSameMethod(dirtyTarget)) { + return originSlot; + } + if (originSlot != -1) { + int passed = PassedLocals.resolve(context, origin, originSlot, dirtyTarget); + if (passed != -1) { + return passed; + } + } + + LocalVariableLookup cleanLocals = LocalVariableLookup.of(cleanTarget.methodNode()); + LocalVariableNode cleanLocal = cleanLocals != null ? cleanLocals.getInScopeOrNull(cleanSlot, cleanInsn) : null; + if (cleanLocal == null) { + return -1; + } + InsnList initializer = LocalsAnalyzer.findInitializerInsns(cleanTarget.methodNode(), cleanSlot); + return LocalsAnalyzer.findLocalWithInitializer(dirtyTarget.methodNode(), cleanLocal.desc, initializer) + .map(local -> local.index) + .orElse(-1); + } + + + private LocalLocator() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/Locator.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Locator.java new file mode 100644 index 00000000..2012aab0 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Locator.java @@ -0,0 +1,11 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; + +public interface Locator { + List locate(MixinContext context, Recipe recipe, Scope scope, AbstractInsnNode cleanInsn); +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/LocatorChain.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/LocatorChain.java new file mode 100644 index 00000000..99b2ce90 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/LocatorChain.java @@ -0,0 +1,111 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.*; +import java.util.function.Function; + +public final class LocatorChain { + private final List locators; + private final ScopeSearch.Reach reach; + + public LocatorChain(List locators, ScopeSearch.Reach reach) { + this.locators = locators; + this.reach = reach; + } + + @Nullable + public Configuration resolve(MixinContext context, Recipe recipe) { + TargetPair cleanTarget = recipe.getCleanTarget(); + if (cleanTarget == null) { + return null; + } + AtData cleanAt = recipe.clean().getAtData(); + List cleanInsns = context.injections().find(cleanTarget).stream() + .map(insn -> cleanAt != null ? Describer.toLocatable(context, cleanAt, insn) : insn) + .toList(); + ScopeSearch search = ScopeSearch.of(context, recipe, this.reach); + return locateAll(context, recipe, search, cleanInsns, this.locators, candidate -> Describer.describe(context, recipe, candidate)); + } + + @Nullable + public static Configuration locateAll(MixinContext context, Recipe recipe, ScopeSearch search, List cleanInsns, + List locators, Function describer) { + if (cleanInsns.isEmpty()) { + return null; + } + Configuration shared = null; + for (AbstractInsnNode cleanInsn : cleanInsns) { + Configuration config = locate(context, recipe, search, cleanInsn, locators, describer); + if (config == null || shared != null && !Describer.isSame(config, shared)) { + return null; + } + shared = config; + } + return shared; + } + + @Nullable + private static Configuration locate(MixinContext context, Recipe recipe, ScopeSearch search, AbstractInsnNode cleanInsn, List locators, + Function describer) { + for (Locator locator : locators) { + for (int tier = 0; tier < search.tierCount(); tier++) { + Map candidates = new HashMap<>(); + List ordered = new ArrayList<>(); + for (Scope scope : search.tier(tier)) { + for (Candidate candidate : locator.locate(context, recipe, scope, cleanInsn)) { + candidates.put(candidate, scope); + ordered.add(candidate); + } + } + if (ordered.isEmpty()) { + continue; + } + List best = pick(ordered, candidates); + Configuration config = best.size() == 1 ? describer.apply(best.getFirst()) : describeAll(best, describer); + if (config != null) { + Candidate first = best.getFirst(); + context.recordCtxAudit("Located injection point with %s (%s) in %s", locator.getClass().getSimpleName(), + best.size() == 1 ? first.confidence() : best.size() + " candidates with one @At", + candidates.get(first).kind().name().toLowerCase(Locale.ROOT) + " " + first.target().methodNode().name); + } + return config; + } + } + return null; + } + + @Nullable + private static Configuration describeAll(List candidates, Function describer) { + Configuration shared = null; + for (Candidate candidate : candidates) { + Configuration config = describer.apply(candidate); + if (config == null || shared != null && !Describer.isSame(config, shared)) { + return null; + } + shared = config; + } + return shared; + } + + private static List pick(List candidates, Map scopes) { + MethodAlignment.Confidence best = candidates.stream() + .map(Candidate::confidence) + .min(Comparator.naturalOrder()) + .orElseThrow(); + List confident = candidates.stream() + .filter(c -> c.confidence() == best) + .toList(); + double similarity = confident.stream().mapToDouble(c -> scopes.get(c).similarity()).max().orElseThrow(); + return confident.stream() + .filter(c -> scopes.get(c).similarity() == similarity) + .toList(); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/NearestCallLocator.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/NearestCallLocator.java new file mode 100644 index 00000000..942e5a6c --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/NearestCallLocator.java @@ -0,0 +1,44 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.List; + +public final class NearestCallLocator implements Locator { + @Override + public List locate(MixinContext context, Recipe recipe, Scope scope, AbstractInsnNode cleanInsn) { + MethodAlignment alignment = scope.alignment(); + List around = alignment.findDirtyAround(cleanInsn); + AbstractInsnNode start = !around.isEmpty() ? around.getFirst() : findAlignedAfter(alignment, cleanInsn); + + for (AbstractInsnNode insn = start; insn != null; insn = insn.getNext()) { + if (insn instanceof MethodInsnNode call) { + return List.of(scope.candidate(call, MethodAlignment.Confidence.POSITIONAL)); + } + if (insn instanceof JumpInsnNode || OpcodeUtil.isReturnOpcode(insn.getOpcode())) { + break; + } + } + return List.of(); + } + + @Nullable + private static AbstractInsnNode findAlignedAfter(MethodAlignment alignment, AbstractInsnNode cleanInsn) { + for (AbstractInsnNode insn = cleanInsn.getNext(); insn != null; insn = insn.getNext()) { + if (insn.getOpcode() >= 0) { + AbstractInsnNode dirty = alignment.resolve(insn); + if (dirty != null) { + return dirty; + } + } + } + return null; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/OriginLocator.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/OriginLocator.java new file mode 100644 index 00000000..6ffa0ccd --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/OriginLocator.java @@ -0,0 +1,83 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.AdapterUtil; + +import java.util.Comparator; +import java.util.List; + +public final class OriginLocator { + /// @param piece a new method that took over part of a removed target + public record Origin(TargetPair method, boolean matches, MethodAlignment alignment, boolean deprecated, boolean piece) { + public double similarity() { + return this.alignment.similarity(); + } + } + + private static final double MIN_PIECE_COVERAGE = 0.5; + private static final Comparator RANKING = Comparator.comparing(Origin::matches).reversed() + .thenComparing(Comparator.comparingDouble(Origin::similarity).reversed()) + .thenComparing(Origin::deprecated) + .thenComparing(Comparator.comparingInt(o -> Parameters.getParameterTypes(o.method().methodNode().desc).size()).reversed()); + + public static List locate(MixinContext context, TargetPair cleanTarget, String targetClass, MethodQualifier cleanQualifier) { + List candidates; + if (MethodAnalyzer.isLambda(cleanQualifier)) { + ClassNode dirtyClass = context.dirtyLookup().getClass(targetClass).orElse(null); + candidates = dirtyClass == null ? List.of() : dirtyClass.methods.stream() + .filter(method -> MethodAnalyzer.isLambda(method) && method.desc.equals(cleanTarget.methodNode().desc)) + .map(method -> new TargetPair(dirtyClass, method)) + .toList(); + } else { + candidates = context.dirtyMethods().findTargetsByName(cleanQualifier); + if (candidates.isEmpty()) { + return findPieces(context, cleanTarget); + } + } + return candidates.stream() + .map(candidate -> new Origin( + candidate, + context.injections().matches(candidate), + MethodAlignment.align(cleanTarget.methodNode(), candidate.methodNode()), + isDirtyDeprecated(context, candidate.methodNode()), + false + )) + .sorted(RANKING) + .toList(); + } + + private static List findPieces(MixinContext context, TargetPair cleanTarget) { + ClassNode cleanClass = cleanTarget.classNode(); + ClassNode dirtyClass = context.dirtyLookup().getClass(cleanClass.name).orElse(null); + if (dirtyClass == null) { + return List.of(); + } + boolean isStatic = MethodAnalyzer.isStatic(cleanTarget.methodNode()); + return dirtyClass.methods.stream() + .filter(method -> !MethodAnalyzer.isLambda(method) && MethodAnalyzer.isStatic(method) == isStatic + && AdapterUtil.findMethod(cleanClass, method.name, method.desc) == null) + .map(method -> { + TargetPair candidate = new TargetPair(dirtyClass, method); + MethodAlignment alignment = MethodAlignment.align(cleanTarget.methodNode(), method); + return new Origin(candidate, context.injections().matches(candidate), alignment, MethodAnalyzer.isDeprecated(method), true); + }) + .filter(origin -> origin.alignment().dirtyCoverage() >= MIN_PIECE_COVERAGE) + .sorted(RANKING) + .toList(); + } + + private static boolean isDirtyDeprecated(MixinContext context, MethodNode dirty) { + TargetPair clean = context.cleanMethods().resolve(MethodQualifier.create(dirty)); + return clean != null ? MethodAnalyzer.isDirtyDeprecatedMethod(clean.methodNode(), dirty) : MethodAnalyzer.isDeprecated(dirty); + } + + private OriginLocator() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/PassedLocals.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/PassedLocals.java new file mode 100644 index 00000000..69ce201d --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/PassedLocals.java @@ -0,0 +1,100 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.SourceValue; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.analysis.method.MethodAnalyzer.IndyHandle; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.*; + +public final class PassedLocals { + private static final int MAX_DEPTH = 3; + + private record Step(TargetPair method, int slot) { + } + + public static int resolve(MixinContext context, TargetPair caller, int slot, TargetPair callee) { + Set visited = new HashSet<>(); + visited.add(caller.methodNode()); + List frontier = List.of(new Step(caller, slot)); + for (int depth = 0; depth < MAX_DEPTH && !frontier.isEmpty(); depth++) { + List next = new ArrayList<>(); + for (Step step : frontier) { + for (Step passed : findPassed(context, step)) { + if (passed.method().isSameMethod(callee)) { + return passed.slot(); + } + if (visited.add(passed.method().methodNode())) { + next.add(passed); + } + } + } + frontier = next; + } + return -1; + } + + public static Set findPassedSlots(DataFlow flow, AbstractInsnNode insn, boolean hasReceiver, String calleeDesc, Set tracked) { + if (tracked.isEmpty()) { + return Set.of(); + } + List operands = flow.operands(insn); + Type[] args = Type.getArgumentTypes(calleeDesc); + Set passed = new HashSet<>(); + int slot = 0; + for (int i = 0; i < operands.size(); i++) { + int argIndex = hasReceiver ? i - 1 : i; + if (argIndex >= args.length) { + break; + } + if (isTrackedLoad(operands.get(i), tracked)) { + passed.add(slot); + } + slot += argIndex < 0 ? 1 : args[argIndex].getSize(); + } + return passed; + } + + private static List findPassed(MixinContext context, Step step) { + TargetPair from = step.method(); + DataFlow flow = context.dataFlow(from); + Set tracked = Set.of(step.slot()); + List passed = new ArrayList<>(); + for (AbstractInsnNode insn : from.methodNode().instructions) { + if (insn instanceof MethodInsnNode call) { + TargetPair method = context.dirtyMethods().resolveInherited(MethodQualifier.create(call)); + if (method != null) { + findPassedSlots(flow, call, call.getOpcode() != Opcodes.INVOKESTATIC, call.desc, tracked) + .forEach(slot -> passed.add(new Step(method, slot))); + } + } + } + for (IndyHandle lambda : MethodAnalyzer.findOwnIndyHandles(from.classNode(), from.methodNode())) { + Handle handle = lambda.handle(); + TargetPair method = new TargetPair(from.classNode(), lambda.method()); + findPassedSlots(flow, lambda.indy(), handle.getTag() != Opcodes.H_INVOKESTATIC, handle.getDesc(), tracked) + .forEach(slot -> passed.add(new Step(method, slot))); + } + return passed; + } + + private static boolean isTrackedLoad(SourceValue value, Set tracked) { + return value.insns.size() == 1 && value.insns.iterator().next() instanceof VarInsnNode load + && OpcodeUtil.isLoadOpcode(load.getOpcode()) && tracked.contains(load.var); + } + + + private PassedLocals() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/ReplacedCallLocator.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/ReplacedCallLocator.java new file mode 100644 index 00000000..06bf3aed --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/ReplacedCallLocator.java @@ -0,0 +1,82 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.objectweb.asm.tree.analysis.SourceValue; +import org.sinytra.adapter.analysis.InsnTraversal; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.injector.MixinFlag; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.List; + +public final class ReplacedCallLocator implements Locator { + @Override + public List locate(MixinContext context, Recipe recipe, Scope scope, AbstractInsnNode cleanInsn) { + if (!(cleanInsn instanceof MethodInsnNode cleanCall)) { + return List.of(); + } + + Type returnType = Type.getReturnType(cleanCall.desc); + MethodAlignment alignment = scope.alignment(); + List calls = alignment.findDirtyAround(cleanCall).stream() + .filter(insn -> insn instanceof MethodInsnNode call && Type.getReturnType(call.desc).equals(returnType)) + .map(MethodInsnNode.class::cast) + .toList(); + if (calls.size() > 1) { + AbstractInsnNode cleanConsumer = nextReal(cleanCall); + List usedAlike = calls.stream() + .filter(call -> nextReal(call) != null && cleanConsumer != null && nextReal(call).getOpcode() == cleanConsumer.getOpcode()) + .toList(); + if (!usedAlike.isEmpty()) { + calls = usedAlike; + } + } + if (calls.isEmpty() && !context.hasFlag(MixinFlag.RETURN_TYPE_SENSITIVE) && !context.hasFlag(MixinFlag.AT_TARGET_SENSITIVE)) { + MethodInsnNode storing = findStoringCall(context, scope, alignment, cleanCall); + calls = storing != null ? List.of(storing) : List.of(); + } + return calls.stream() + .map(call -> scope.candidate(call, MethodAlignment.Confidence.POSITIONAL)) + .toList(); + } + + /// {@return the dirty call whose value goes into the local the clean call's result was stored into, or null} + @Nullable + private static MethodInsnNode findStoringCall(MixinContext context, Scope scope, MethodAlignment alignment, MethodInsnNode cleanCall) { + AbstractInsnNode cleanStore = skipCasts(nextReal(cleanCall)); + if (cleanStore == null || !OpcodeUtil.isStoreOpcode(cleanStore.getOpcode()) + || !(alignment.resolve(cleanStore) instanceof VarInsnNode dirtyStore) + ) { + return null; + } + + DataFlow flow = context.dataFlow(scope.method()); + AbstractInsnNode producer = dirtyStore; + do { + List operands = flow.operands(producer); + producer = operands.size() == 1 && operands.getFirst().insns.size() == 1 ? operands.getFirst().insns.iterator().next() : null; + } while (producer != null && producer.getOpcode() == Opcodes.CHECKCAST); + return producer instanceof MethodInsnNode call ? call : null; + } + + @Nullable + private static AbstractInsnNode skipCasts(@Nullable AbstractInsnNode insn) { + while (insn != null && insn.getOpcode() == Opcodes.CHECKCAST) { + insn = nextReal(insn); + } + return insn; + } + + @Nullable + private static AbstractInsnNode nextReal(AbstractInsnNode insn) { + return InsnTraversal.find(insn, InsnTraversal.FORWARD, i -> i.getOpcode() >= 0); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/SameMethodLocator.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/SameMethodLocator.java new file mode 100644 index 00000000..495ca3e5 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/SameMethodLocator.java @@ -0,0 +1,99 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.analysis.method.InheritanceHandler; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class SameMethodLocator implements Locator { + @Override + public List locate(MixinContext context, Recipe recipe, Scope scope, AbstractInsnNode cleanInsn) { + return find(context, scope, cleanInsn); + } + + public static List find(MixinContext context, Scope scope, AbstractInsnNode cleanInsn) { + MethodAlignment alignment = scope.alignment(); + List matches = alignment.find(cleanInsn); + if (!matches.isEmpty()) { + return matches.stream() + .map(match -> scope.candidate(match.dirty(), match.confidence())) + .toList(); + } + + if (OpcodeUtil.isReturnOpcode(cleanInsn.getOpcode())) { + AbstractInsnNode dirtyReturn = findReturnReached(alignment, cleanInsn); + return dirtyReturn != null ? List.of(scope.candidate(dirtyReturn, MethodAlignment.Confidence.POSITIONAL)) : List.of(); + } + + if (!(cleanInsn instanceof MethodInsnNode cleanCall)) { + return List.of(); + } + List replacements = alignment.findDirtyAround(cleanInsn).stream() + .filter(insn -> insn instanceof MethodInsnNode dirtyCall && isReplacement(context, cleanCall, dirtyCall)) + .map(insn -> scope.candidate(insn, MethodAlignment.Confidence.POSITIONAL)) + .toList(); + if (!replacements.isEmpty() || scope.kind() == Scope.Kind.SAME) { + return replacements; + } + List anywhere = new ArrayList<>(); + for (AbstractInsnNode insn : scope.method().methodNode().instructions) { + if (insn instanceof MethodInsnNode dirtyCall && isReplacement(context, cleanCall, dirtyCall)) { + anywhere.add(scope.candidate(insn, MethodAlignment.Confidence.POSITIONAL)); + } + } + return anywhere; + } + + @Nullable + private static AbstractInsnNode findReturnReached(MethodAlignment alignment, AbstractInsnNode cleanReturn) { + AbstractInsnNode dirty = null; + for (AbstractInsnNode insn = cleanReturn.getPrevious(); insn != null && dirty == null; insn = insn.getPrevious()) { + if (insn instanceof JumpInsnNode || OpcodeUtil.isReturnOpcode(insn.getOpcode()) || insn.getOpcode() == Opcodes.ATHROW) { + return null; + } + if (insn.getOpcode() >= 0) { + dirty = alignment.resolve(insn); + } + } + if (dirty == null) { + return null; + } + + Set visited = new HashSet<>(); + for (AbstractInsnNode insn = dirty.getNext(); insn != null && visited.add(insn); insn = insn.getNext()) { + if (insn.getOpcode() == cleanReturn.getOpcode()) { + return insn; + } + if (insn instanceof JumpInsnNode jump && jump.getOpcode() == Opcodes.GOTO) { + insn = jump.label; + } else if (insn instanceof JumpInsnNode || OpcodeUtil.isReturnOpcode(insn.getOpcode()) || insn.getOpcode() == Opcodes.ATHROW) { + return null; + } + } + return null; + } + + private static boolean isReplacement(MixinContext context, MethodInsnNode clean, MethodInsnNode dirty) { + return clean.owner.equals(dirty.owner) && clean.name.equals(dirty.name) + || clean.name.equals(dirty.name) && clean.desc.equals(dirty.desc) && isRelated(context, clean.owner, dirty.owner) + || context.getTypeAdapter(Type.getObjectType(dirty.owner), Type.getObjectType(clean.owner)) != null + && Type.getArgumentTypes(clean.desc).length == Type.getArgumentTypes(dirty.desc).length; + } + + private static boolean isRelated(MixinContext context, String a, String b) { + InheritanceHandler inheritance = context.environment().inheritanceHandler(context.dirtyLookup()); + return inheritance.isClassInherited(a, b) || inheritance.isClassInherited(b, a); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/Scope.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Scope.java new file mode 100644 index 00000000..ea31d60a --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/Scope.java @@ -0,0 +1,32 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.model.Relocation; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.Targets; + +public record Scope(Targets targets, Kind kind, int depth, MethodAlignment alignment, @Nullable Relocation relocation) { + public enum Kind { + SAME, + REPLACEMENT, + CALLED + } + + public static Scope of(Targets targets) { + return new Scope(targets, Kind.SAME, 0, MethodAlignment.align(targets.clean().methodNode(), targets.dirty().methodNode()), null); + } + + public TargetPair method() { + return this.targets.dirty(); + } + + public double similarity() { + return this.alignment.similarity(); + } + + public Candidate candidate(AbstractInsnNode insn, MethodAlignment.Confidence confidence) { + return new Candidate(method(), insn, confidence, this.relocation); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/ScopeSearch.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/ScopeSearch.java new file mode 100644 index 00000000..138faea2 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/ScopeSearch.java @@ -0,0 +1,169 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.analysis.flow.DataFlow.ImplicitCalls; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.analysis.method.MethodAnalyzer.IndyHandle; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.Relocation; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.AdapterUtil; + +import java.util.*; +import java.util.function.Predicate; + +public final class ScopeSearch { + public enum Reach { + TARGET, + CALLS + } + + private static final int MAX_DEPTH = 3; + private static final List IGNORED_PACKAGES = List.of("java/", "javax/", "jdk/", "sun/", "com/sun/"); + + private record Callee(TargetPair root, TargetPair method, Set originLocals, @Nullable MethodInsnNode calledFrom, @Nullable MethodInsnNode rootCall) { + } + + private final MixinContext context; + @Nullable + private final TargetPair cleanTarget; + private final List> tiers = new ArrayList<>(); + private final Set visited = new HashSet<>(); + private final int maxTier; + private List frontier; + + private ScopeSearch(MixinContext context, @Nullable TargetPair cleanTarget, List same, List replacements, Reach reach) { + this.context = context; + this.cleanTarget = cleanTarget; + this.tiers.add(same); + this.tiers.add(replacements); + this.maxTier = reach == Reach.CALLS ? 1 + MAX_DEPTH : 1; + same.forEach(scope -> this.visited.add(scope.method().methodNode())); + replacements.forEach(scope -> this.visited.add(scope.method().methodNode())); + } + + public static ScopeSearch of(MixinContext context, Recipe recipe, Reach reach) { + TargetPair cleanTarget = recipe.getCleanTarget(); + if (cleanTarget == null) { + return new ScopeSearch(context, null, List.of(), List.of(), Reach.TARGET); + } + TargetPair same = recipe.getNewCleanTarget(); + List sameScopes = same != null ? List.of(Scope.of(new Targets(cleanTarget, same))) : List.of(); + MethodQualifier qualifier = recipe.clean().getTargetMethod(); + List replacements = qualifier == null ? List.of() : OriginLocator.locate(context, cleanTarget, recipe.clean().getTargetClass(), qualifier).stream() + .filter(origin -> same == null || !origin.method().isSameMethod(same)) + .map(origin -> new Scope(new Targets(cleanTarget, origin.method()), Scope.Kind.REPLACEMENT, 0, origin.alignment(), null)) + .toList(); + return new ScopeSearch(context, cleanTarget, sameScopes, replacements, reach); + } + + public static ScopeSearch of(MixinContext context, Scope scope) { + return new ScopeSearch(context, scope.targets().clean(), List.of(scope), List.of(), Reach.TARGET); + } + + public int tierCount() { + return this.maxTier + 1; + } + + public List tier(int tier) { + while (this.tiers.size() <= tier && this.tiers.size() <= this.maxTier) { + this.tiers.add(nextCalledTier()); + } + return tier < this.tiers.size() ? this.tiers.get(tier) : List.of(); + } + + private List nextCalledTier() { + int depth = this.tiers.size() - 1; + if (this.frontier == null) { + this.frontier = new ArrayList<>(); + for (Scope root : this.tiers.get(0)) { + this.frontier.addAll(findRootCallees(root)); + } + for (Scope root : this.tiers.get(1)) { + this.frontier.addAll(findRootCallees(root)); + } + } else { + List next = new ArrayList<>(); + for (Callee callee : this.frontier) { + next.addAll(findCallees(callee, call -> true, false)); + } + this.frontier = next; + } + return this.frontier.stream() + .map(callee -> new Scope(new Targets(this.cleanTarget, callee.method()), Scope.Kind.CALLED, depth, + MethodAlignment.align(this.cleanTarget.methodNode(), callee.method().methodNode()), relocation(callee))) + .toList(); + } + + private List findRootCallees(Scope root) { + Set newCalls = new HashSet<>(MethodAnalyzer.findDirtyOnlyCalls(this.cleanTarget.methodNode(), root.method().methodNode(), false)); + Callee start = new Callee(root.method(), root.method(), this.context.isStatic() ? Set.of() : Set.of(0), null, null); + return findCallees(start, newCalls::contains, true); + } + + @Nullable + private static Relocation relocation(Callee callee) { + MethodNode method = callee.method().methodNode(); + boolean isStatic = MethodAnalyzer.isStatic(method); + if (callee.method().classNode().name.equals(callee.root().classNode().name) || !isStatic && callee.originLocals().contains(0)) { + return null; + } + Type[] args = Type.getArgumentTypes(method.desc); + int slot = isStatic ? 0 : 1; + for (int i = 0; i < args.length; i++) { + if (callee.originLocals().contains(slot)) { + Relocation.OriginCall via = callee.rootCall() != null ? new Relocation.OriginCall(callee.root(), callee.rootCall()) : null; + return new Relocation.ReceivesOrigin(i, via); + } + slot += args[i].getSize(); + } + return callee.calledFrom() != null ? new Relocation.CalledFrom() : null; + } + + private List findCallees(Callee caller, Predicate filter, boolean fromRoot) { + TargetPair from = caller.method(); + DataFlow flow = this.context.dataFlow(from); + List callees = new ArrayList<>(); + + for (MethodInsnNode call : flow.findTopLevelCalls(ImplicitCalls.UNWRAP)) { + if (!filter.test(call) + || IGNORED_PACKAGES.stream().anyMatch(call.owner::startsWith) + || !this.context.patchContext().environment().isKnownPackage(AdapterUtil.internalNameToPkg(call.owner)) + ) { + continue; + } + TargetPair method = this.context.dirtyMethods().resolveInherited(MethodQualifier.create(call)); + if (method != null && !this.visited.contains(method.methodNode())) { + Set locals = PassedLocals.findPassedSlots(flow, call, call.getOpcode() != Opcodes.INVOKESTATIC, call.desc, caller.originLocals()); + boolean mirrorable = fromRoot && call.getOpcode() == Opcodes.INVOKESTATIC && !method.classNode().name.equals(from.classNode().name); + addCallee(callees, new Callee(caller.root(), method, locals, mirrorable ? call : null, fromRoot ? call : caller.rootCall())); + } + } + + for (IndyHandle lambda : MethodAnalyzer.findOwnIndyHandles(from.classNode(), from.methodNode())) { + if (!this.visited.contains(lambda.method())) { + Handle handle = lambda.handle(); + Set locals = PassedLocals.findPassedSlots(flow, lambda.indy(), handle.getTag() != Opcodes.H_INVOKESTATIC, handle.getDesc(), caller.originLocals()); + addCallee(callees, new Callee(caller.root(), new TargetPair(from.classNode(), lambda.method()), locals, null, fromRoot ? null : caller.rootCall())); + } + } + return callees; + } + + private void addCallee(List callees, Callee callee) { + if (this.context.isStatic() || !callee.originLocals().isEmpty() || callee.calledFrom() != null) { + this.visited.add(callee.method().methodNode()); + callees.add(callee); + } + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/locate/SelectedLocator.java b/core/src/main/java/org/sinytra/adapter/pipeline/locate/SelectedLocator.java new file mode 100644 index 00000000..8cc70e0a --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/locate/SelectedLocator.java @@ -0,0 +1,23 @@ +package org.sinytra.adapter.pipeline.locate; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; + +public final class SelectedLocator implements Locator { + @Override + public List locate(MixinContext context, Recipe recipe, Scope scope, AbstractInsnNode cleanInsn) { + AtData cleanAt = recipe.clean().getAtData(); + if (cleanAt == null || scope.kind() == Scope.Kind.SAME) { + return List.of(); + } + return Describer.findSelectable(context, scope.method(), cleanAt, at -> InjectionQuery.of(at).ignoringSlice()).stream() + .map(insn -> scope.candidate(insn, MethodAlignment.Confidence.POSITIONAL)) + .toList(); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/ConfigurationMatcher.java b/core/src/main/java/org/sinytra/adapter/pipeline/manual/ConfigurationMatcher.java similarity index 95% rename from core/src/main/java/org/sinytra/adapter/transform/patch/ConfigurationMatcher.java rename to core/src/main/java/org/sinytra/adapter/pipeline/manual/ConfigurationMatcher.java index 33544f81..e8183169 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/ConfigurationMatcher.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/manual/ConfigurationMatcher.java @@ -1,8 +1,8 @@ -package org.sinytra.adapter.transform.patch; +package org.sinytra.adapter.pipeline.manual; import com.google.common.collect.ImmutableList; -import org.sinytra.adapter.patch.config.PropertyContainer; -import org.sinytra.adapter.patch.config.PropertyKey; +import org.sinytra.adapter.model.config.PropertyContainer; +import org.sinytra.adapter.model.config.PropertyKey; import java.util.*; import java.util.function.Consumer; diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilderImpl.java b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchBuilderImpl.java similarity index 83% rename from core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilderImpl.java rename to core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchBuilderImpl.java index 1192e5e7..af11badf 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilderImpl.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchBuilderImpl.java @@ -1,29 +1,29 @@ -package org.sinytra.adapter.transform.patch; +package org.sinytra.adapter.pipeline.manual; import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.commons.InstructionAdapter; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.util.MixinAnnotationConstants; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.patch.config.key.SpecialKeys; -import org.sinytra.adapter.transform.MethodTransformer; -import org.sinytra.adapter.util.MethodQualifier; +import org.sinytra.adapter.api.MethodPatch; +import org.sinytra.adapter.api.MethodPatchBuilder; +import org.sinytra.adapter.api.MethodTransformer; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.MixinAnnotationConstants; +import org.sinytra.adapter.model.param.MethodParameters; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.UnaryOperator; import java.util.stream.Stream; import static java.util.function.Predicate.isEqual; -import static org.sinytra.adapter.patch.config.key.ControlKeys.MIXIN_TYPE; -import static org.sinytra.adapter.patch.config.key.ControlKeys.TARGET_CLASS; +import static org.sinytra.adapter.model.config.key.ControlKeys.MIXIN_TYPE; +import static org.sinytra.adapter.model.config.key.ControlKeys.TARGET_CLASS; public class MethodPatchBuilderImpl implements MethodPatchBuilder { private final ConfigurationMatcher.Builder matcher = ConfigurationMatcher.builder(); @@ -146,7 +146,7 @@ public MethodPatchBuilder modifyParams(UnaryOperator op) { @Override public MethodPatchBuilder modifyStatic(boolean isStatic) { - this.config.setProperty(SpecialKeys.STATIC, isStatic); + this.config.setProperty(ControlKeys.STATIC, isStatic); return this; } @@ -162,12 +162,6 @@ public MethodPatchBuilder disable() { return this; } - @Override - public MethodPatchBuilder divertRedirector(Consumer patcher) { - this.config.setProperty(SpecialKeys.REDIRECT_ADAPTER, patcher); - return this; - } - @Override public MethodPatchBuilder transform(MethodTransformer transformer) { this.transforms.add(transformer); diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchImpl.java b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchImpl.java similarity index 81% rename from core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchImpl.java rename to core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchImpl.java index 7a2e553f..01ea1372 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchImpl.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchImpl.java @@ -1,8 +1,9 @@ -package org.sinytra.adapter.transform.patch; +package org.sinytra.adapter.pipeline.manual; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.transform.MethodTransformer; +import org.sinytra.adapter.api.MethodPatch; +import org.sinytra.adapter.api.MethodTransformer; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; import java.util.List; import java.util.function.BiConsumer; diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchProcessor.java new file mode 100644 index 00000000..9b67f4b8 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchProcessor.java @@ -0,0 +1,20 @@ +package org.sinytra.adapter.pipeline.manual; + +import org.sinytra.adapter.api.MethodTransformer; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.processor.Processor; + +import java.util.List; + +public record MethodPatchProcessor(List transforms) implements Processor { + @Override + public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + for (MethodTransformer transformer : this.transforms) { + transformer.apply(context); + } + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchResolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchResolver.java similarity index 80% rename from core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchResolver.java rename to core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchResolver.java index a68bef58..82ec68a6 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchResolver.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/manual/MethodPatchResolver.java @@ -1,12 +1,13 @@ -package org.sinytra.adapter.transform.patch; +package org.sinytra.adapter.pipeline.manual; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.Configurations; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.resolver.Resolver; -import org.sinytra.adapter.transform.MethodTransformer; +import org.sinytra.adapter.api.MethodPatch; +import org.sinytra.adapter.api.MethodTransformer; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.Configurations; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolver; import java.util.ArrayList; import java.util.List; diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/DisableMixinProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/DisableMixinProcessor.java similarity index 64% rename from core/src/main/java/org/sinytra/adapter/patch/processor/DisableMixinProcessor.java rename to core/src/main/java/org/sinytra/adapter/pipeline/processor/DisableMixinProcessor.java index 0bf7f018..1a8d8b56 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/DisableMixinProcessor.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/DisableMixinProcessor.java @@ -1,9 +1,9 @@ -package org.sinytra.adapter.patch.processor; +package org.sinytra.adapter.pipeline.processor; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; public class DisableMixinProcessor implements Processor { @Override diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/ExpressionProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/ExpressionProcessor.java new file mode 100644 index 00000000..4a6420f3 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/ExpressionProcessor.java @@ -0,0 +1,30 @@ +package org.sinytra.adapter.pipeline.processor; + +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.model.ann.ExpressionData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.key.IntentKeys; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.ArrayList; + +public class ExpressionProcessor implements Processor { + @Override + public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + ExpressionData expression = dirty.getProperty(IntentKeys.EXPRESSION).orElse(null); + if (expression == null) { + return TxResult.PASS; + } + + MethodNode handler = context.methodNode(); + if (handler.invisibleAnnotations == null) { + handler.invisibleAnnotations = new ArrayList<>(); + } + handler.invisibleAnnotations.removeIf(ExpressionData::isExpressionAnnotation); + handler.invisibleAnnotations.addAll(expression.toAnnotationNodes()); + context.recordCtxAudit("Set expression to: %s", expression.expression()); + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/MixinTypeProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/MixinTypeProcessor.java similarity index 75% rename from core/src/main/java/org/sinytra/adapter/patch/processor/MixinTypeProcessor.java rename to core/src/main/java/org/sinytra/adapter/pipeline/processor/MixinTypeProcessor.java index e499ac39..3c1a4e1a 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/MixinTypeProcessor.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/MixinTypeProcessor.java @@ -1,12 +1,12 @@ -package org.sinytra.adapter.patch.processor; +package org.sinytra.adapter.pipeline.processor; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; public class MixinTypeProcessor implements Processor { diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/Processor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/Processor.java new file mode 100644 index 00000000..6d634ac6 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/Processor.java @@ -0,0 +1,10 @@ +package org.sinytra.adapter.pipeline.processor; + +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +public interface Processor { + TxResult process(MixinContext context, Configuration dirty, Recipe recipe); +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/Processors.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/Processors.java similarity index 55% rename from core/src/main/java/org/sinytra/adapter/patch/processor/Processors.java rename to core/src/main/java/org/sinytra/adapter/pipeline/processor/Processors.java index f574aee3..f1836837 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/Processors.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/Processors.java @@ -1,7 +1,8 @@ -package org.sinytra.adapter.patch.processor; +package org.sinytra.adapter.pipeline.processor; -import org.sinytra.adapter.env.util.OrderedRegistry; -import org.sinytra.adapter.patch.processor.extract.ExtractMixinProcessor; +import org.sinytra.adapter.pipeline.processor.relocate.RelocationProcessor; +import org.sinytra.adapter.pipeline.processor.signature.SignatureProcessor; +import org.sinytra.adapter.util.OrderedRegistry; public class Processors extends OrderedRegistry { @@ -19,21 +20,19 @@ private void registerDefaultProcessors() { // === Standard changes === // General annotation props add(new PropertyProcessor()); + // MixinExtras expression of an expression @At before anything evaluates the dirty @At + add(new ExpressionProcessor()); // Target method post processor add(new TargetMethodProcessor()); - // Mixin method parameters - add(new ParametersProcessor()); - // Parameter casts - add(new ParametersPostProcessor()); - // Mixin method return type - add(new ReturnTypeProcessor()); - // Static access modifier - add(new StaticAccessProcessor()); + // Mixin method signature + add(new SignatureProcessor()); + // Sugar @Local parameters, once the signature fits the dirty target + add(new SugarLocalProcessor()); // Side effects that must only happen once the mixin is otherwise patched add(new SplitCancellationProcessor()); // Late extract processor - must come last // Changed class target - add(new ExtractMixinProcessor()); + add(new RelocationProcessor()); } } diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/PropertyProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/PropertyProcessor.java similarity index 70% rename from core/src/main/java/org/sinytra/adapter/patch/processor/PropertyProcessor.java rename to core/src/main/java/org/sinytra/adapter/pipeline/processor/PropertyProcessor.java index b477dd2d..41aafa52 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/PropertyProcessor.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/PropertyProcessor.java @@ -1,20 +1,21 @@ -package org.sinytra.adapter.patch.processor; - -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.MutableConfiguration; -import org.sinytra.adapter.patch.config.PropertyKey; -import org.sinytra.adapter.patch.config.key.MixinKeys; - +package org.sinytra.adapter.pipeline.processor; + +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.PropertyKey; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; public class PropertyProcessor implements Processor { - private static final Set> ACCEPTED_KEYS = Set.of( + private static final List> ACCEPTED_KEYS = List.of( MixinKeys.TARGET_METHOD, MixinKeys.TARGET_AT, MixinKeys.TARGET_CONSTANT, MixinKeys.ORDINAL, MixinKeys.INDEX, MixinKeys.SLICE, MixinKeys.SLICES, MixinKeys.ARGS_ONLY ); diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/SplitCancellationProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/SplitCancellationProcessor.java new file mode 100644 index 00000000..506ced53 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/SplitCancellationProcessor.java @@ -0,0 +1,53 @@ +package org.sinytra.adapter.pipeline.processor; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.pipeline.processor.relocate.RelocationGenerator; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; +import java.util.stream.IntStream; + +public class SplitCancellationProcessor implements Processor { + @Override + public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + if (!recipe.clean().isCancellable()) { + return TxResult.PASS; + } + + TargetPair cleanTarget = recipe.getCleanTarget(); + TargetPair split = recipe.getNewCleanTarget(); + TargetPair piece = recipe.getDirtyTarget(); + if (cleanTarget == null || split == null || piece == null + || !MethodAnalyzer.isDirtyDeprecatedMethod(cleanTarget.methodNode(), split.methodNode()) + || Type.getReturnType(split.methodNode().desc) != Type.VOID_TYPE + ) { + return TxResult.PASS; + } + + List pieces = context.dataFlow(split).findTopLevelOwnCalls(); + MethodNode target = piece.methodNode(); + int index = IntStream.range(0, pieces.size()) + .filter(i -> pieces.get(i).name.equals(target.name) && pieces.get(i).desc.equals(target.desc)) + .findFirst() + .orElse(-1); + List laterPieces = index != -1 ? pieces.subList(index + 1, pieces.size()) : List.of(); + if (laterPieces.isEmpty()) { + return TxResult.PASS; + } + + if (laterPieces.stream().anyMatch(m -> MethodAnalyzer.isStatic(m) != context.isStatic())) { + context.recordCtxAudit("Can't propagate cancellation: the pieces of %s differ in static-ness", split.methodNode().name); + return TxResult.PASS; + } + + RelocationGenerator.generateCancellers(context, split.classNode(), laterPieces); + context.recordCtxAudit("Propagate cancellation to %s", laterPieces.stream().map(m -> m.name).toList()); + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/SugarLocalProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/SugarLocalProcessor.java new file mode 100644 index 00000000..71665e6f --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/SugarLocalProcessor.java @@ -0,0 +1,173 @@ +package org.sinytra.adapter.pipeline.processor; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.analysis.method.InheritanceHandler; +import org.sinytra.adapter.edit.param.ParamTransformationUtil; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.locate.Describer; +import org.sinytra.adapter.pipeline.locate.LocalLocator; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.MixinBridge; + +import java.util.ArrayList; +import java.util.List; + + +public class SugarLocalProcessor implements Processor { + @Override + public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + TargetPair cleanTarget = recipe.getCleanTarget(); + TargetPair dirtyTarget = recipe.getDirtyTarget(); + AtData dirtyAt = dirty.getAtData(); + if (cleanTarget == null || dirtyTarget == null || dirtyAt == null) { + return TxResult.PASS; + } + + MethodNode cleanHandler = context.unmodifiedMethodNode(); + MethodNode handler = context.methodNode(); + List cleanLocals = findLocalParameters(cleanHandler); + List locals = findLocalParameters(handler); + if (locals.isEmpty() || cleanLocals.size() != locals.size()) { + return TxResult.PASS; + } + AbstractInsnNode cleanInsn = context.injections().resolveFirst(cleanTarget); + AbstractInsnNode dirtyInsn = context.injections().resolveFirst(dirtyTarget, InjectionQuery.of(dirtyAt)); + if (cleanInsn == null || dirtyInsn == null) { + return TxResult.PASS; + } + + String mixin = context.classNode().name; + MixinBridge.Session cleanSession = MixinBridge.session(mixin, cleanHandler, cleanTarget.classNode(), cleanTarget.methodNode(), context.environment()); + MixinBridge.Session dirtySession = MixinBridge.session(mixin, handler, dirtyTarget.classNode(), dirtyTarget.methodNode(), context.environment()); + TargetPair origin = recipe.getNewCleanTarget(); + boolean sameMethod = origin != null && origin.isSameMethod(dirtyTarget); + boolean changed = false; + for (int i = 0; i < locals.size(); i++) { + int cleanIndex = cleanLocals.get(i); + int index = locals.get(i); + AnnotationNode cleanSelector = getLocalAnnotation(cleanHandler, cleanIndex); + AnnotationNode selector = getLocalAnnotation(handler, index); + Type cleanType = Type.getArgumentTypes(cleanHandler.desc)[cleanIndex]; + Type type = Type.getArgumentTypes(handler.desc)[index]; + + int current = dirtySession.findLocal(selector, type, dirtyInsn); + if (current != -1 && sameMethod) { + continue; + } + int cleanSlot = cleanSession.findLocal(cleanSelector, cleanType, cleanInsn); + if (cleanSlot == -1) { + continue; + } + int slot = LocalLocator.resolve(context, recipe, cleanTarget, cleanSlot, cleanInsn, dirtyTarget); + if (slot == -1 || current == slot) { + continue; + } + + Type dirtyType = getLocalType(dirtyTarget.methodNode(), slot, dirtyInsn); + Boolean widened = dirtyType == null ? null : compareTypes(context, type, dirtyType); + if (widened == null) { + context.recordCtxAudit("Can't select local %s for @Local parameter %s of type %s", slot, index, type); + continue; + } + AnnotationNode newSelector = Describer.describeLocalSelector(context, dirtySession, selector, dirtyTarget, dirtyInsn, slot, dirtyType); + if (newSelector == null) { + context.recordCtxAudit("Can't select local %s for @Local parameter %s", slot, index); + continue; + } + + selector.values = newSelector.values; + if (!dirtyType.equals(type)) { + ParamTransformationUtil.retypeParameter(handler, index, dirtyType, widened); + } + updateModel(dirty.getParameters(), handler, index); + context.recordCtxAudit("Select local %s of type %s for @Local parameter %s", slot, dirtyType, index); + changed = true; + } + return changed ? TxResult.SUCCESS : TxResult.PASS; + } + + private static List findLocalParameters(MethodNode method) { + List indices = new ArrayList<>(); + int count = Type.getArgumentCount(method.desc); + for (int i = 0; i < count; i++) { + if (getLocalAnnotation(method, i) != null) { + indices.add(i); + } + } + return indices; + } + + @Nullable + public static AnnotationNode getLocalAnnotation(MethodNode method, int index) { + for (List[] annotations : List.of( + method.invisibleParameterAnnotations != null ? method.invisibleParameterAnnotations : new List[0], + method.visibleParameterAnnotations != null ? method.visibleParameterAnnotations : new List[0] + )) { + AnnotationNode local = index < annotations.length ? AdapterUtil.getAnnotation(annotations[index], MixinAnnotations.LOCAL) : null; + if (local != null) { + return local; + } + } + return null; + } + + @Nullable + private static Type getLocalType(MethodNode method, int slot, AbstractInsnNode insn) { + LocalVariableLookup lookup = LocalVariableLookup.of(method); + LocalVariableNode local = lookup != null ? lookup.getInScopeOrNull(slot, insn) : null; + if (local == null && lookup != null && Describer.isParameter(method, slot)) { + local = lookup.getLocals().stream().filter(l -> l.index == slot).findFirst().orElse(null); + } + return local != null ? Type.getType(local.desc) : null; + } + + @Nullable + private static Boolean compareTypes(MixinContext context, Type type, Type dirtyType) { + if (type.equals(dirtyType)) { + return false; + } + if (type.getSort() != Type.OBJECT || dirtyType.getSort() != Type.OBJECT) { + return null; + } + InheritanceHandler inheritance = context.environment().inheritanceHandler(context.dirtyLookup()); + if (inheritance.isClassInherited(dirtyType.getInternalName(), type.getInternalName())) { + return false; + } + if (inheritance.isClassInherited(type.getInternalName(), dirtyType.getInternalName())) { + return true; + } + return null; + } + + private static void updateModel(@Nullable MethodParameters params, MethodNode handler, int index) { + if (params == null) { + return; + } + Parameter parameter = Parameters.parse(handler, index, index + 1).getFirst(); + int offset = 0; + for (MethodParameters.ParamGroup group : params.getOrder()) { + List members = params.get(group); + if (index < offset + members.size()) { + members.set(index - offset, parameter); + return; + } + offset += members.size(); + } + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/TargetMethodProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/TargetMethodProcessor.java new file mode 100644 index 00000000..55a1665e --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/TargetMethodProcessor.java @@ -0,0 +1,38 @@ +package org.sinytra.adapter.pipeline.processor; + +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.locals.LocalVariable; +import org.sinytra.adapter.analysis.locals.LocalsAnalyzer; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; +import java.util.Objects; + +public class TargetMethodProcessor implements Processor { + @Override + public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + if (dirty.getTargetMethod() == null) return TxResult.FAIL; + + if (Objects.equals(recipe.clean().getTargetClass(), dirty.getTargetClass())) { + upgradeCapturedLocals(context.methodNode(), context, recipe); + } + + return TxResult.SUCCESS; + } + + // TODO Is there a better approach? + private static void upgradeCapturedLocals(MethodNode methodNode, MixinContext context, Recipe recipe) { + LocalsAnalyzer.CapturedLocals capturedLocals = context.locals().resolveCapturedLocals(recipe.getDirtyTarget()); + if (capturedLocals == null) return; + + List availableLocals = context.locals().resolveAvailableLocals(capturedLocals.target()); + // For now, only handle cases where all locals are part of the method's params, convenient when switching the target to a lambda + if (availableLocals == null || !availableLocals.isEmpty()) return; + + LocalsAnalyzer.CapturedLocalsTransform transform = LocalsAnalyzer.analyzeCapturedLocals(capturedLocals, methodNode); + transform.remover().apply(context); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpSurgeon.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/WrapOpSurgeon.java similarity index 91% rename from core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpSurgeon.java rename to core/src/main/java/org/sinytra/adapter/pipeline/processor/WrapOpSurgeon.java index daba6e89..aebdeee7 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpSurgeon.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/WrapOpSurgeon.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.patch.processor.wrapop; +package org.sinytra.adapter.pipeline.processor; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; @@ -7,14 +7,15 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.TargetPair; -import org.sinytra.adapter.patch.Recipe; +import org.sinytra.adapter.analysis.flow.DataFlow; import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.types.TypeAdapter; +import org.sinytra.adapter.analysis.method.WrapOpOriginalCall; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.sinytra.adapter.util.AdapterUtil; import org.sinytra.adapter.util.OpcodeUtil; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; import java.util.ArrayList; import java.util.HashMap; @@ -28,7 +29,7 @@ public class WrapOpSurgeon { public static boolean tryUpgrade(MixinContext context, Recipe recipe, List methodParams, MethodInsnNode cleanInsn, MethodInsnNode dirtyInsn) { MethodNode methodNode = context.methodNode(); - LocalVariableLookup mixinLocals = context.methods().getLVT(methodNode); + LocalVariableLookup mixinLocals = LocalVariableLookup.of(methodNode); if (mixinLocals == null) return false; LocalVariableNode instanceLocal = mixinLocals.getByParameterOrdinalOrNull(0); @@ -68,7 +69,7 @@ public static boolean tryUpgrade(MixinContext context, Recipe recipe, List TargetPair cleanTarget = recipe.getCleanTarget(); if (cleanTarget == null) return null; - List> callArgInsns = MethodAnalyzer.getMethodCallArgInsns(cleanTarget.methodNode(), cleanInsn); + List> callArgInsns = context.dataFlow(cleanTarget).findOperandExpressions(cleanInsn); if (callArgInsns.isEmpty()) return null; List receiverInsns = callArgInsns.getFirst(); @@ -88,7 +89,7 @@ public static boolean tryUpgrade(MixinContext context, Recipe recipe, List TypeAdapter adapter = typeAdapter.andThen((list, insn) -> { // Fix receiver type originally changed by ReplaceParametersTransformer - MethodAnalyzer.forEachReceiver(context.methodNode(), (call, receiver) -> { + DataFlow.of(context.methodNode()).forEachReceiver((call, receiver) -> { if (call.owner.equals(dirtyInsn.owner) && receiver.contains(insn)) { call.owner = cleanInsn.owner; } diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationGenerator.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationGenerator.java new file mode 100644 index 00000000..1a0e4434 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationGenerator.java @@ -0,0 +1,489 @@ +package org.sinytra.adapter.pipeline.processor.relocate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ValueSource; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.constant.TypeConstants; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_ARGS_ONLY; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_INDEX; + +public final class RelocationGenerator { + public static MethodNode generateMirror(MixinContext context, String destinationClass, RelocationPlan.Mirror plan) { + MethodNode handler = context.methodNode(); + ClassNode handlerOwner = plan.handlerOwner(); + boolean isStatic = plan.isStatic(); + Type[] handlerArgs = Type.getArgumentTypes(handler.desc); + + int forwardedCount = (int) plan.params().stream().filter(ValueSource.Forwarded.class::isInstance).count(); + List params = new ArrayList<>(); + for (int i = 0; i < forwardedCount; i++) { + params.add(null); + } + for (int i = 0; i < plan.params().size(); i++) { + if (plan.params().get(i) instanceof ValueSource.Forwarded(int param)) { + params.set(param, handlerArgs[i]); + } + } + + Map paramIndices = new HashMap<>(); + List captured = new ArrayList<>(); + List sources = new ArrayList<>(); + if (plan.receiver() != null) { + sources.add(plan.receiver()); + } + sources.addAll(plan.params()); + for (ValueSource source : sources) { + if (source instanceof ValueSource.Captured capture && !paramIndices.containsKey(capture)) { + paramIndices.put(capture, params.size()); + params.add(capture.type()); + captured.add(capture); + } + } + + String name = handler.name + "$adapter$mirror$" + simpleName(context.classNode()); + String desc = Type.getMethodDescriptor(Type.getReturnType(handler.desc), params.toArray(Type[]::new)); + MethodNode mirror = new MethodNode(Opcodes.ACC_PRIVATE | (isStatic ? Opcodes.ACC_STATIC : 0), name, desc, null, null); + mirror.visibleAnnotations = new ArrayList<>(handler.visibleAnnotations); + copyParameterAnnotations(handler, mirror, plan.params()); + for (ValueSource.Captured capture : captured) { + AnnotationVisitor local = mirror.visitParameterAnnotation(paramIndices.get(capture), MixinAnnotations.LOCAL, false); + local.visit(PROPERTY_ARGS_ONLY, capture.argsOnly()); + local.visit(PROPERTY_INDEX, capture.slot()); + } + + GeneratorAdapter gen = new GeneratorAdapter(mirror, mirror.access, mirror.name, mirror.desc); + gen.newLabel(); + int stack = 0; + int guardStack = 0; + if (plan.receiver() != null && plan.guard() != null) { + String active = generateGuard(context, plan.guard()); + Label run = new Label(); + guardStack = loadReceiver(gen, plan.receiver(), paramIndices, params, handlerOwner); + gen.visitMethodInsn(Opcodes.INVOKEVIRTUAL, handlerOwner.name, active, "()Z", false); + gen.visitJumpInsn(Opcodes.IFNE, run); + switch (plan.guard().passthrough()) { + case SKIP -> { + } + case KEEP_VALUE -> gen.loadArg(0); + case PROCEED -> gen.push(true); + case CALL_ORIGINAL -> guardStack = Math.max(guardStack, callOriginal(gen, params)); + } + gen.returnValue(); + gen.visitLabel(run); + } + if (plan.receiver() != null) { + stack += loadReceiver(gen, plan.receiver(), paramIndices, params, handlerOwner); + } + for (ValueSource source : plan.params()) { + stack += load(gen, source, paramIndices); + } + invokeHandler(gen, handler, handlerOwner); + gen.newLabel(); + gen.returnValue(); + gen.newLabel(); + gen.endMethod(); + mirror.maxStack = Math.max(Math.max(stack, guardStack), Type.getReturnType(handler.desc).getSize()); + mirror.maxLocals = (Type.getArgumentsAndReturnSizes(desc) >> 2) - (isStatic ? 1 : 0); + + handler.access = OpcodeUtil.setAccessVisibility(handler.access, Opcodes.ACC_PUBLIC); + handler.visibleAnnotations.remove(context.methodAnnotation().unwrap()); + handler.visitAnnotation(MixinAnnotations.UNIQUE, true); + + ClassNode generatedTarget = getGeneratedTarget(context, destinationClass); + synchronized (generatedTarget) { + mirror.name = uniqueMethodName(generatedTarget, mirror.name); + generatedTarget.methods.add(mirror); + } + context.patchContext().onRollback(() -> { + synchronized (generatedTarget) { + generatedTarget.methods.remove(mirror); + } + }); + return mirror; + } + + private static int loadReceiver(GeneratorAdapter gen, ValueSource receiver, Map paramIndices, List params, ClassNode handlerOwner) { + int size = load(gen, receiver, paramIndices); + Type ownerType = Type.getObjectType(handlerOwner.name); + if (!params.get(argIndex(receiver, paramIndices)).equals(ownerType)) { + gen.checkCast(ownerType); + } + return size; + } + + private static int callOriginal(GeneratorAdapter gen, List params) { + int operation = params.indexOf(TypeConstants.OPERATION_TYPE); + gen.loadArg(operation); + gen.push(operation); + gen.newArray(TypeConstants.OBJECT_TYPE); + int operandSize = 1; + for (int i = 0; i < operation; i++) { + gen.dup(); + gen.push(i); + gen.loadArg(i); + gen.box(params.get(i)); + gen.arrayStore(TypeConstants.OBJECT_TYPE); + operandSize = Math.max(operandSize, params.get(i).getSize()); + } + gen.invokeInterface(TypeConstants.OPERATION_TYPE, new Method("call", TypeConstants.OBJECT_TYPE, new Type[]{Type.getType(Object[].class)})); + Type returnType = gen.getReturnType(); + if (returnType == Type.VOID_TYPE) { + gen.pop(); + } else { + gen.unbox(returnType); + } + return 4 + operandSize + 2; + } + + private static String generateGuard(MixinContext context, RelocationPlan.Guard guard) { + TargetPair origin = guard.via().method(); + MethodInsnNode call = guard.via().call(); + int ordinal = 0; + for (AbstractInsnNode insn = origin.methodNode().instructions.getFirst(); insn != call; insn = insn.getNext()) { + if (insn instanceof MethodInsnNode other && other.owner.equals(call.owner) && other.name.equals(call.name) && other.desc.equals(call.desc)) { + ordinal++; + } + } + + ClassNode generatedTarget = getGeneratedTarget(context, origin.classNode().name); + String base = "adapter$via$" + simpleName(context.classNode()) + "$" + context.methodNode().name; + FieldNode depth; + List methods; + synchronized (generatedTarget) { + String name = base; + for (int i = 1; hasMethodNamed(generatedTarget, name) || hasFieldNamed(generatedTarget, name); i++) { + name = base + "$" + i; + } + depth = new FieldNode(Opcodes.ACC_PRIVATE, name, Type.INT_TYPE.getDescriptor(), null, null); + methods = List.of( + generateIsActive(generatedTarget, depth), + generateDepthChange(origin, call, ordinal, generatedTarget, depth, 1), + generateDepthChange(origin, call, ordinal, generatedTarget, depth, -1) + ); + generatedTarget.fields.add(depth); + generatedTarget.methods.addAll(methods); + } + context.patchContext().onRollback(() -> { + synchronized (generatedTarget) { + generatedTarget.fields.remove(depth); + generatedTarget.methods.removeAll(methods); + } + }); + return depth.name; + } + + private static MethodNode generateIsActive(ClassNode generatedTarget, FieldNode depth) { + MethodNode active = new MethodNode(Opcodes.ACC_PUBLIC, depth.name, "()Z", null, null); + GeneratorAdapter gen = new GeneratorAdapter(active, active.access, active.name, active.desc); + Label inactive = new Label(); + gen.loadThis(); + gen.getField(Type.getObjectType(generatedTarget.name), depth.name, Type.INT_TYPE); + gen.visitJumpInsn(Opcodes.IFLE, inactive); + gen.push(true); + gen.returnValue(); + gen.visitLabel(inactive); + gen.push(false); + gen.returnValue(); + gen.endMethod(); + active.maxStack = 1; + active.maxLocals = 1; + return active; + } + + private static MethodNode generateDepthChange(TargetPair origin, MethodInsnNode call, int ordinal, ClassNode generatedTarget, FieldNode depth, int change) { + Type callback = Type.getReturnType(origin.methodNode().desc) == Type.VOID_TYPE ? TypeConstants.CI_TYPE : TypeConstants.CIR_TYPE; + String name = depth.name + (change > 0 ? "$enter" : "$exit"); + MethodNode inject = new MethodNode(Opcodes.ACC_PRIVATE, name, Type.getMethodDescriptor(Type.VOID_TYPE, callback), null, null); + AnnotationVisitor annotation = inject.visitAnnotation(MixinAnnotations.INJECT, true); + AnnotationVisitor method = annotation.visitArray("method"); + method.visit(null, origin.methodNode().name + origin.methodNode().desc); + method.visitEnd(); + AnnotationVisitor ats = annotation.visitArray("at"); + AnnotationVisitor at = ats.visitAnnotation(null, MixinAnnotations.AT); + at.visit("value", "INVOKE"); + at.visit("target", Type.getObjectType(call.owner).getDescriptor() + call.name + call.desc); + at.visit("ordinal", ordinal); + if (change < 0) { + at.visitEnum("shift", "Lorg/spongepowered/asm/mixin/injection/At$Shift;", "AFTER"); + } + at.visitEnd(); + ats.visitEnd(); + annotation.visitEnd(); + + Type owner = Type.getObjectType(generatedTarget.name); + GeneratorAdapter gen = new GeneratorAdapter(inject, inject.access, inject.name, inject.desc); + gen.loadThis(); + gen.dup(); + gen.getField(owner, depth.name, Type.INT_TYPE); + gen.push(change); + gen.math(GeneratorAdapter.ADD, Type.INT_TYPE); + gen.putField(owner, depth.name, Type.INT_TYPE); + gen.returnValue(); + gen.endMethod(); + inject.maxStack = 3; + inject.maxLocals = 2; + return inject; + } + + public static void generateMove(MixinContext context, String destinationClass, RelocationPlan.Move plan) { + ClassNode mixin = context.classNode(); + ClassNode generatedTarget = getGeneratedTarget(context, destinationClass); + List moved = new ArrayList<>(plan.lambdas()); + moved.addFirst(context.methodNode()); + registerMoveRollback(context, generatedTarget, plan, moved); + + if (plan.makeStatic()) { + AdapterUtil.makeStatic(context.methodNode()); + } + if (MethodAnalyzer.isStatic(context.methodNode()) && (context.methodNode().access & Opcodes.ACC_SYNTHETIC) == 0) { + context.methodNode().access = OpcodeUtil.setAccessVisibility(context.methodNode().access, Opcodes.ACC_PRIVATE); + } + plan.widenedFields().forEach(field -> field.access = widenAccess(field.access)); + plan.widenedMethods().forEach(method -> method.access = widenAccess(method.access)); + for (MethodNode method : moved) { + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof MethodInsnNode methodInsn && methodInsn.owner.equals(mixin.name)) { + methodInsn.owner = destinationClass; + } else if (insn instanceof FieldInsnNode fieldInsn && fieldInsn.owner.equals(mixin.name)) { + fieldInsn.owner = destinationClass; + } else if (insn instanceof InvokeDynamicInsnNode indy) { + retargetLambdas(indy, mixin, plan.lambdas(), generatedTarget); + } + } + if (!MethodAnalyzer.isStatic(method) && method.localVariables != null) { + method.localVariables.stream() + .filter(local -> local.index == 0) + .findFirst() + .ifPresent(local -> local.desc = Type.getObjectType(generatedTarget.name).getDescriptor()); + } + } + + synchronized (generatedTarget) { + generatedTarget.methods.addAll(moved); + } + context.patchContext().postApply(() -> mixin.methods.removeAll(moved)); + } + + public static void generateCancellers(MixinContext context, ClassNode targetClass, List laterPieces) { + MethodNode handler = context.methodNode(); + boolean isStatic = context.isStatic(); + ClassNode generatedTarget = getGeneratedTarget(context, targetClass.name); + FieldNode tracker = new FieldNode(Opcodes.ACC_PRIVATE | (isStatic ? Opcodes.ACC_STATIC : 0), + "adapter$cancelled$" + simpleName(context.classNode()) + "$" + handler.name, Type.BOOLEAN_TYPE.getDescriptor(), null, null); + List cancellers = new ArrayList<>(); + synchronized (generatedTarget) { + String base = tracker.name; + for (int i = 1; generatedTarget.fields.stream().anyMatch(f -> f.name.equals(tracker.name)); i++) { + tracker.name = base + "$" + i; + } + generatedTarget.fields.add(tracker); + for (int i = 0; i < laterPieces.size(); i++) { + MethodNode canceller = generateCanceller(generatedTarget, tracker, handler, laterPieces.get(i), isStatic, i == laterPieces.size() - 1); + canceller.name = uniqueMethodName(generatedTarget, canceller.name); + generatedTarget.methods.add(canceller); + cancellers.add(canceller); + } + } + context.patchContext().onRollback(() -> { + synchronized (generatedTarget) { + generatedTarget.fields.remove(tracker); + generatedTarget.methods.removeAll(cancellers); + } + }); + + MethodQualifier cancel = new MethodQualifier(TypeConstants.CI_TYPE.getDescriptor(), "cancel", "()V"); + for (MethodInsnNode call : MethodAnalyzer.findCalls(handler, cancel)) { + InsnList set = new InsnList(); + if (!isStatic) { + set.add(new VarInsnNode(Opcodes.ALOAD, 0)); + } + set.add(new InsnNode(Opcodes.ICONST_1)); + set.add(new FieldInsnNode(isStatic ? Opcodes.PUTSTATIC : Opcodes.PUTFIELD, targetClass.name, tracker.name, tracker.desc)); + handler.instructions.insertBefore(call, set); + } + } + + private static MethodNode generateCanceller(ClassNode generatedTarget, FieldNode tracker, MethodNode handler, MethodNode piece, boolean isStatic, boolean last) { + String desc = Type.getMethodDescriptor(Type.VOID_TYPE, TypeConstants.CI_TYPE); + MethodNode canceller = new MethodNode(Opcodes.ACC_PRIVATE | (isStatic ? Opcodes.ACC_STATIC : 0), handler.name + "$adapter$canceller$" + piece.name, desc, null, null); + AnnotationVisitor inject = canceller.visitAnnotation(MixinAnnotations.INJECT, true); + AnnotationVisitor method = inject.visitArray("method"); + method.visit(null, piece.name + piece.desc); + method.visitEnd(); + AnnotationVisitor at = inject.visitArray("at"); + AnnotationVisitor head = at.visitAnnotation(null, MixinAnnotations.AT); + head.visit("value", "HEAD"); + head.visitEnd(); + at.visitEnd(); + inject.visit("cancellable", Boolean.TRUE); + inject.visitEnd(); + + Type owner = Type.getObjectType(generatedTarget.name); + GeneratorAdapter gen = new GeneratorAdapter(canceller, canceller.access, canceller.name, canceller.desc); + Label end = new Label(); + gen.newLabel(); + if (isStatic) { + gen.getStatic(owner, tracker.name, Type.BOOLEAN_TYPE); + } else { + gen.loadThis(); + gen.getField(owner, tracker.name, Type.BOOLEAN_TYPE); + } + gen.visitJumpInsn(Opcodes.IFEQ, end); + if (last) { + gen.newLabel(); + if (isStatic) { + gen.push(false); + gen.putStatic(owner, tracker.name, Type.BOOLEAN_TYPE); + } else { + gen.loadThis(); + gen.push(false); + gen.putField(owner, tracker.name, Type.BOOLEAN_TYPE); + } + } + gen.newLabel(); + gen.loadArg(0); + gen.invokeVirtual(TypeConstants.CI_TYPE, new Method("cancel", "()V")); + gen.visitLabel(end); + gen.returnValue(); + gen.newLabel(); + gen.endMethod(); + canceller.maxStack = 2; + canceller.maxLocals = isStatic ? 1 : 2; + return canceller; + } + + private static String uniqueMethodName(ClassNode generatedTarget, String name) { + String unique = name; + for (int i = 1; hasMethodNamed(generatedTarget, unique); i++) { + unique = name + "$" + i; + } + return unique; + } + + private static boolean hasMethodNamed(ClassNode cls, String name) { + return cls.methods.stream().anyMatch(m -> m.name.equals(name)); + } + + private static boolean hasFieldNamed(ClassNode cls, String name) { + return cls.fields.stream().anyMatch(f -> f.name.equals(name)); + } + + private static String simpleName(ClassNode cls) { + return cls.name.substring(cls.name.lastIndexOf('/') + 1); + } + + private static void registerMoveRollback(MixinContext context, ClassNode generatedTarget, RelocationPlan.Move plan, List moved) { + ClassNode mixin = context.classNode(); + int[] fieldAccess = plan.widenedFields().stream().mapToInt(field -> field.access).toArray(); + int[] methodAccess = plan.widenedMethods().stream().mapToInt(method -> method.access).toArray(); + List lambdaCopies = plan.lambdas().stream().map(AdapterUtil::copyMethod).toList(); + context.patchContext().onRollback(() -> { + synchronized (generatedTarget) { + generatedTarget.methods.removeAll(moved); + } + for (int i = 0; i < fieldAccess.length; i++) { + plan.widenedFields().get(i).access = fieldAccess[i]; + } + for (int i = 0; i < methodAccess.length; i++) { + plan.widenedMethods().get(i).access = methodAccess[i]; + } + for (int i = 0; i < lambdaCopies.size(); i++) { + int index = mixin.methods.indexOf(plan.lambdas().get(i)); + if (index != -1) { + mixin.methods.set(index, lambdaCopies.get(i)); + } + } + }); + } + + private static ClassNode getGeneratedTarget(MixinContext context, String destinationClass) { + PatchEnvironment environment = context.environment(); + String parent = environment.dirtyClassLookup().getClass(destinationClass).map(cls -> cls.superName).orElse(null); + ClassNode generatedTarget = environment.classGenerator().getOrGenerateMixinClass(context.classNode(), destinationClass, parent); + environment.refmapHolder().copyEntries(context.classNode().name, generatedTarget.name); + return generatedTarget; + } + + private static void retargetLambdas(InvokeDynamicInsnNode indy, ClassNode mixin, List lambdas, ClassNode generatedTarget) { + for (int i = 0; i < indy.bsmArgs.length; i++) { + if (indy.bsmArgs[i] instanceof Handle handle && handle.getOwner().equals(mixin.name) + && lambdas.stream().anyMatch(lambda -> lambda.name.equals(handle.getName()) && lambda.desc.equals(handle.getDesc())) + ) { + indy.bsmArgs[i] = new Handle(handle.getTag(), generatedTarget.name, handle.getName(), handle.getDesc(), handle.isInterface()); + } + } + } + + private static int widenAccess(int access) { + int visibility = OpcodeUtil.getAccessVisibility(access); + if (visibility == Opcodes.ACC_PRIVATE || visibility == 0) { + return OpcodeUtil.setAccessVisibility(access, Opcodes.ACC_PUBLIC) | Opcodes.ACC_SYNTHETIC; + } + return access; + } + + private static void invokeHandler(GeneratorAdapter gen, MethodNode handler, ClassNode handlerOwner) { + boolean isInterface = (handlerOwner.access & Opcodes.ACC_INTERFACE) != 0; + int opcode = MethodAnalyzer.isStatic(handler) ? Opcodes.INVOKESTATIC : isInterface ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL; + gen.visitMethodInsn(opcode, handlerOwner.name, handler.name, handler.desc, isInterface); + } + + private static int load(GeneratorAdapter gen, ValueSource source, Map paramIndices) { + int arg = argIndex(source, paramIndices); + gen.loadArg(arg); + return gen.getArgumentTypes()[arg].getSize(); + } + + private static int argIndex(ValueSource source, Map paramIndices) { + return switch (source) { + case ValueSource.Forwarded(int param) -> param; + case ValueSource.Captured capture -> paramIndices.get(capture); + }; + } + + private static void copyParameterAnnotations(MethodNode handler, MethodNode mirror, List sources) { + int count = Type.getArgumentCount(mirror.desc); + mirror.visibleParameterAnnotations = copyParameterAnnotations(handler.visibleParameterAnnotations, sources, count); + mirror.invisibleParameterAnnotations = copyParameterAnnotations(handler.invisibleParameterAnnotations, sources, count); + } + + @SuppressWarnings("unchecked") + @Nullable + private static List[] copyParameterAnnotations(@Nullable List[] annotations, List sources, int count) { + if (annotations == null) { + return null; + } + List[] copy = new List[count]; + for (int i = 0; i < sources.size() && i < annotations.length; i++) { + if (sources.get(i) instanceof ValueSource.Forwarded(int param) && annotations[i] != null) { + copy[param] = new ArrayList<>(annotations[i]); + } + } + return copy; + } + + private RelocationGenerator() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationProcessor.java new file mode 100644 index 00000000..c23fef8f --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationProcessor.java @@ -0,0 +1,32 @@ +package org.sinytra.adapter.pipeline.processor.relocate; + +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.processor.Processor; + +public class RelocationProcessor implements Processor { + @Override + public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + String destinationClass = dirty.getTargetClass(); + if (recipe.clean().getTargetClass().equals(destinationClass)) return TxResult.PASS; + + switch (RelocationSolver.solve(context, recipe, dirty)) { + case RelocationSolver.Result.Solved(RelocationPlan.Move move) -> { + RelocationGenerator.generateMove(context, destinationClass, move); + context.recordCtxAudit("Move mixin to target %s", destinationClass); + } + case RelocationSolver.Result.Solved(RelocationPlan.Mirror mirror) -> { + RelocationGenerator.generateMirror(context, destinationClass, mirror); + context.recordCtxAudit("Mirror mixin to target %s", destinationClass); + } + case RelocationSolver.Result.Unbound(String reason) -> { + context.recordCtxAudit("Can't relocate mixin to target %s: %s", destinationClass, reason); + return TxResult.FAIL; + } + } + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationSolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationSolver.java new file mode 100644 index 00000000..d17237d9 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/relocate/RelocationSolver.java @@ -0,0 +1,195 @@ +package org.sinytra.adapter.pipeline.processor.relocate; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.Relocation; +import org.sinytra.adapter.model.RelocationPlan; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ValueSource; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.key.IntentKeys; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.param.Annotation; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.MixinBridge; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +public final class RelocationSolver { + public sealed interface Result { + record Solved(RelocationPlan plan) implements Result { + } + + record Unbound(String reason) implements Result { + } + } + + public static Result solve(MixinContext context, Recipe recipe, Configuration dirty) { + List params = Parameters.parse(context.methodNode()); + if (context.locals().capturesLocals()) { + return new Result.Unbound("the handler captures locals of the original target"); + } + + String destination = dirty.getTargetClass(); + RelocationPlan.Move move = findMove(context, recipe, destination); + if (move != null) { + return new Result.Solved(move); + } + + Relocation relocation = dirty.getProperty(IntentKeys.RELOCATION).orElse(null); + if (relocation == null) { + return new Result.Unbound("the handler uses its own class, and nothing at the new target holds its instance"); + } + TargetPair newTarget = recipe.getDirtyTarget(); + if (newTarget == null) { + return new Result.Unbound("the new target method doesn't exist"); + } + + ClassNode handlerOwner = context.dirtyLookup().getClass(recipe.clean().getTargetClass()).orElse(null); + if (handlerOwner == null) { + return new Result.Unbound("the original target class doesn't exist"); + } + boolean isStatic = MethodAnalyzer.isStatic(newTarget.methodNode()); + if (params.stream().anyMatch(Parameter::isShare)) { + return new Result.Unbound("the handler has @Share parameters, which the generated injector can't share"); + } + if (!selectsLocals(context, dirty, newTarget, params)) { + return new Result.Unbound("a @Local parameter selects nothing at the new target"); + } + + List forwarded = IntStream.range(0, params.size()).mapToObj(ValueSource.Forwarded::new).toList(); + if (context.isStatic()) { + return new Result.Solved(new RelocationPlan.Mirror(handlerOwner, isStatic, null, forwarded, null)); + } + ValueSource receiver = findReceiver(relocation, newTarget); + if (receiver == null) { + return new Result.Unbound("nothing at the new target holds the original instance"); + } + return new Result.Solved(new RelocationPlan.Mirror(handlerOwner, isStatic, receiver, forwarded, findGuard(context, dirty, relocation, handlerOwner))); + } + + /// {@return a guard that limits the mirror to calls from the original method, or null if it can't be limited} + /// Several original methods may now reach the new target, and the handler only ran for one of them. + @Nullable + private static RelocationPlan.Guard findGuard(MixinContext context, Configuration dirty, Relocation relocation, ClassNode handlerOwner) { + Relocation.OriginCall via = relocation instanceof Relocation.ReceivesOrigin receives ? receives.via() : null; + if (via == null) { + return null; + } + ClassNode origin = via.method().classNode(); + boolean reachable = handlerOwner.name.equals(origin.name) + || context.environment().inheritanceHandler(context.dirtyLookup()).isClassInherited(handlerOwner.name, origin.name); + if (!reachable || (origin.access & Opcodes.ACC_INTERFACE) != 0 || MethodAnalyzer.isStatic(via.method().methodNode()) + || !via.method().methodNode().instructions.contains(via.call()) + ) { + return null; + } + RelocationPlan.Passthrough passthrough = context.getMixinType().passthrough(context, dirty); + return passthrough != null ? new RelocationPlan.Guard(via, passthrough) : null; + } + + @Nullable + private static RelocationPlan.Move findMove(MixinContext context, Recipe recipe, String destination) { + ClassNode mixin = context.classNode(); + MethodNode handler = context.methodNode(); + TargetPair cleanTarget = recipe.getCleanTarget(); + String origin = cleanTarget != null ? cleanTarget.classNode().name : destination; + boolean inherited = context.environment().inheritanceHandler(context.dirtyLookup()).isClassInherited(destination, origin); + TargetPair newTarget = recipe.getDirtyTarget(); + boolean staticTarget = newTarget != null && MethodAnalyzer.isStatic(newTarget.methodNode()); + + List lambdas = MethodAnalyzer.findOwnIndyHandles(mixin, handler).stream() + .filter(lambda -> lambda.handle().getName().startsWith(MethodAnalyzer.LAMBDA_PREFIX + handler.name)) + .map(MethodAnalyzer.IndyHandle::method) + .toList(); + List moved = new ArrayList<>(lambdas); + moved.addFirst(handler); + + List widenedFields = new ArrayList<>(); + List widenedMethods = new ArrayList<>(); + for (MethodNode method : moved) { + boolean ownInstance = (!inherited || staticTarget) && !MethodAnalyzer.isStatic(method); + for (AbstractInsnNode insn : method.instructions) { + // The moved method's instance becomes the destination's, which isn't the origin class's, or none at a static target + if (ownInstance && insn instanceof VarInsnNode varInsn && varInsn.getOpcode() == Opcodes.ALOAD && varInsn.var == 0) { + return null; + } + if (insn instanceof FieldInsnNode fieldInsn && fieldInsn.owner.equals(mixin.name)) { + FieldNode field = AdapterUtil.findOne(mixin.fields, f -> f.name.equals(fieldInsn.name)).orElse(null); + if (field == null || !isAvailable(field.visibleAnnotations, inherited)) { + return null; + } + addWidened(widenedFields, field, field.visibleAnnotations); + } else if (insn instanceof MethodInsnNode methodInsn && methodInsn.owner.equals(mixin.name)) { + MethodNode member = AdapterUtil.findMethod(mixin, methodInsn.name, methodInsn.desc); + if (member == null || !isAvailable(member.visibleAnnotations, inherited)) { + return null; + } + addWidened(widenedMethods, member, member.visibleAnnotations); + } + } + } + return new RelocationPlan.Move(lambdas, widenedFields, widenedMethods, staticTarget && !context.isStatic()); + } + + private static boolean isAvailable(@Nullable List annotations, boolean inherited) { + return inherited || AdapterUtil.hasAnnotation(annotations, MixinAnnotations.SHADOW); + } + + private static void addWidened(List widened, T member, @Nullable List annotations) { + if (!AdapterUtil.hasAnnotation(annotations, MixinAnnotations.SHADOW) && !widened.contains(member)) { + widened.add(member); + } + } + + private static boolean selectsLocals(MixinContext context, Configuration dirty, TargetPair newTarget, List params) { + if (params.stream().noneMatch(Parameter::isLocal)) { + return true; + } + AtData at = dirty.getAtData(); + AbstractInsnNode insn = at != null ? context.injections().resolveFirst(newTarget, InjectionQuery.of(at)) : null; + if (insn == null) { + return false; + } + MixinBridge.Session session = MixinBridge.session(context.classNode().name, context.methodNode(), newTarget.classNode(), newTarget.methodNode(), context.environment()); + for (Parameter param : params) { + for (Annotation annotation : param.annotations()) { + if (annotation.getDesc().equals(MixinAnnotations.LOCAL) && session.findLocal(annotation.toAnnotationNode(), param.type(), insn) == -1) { + return false; + } + } + } + return true; + } + + @Nullable + private static ValueSource findReceiver(Relocation relocation, TargetPair newTarget) { + if (relocation instanceof Relocation.ReceivesOrigin receives) { + int argIndex = receives.argIndex(); + Type[] args = Type.getArgumentTypes(newTarget.methodNode().desc); + if (argIndex >= args.length) { + return null; + } + int slot = MethodAnalyzer.isStatic(newTarget.methodNode()) ? 0 : 1; + for (int i = 0; i < argIndex; i++) { + slot += args[i].getSize(); + } + return new ValueSource.Captured(args[argIndex], slot, true); + } + return null; + } + + private RelocationSolver() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpParamsProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/OperationCallStep.java similarity index 54% rename from core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpParamsProcessor.java rename to core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/OperationCallStep.java index 8f2bca3d..d5989ab3 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/wrapop/WrapOpParamsProcessor.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/OperationCallStep.java @@ -1,30 +1,31 @@ -package org.sinytra.adapter.patch.processor.wrapop; +package org.sinytra.adapter.pipeline.processor.signature; import com.mojang.logging.LogUtils; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.injection.InjectionQuery; import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.Parameter; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.processor.Processor; +import org.sinytra.adapter.analysis.method.WrapOpOriginalCall; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.processor.WrapOpSurgeon; import org.sinytra.adapter.util.AdapterUtil; import org.slf4j.Logger; import java.util.ArrayList; import java.util.List; -import java.util.stream.IntStream; import static org.sinytra.adapter.util.AdapterUtil.MIXINPATCH; -public class WrapOpParamsProcessor implements Processor { +public class OperationCallStep implements SignatureStep { private static final Logger LOGGER = LogUtils.getLogger(); @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + public TxResult apply(MixinContext context, Configuration dirty, Recipe recipe) { if (dirty.getParameters() == null) return TxResult.FAIL; // Find original.call(...) @@ -35,17 +36,15 @@ public TxResult process(MixinContext context, Configuration dirty, Recipe recipe } for (int i = 0; i < dirtyCalls.size(); i++) { - List cleanCall = cleanCalls.get(i); - List dirtyCall = dirtyCalls.get(i); - if (!upgradeOriginalCall(context, dirty, cleanCall, dirtyCall)) { + if (!upgradeOriginalCall(context, dirty, cleanCalls.get(i), dirtyCalls.get(i))) { return TxResult.FAIL; } } if (dirty.getAtData() != null) { // Upgrade calls on "instance" variable - AbstractInsnNode cleanInsn = context.methods().findInjectionTargetInsn(recipe.getCleanTarget()); - AbstractInsnNode dirtyInsn = context.methods().findInjectionTargetInsn(recipe.getDirtyTarget(), dirty.getAtData()); + AbstractInsnNode cleanInsn = context.injections().resolve(recipe.getCleanTarget()); + AbstractInsnNode dirtyInsn = context.injections().resolve(recipe.getDirtyTarget(), InjectionQuery.of(dirty.getAtData()).keepingShift()); if (cleanInsn instanceof MethodInsnNode cleanMinsn && dirtyInsn instanceof MethodInsnNode dirtyMinsn) { List methodParams = dirty.getParameters().getTypes(MethodParameters.ParamGroup.METHOD_PARAMS); if (!WrapOpSurgeon.tryUpgrade(context, recipe, methodParams, cleanMinsn, dirtyMinsn)) { @@ -58,40 +57,31 @@ public TxResult process(MixinContext context, Configuration dirty, Recipe recipe } private static boolean upgradeOriginalCall(MixinContext context, Configuration dirty, List cleanCallInsns, List dirtyCallInsns) { - WrapOpOriginalCall cleanCall = WrapOpOriginalCall.parse(context.unmodifiedMethodNode(), cleanCallInsns); - if (cleanCall == null) return false; - - WrapOpOriginalCall dirtyCall = WrapOpOriginalCall.parse(context.methodNode(), dirtyCallInsns); - if (dirtyCall == null) return false; - - AbstractInsnNode arrayInit = dirtyCallInsns.get(1); - List params = dirty.getParameters().get(MethodParameters.ParamGroup.METHOD_PARAMS); - - LocalVariableLookup lookup = context.methods().getLVT(context.methodNode()); - if (lookup == null || IntStream.range(0, params.size()).anyMatch(i -> lookup.getByParameterOrdinalOrNull(i) == null)) { + LocalVariableLookup lookup = LocalVariableLookup.of(context.methodNode()); + if (lookup == null || !(dirtyCallInsns.getLast() instanceof MethodInsnNode dirtyInvoke) + || WrapOpOriginalCall.parse(context.unmodifiedMethodNode(), cleanCallInsns) == null + || WrapOpOriginalCall.parse(context.methodNode(), dirtyCallInsns) == null) { return false; } - List args = IntStream.range(0, params.size()) - .mapToObj(i -> { - Parameter param = params.get(i); - int index = lookup.getByParameterOrdinalOrNull(i).index; - Type type = param.type(); - - List loadInsns = new ArrayList<>(); - loadInsns.add(AdapterUtil.loadType(type, index)); - if (type.getSort() < Type.ARRAY) { - loadInsns.add(AdapterUtil.box(type)); - } - - return WrapOpOriginalCall.CallArg.create(i, loadInsns); - }) - .toList(); + List params = dirty.getParameters().get(MethodParameters.ParamGroup.METHOD_PARAMS); + List args = new ArrayList<>(); + for (int i = 0; i < params.size(); i++) { + LocalVariableNode local = lookup.getByParameterOrdinalOrNull(i); + if (local == null) { + return false; + } + Type type = params.get(i).type(); + List loadInsns = new ArrayList<>(List.of(AdapterUtil.loadType(type, local.index))); + if (type.getSort() < Type.ARRAY) { + loadInsns.add(AdapterUtil.box(type)); + } + args.add(WrapOpOriginalCall.CallArg.create(i, loadInsns)); + } WrapOpOriginalCall reconstruct = new WrapOpOriginalCall(args); + AbstractInsnNode arrayInit = dirtyCallInsns.get(1); AbstractInsnNode start = arrayInit.getNext(); // After ANEWARRAY - AbstractInsnNode dirtyInvoke = dirtyCallInsns.getLast(); - if (!(dirtyInvoke instanceof MethodInsnNode)) return false; AbstractInsnNode end = dirtyInvoke.getPrevious(); // Before INVOKEVIRTUAL call() InsnList methodInsns = context.methodNode().instructions; diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ParameterCastStep.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ParameterCastStep.java new file mode 100644 index 00000000..b2fc7e5c --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ParameterCastStep.java @@ -0,0 +1,87 @@ +package org.sinytra.adapter.pipeline.processor.signature; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.analysis.params.ParamsEditDistance; +import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; +import org.sinytra.adapter.edit.param.ReplaceParametersTransformer; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.MethodParameters.ParamGroup; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * If a mixin has its parameter changed to a higher class e.g. ServerPlayer -> LivingEntity but + * uses it to call a method that only accepts a ServerPlayer, we have to cast it back. + *

+ * Before: + *

{@code
+ *     private void modify(ServerLevel world, ServerPlayer player, CallbackInfo ci) {
+ *         trigger(player, stack, getDamageValue());
+ *     }
+ * }
+ *

+ * After: + *

{@code
+ *     private void modify(ServerLevel world, LivingEntity player, CallbackInfo ci) {
+ *         trigger((ServerPlayer)player, stack, getDamageValue());
+ * //              ^^^^^^^^^^^^^^ Added cast after param change 
+ *     }
+ * }
+ */ +// TODO Add type safety check +public class ParameterCastStep implements SignatureStep { + private record Cast(VarInsnNode load, Type type) { + } + + @Override + public TxResult apply(MixinContext context, Configuration dirty, Recipe recipe) { + MethodParameters cleanParams = recipe.clean().getParameters(); + MethodParameters dirtyParams = dirty.getParameters(); + MethodNode methodNode = context.methodNode(); + LocalVariableLookup lookup = LocalVariableLookup.of(methodNode); + if (dirtyParams == null || lookup == null || !cleanParams.has(ParamGroup.METHOD_PARAMS) || !dirtyParams.has(ParamGroup.METHOD_PARAMS)) { + return TxResult.PASS; + } + + Map replaced = new HashMap<>(); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(cleanParams.getTypes(ParamGroup.METHOD_PARAMS), dirtyParams.getTypes(ParamGroup.METHOD_PARAMS), + context.typeRelation()); + for (ReplaceParametersTransformer replacement : diff.replacements()) { + LocalVariableNode node = lookup.getByParameterOrdinalOrNull(replacement.index()); + if (node != null) { + replaced.put(node.index, Type.getType(node.desc)); + } + } + + List casts = new ArrayList<>(); + DataFlow flow = DataFlow.of(methodNode); + for (AbstractInsnNode insn : methodNode.instructions) { + List operands = insn instanceof MethodInsnNode call ? flow.resolveCallOperandProducers(call) : null; + if (operands == null) continue; + + MethodInsnNode call = (MethodInsnNode) insn; + List args = Parameters.getParameterTypes(call.desc); + int instanceOffset = call.getOpcode() == Opcodes.INVOKESTATIC ? 0 : 1; + for (int i = instanceOffset; i < operands.size(); i++) { + Type expected = args.get(i - instanceOffset); + if (operands.get(i) instanceof VarInsnNode load && replaced.containsKey(load.var) && !replaced.get(load.var).equals(expected)) { + casts.add(new Cast(load, expected)); + } + } + } + casts.forEach(cast -> methodNode.instructions.insert(cast.load(), new TypeInsnNode(Opcodes.CHECKCAST, cast.type().getInternalName()))); + return casts.isEmpty() ? TxResult.PASS : TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ParametersStep.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ParametersStep.java new file mode 100644 index 00000000..87c66be0 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ParametersStep.java @@ -0,0 +1,55 @@ +package org.sinytra.adapter.pipeline.processor.signature; + +import com.mojang.datafixers.util.Pair; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.sinytra.adapter.analysis.params.ParamsEditDistance; +import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.edit.param.ParamTransformationUtil; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; +import java.util.Map; + +public class ParametersStep implements SignatureStep { + @Override + public TxResult apply(MixinContext context, Configuration dirty, Recipe recipe) { + MethodParameters cleanParams = recipe.clean().getParameters(); + MethodParameters dirtyParams = dirty.getParameters(); + if (dirtyParams == null) return TxResult.FAIL; + if (cleanParams.merge().equals(dirtyParams.merge())) return TxResult.PASS; + + // Apply mappings of params that will be removed in dirty + MethodNode method = context.methodNode(); + Map> oldVarMap = ParamTransformationUtil.gatherVarMappings(method, cleanParams.merge(), dirtyParams.merge(), dirtyParams.getMapping()); + + if (cleanParams.getOrder().equals(dirtyParams.getOrder())) { + int offset = 0; + for (MethodParameters.ParamGroup group : dirtyParams.getOrder()) { + if (!applyDiff(cleanParams.getTypes(group), dirtyParams.getTypes(group), context, offset)) { + return TxResult.FAIL; + } + offset += dirtyParams.getTypes(group).size(); + } + } else if (!applyDiff(cleanParams.mergeTypes(), dirtyParams.mergeTypes(), context, 0)) { + return TxResult.FAIL; + } + + // Apply mappings of params that only exist in dirty + ParamTransformationUtil.applyVarMappings(method, ParamTransformationUtil.gatherVarMappings(method, dirtyParams.merge(), dirtyParams.merge(), dirtyParams.getMapping())); + ParamTransformationUtil.applyVarMappings(method, oldVarMap); + ParamTransformationUtil.applyAnnotations(method, dirtyParams.merge()); + return TxResult.SUCCESS; + } + + private static boolean applyDiff(List clean, List dirty, MixinContext context, int offset) { + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(clean, dirty, context.typeRelation()); + return diff.isEmpty() || diff.offset(offset).asParameterTransformer(false).apply(context) != PatchResult.PASS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/processor/redirect/ParameterUsageProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/RedirectedCallStep.java similarity index 57% rename from core/src/main/java/org/sinytra/adapter/patch/processor/redirect/ParameterUsageProcessor.java rename to core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/RedirectedCallStep.java index c164f339..4b02b59a 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/processor/redirect/ParameterUsageProcessor.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/RedirectedCallStep.java @@ -1,36 +1,30 @@ -package org.sinytra.adapter.patch.processor.redirect; +package org.sinytra.adapter.pipeline.processor.signature; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodInsnNode; +import org.sinytra.adapter.analysis.flow.DataFlow; import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; +import org.sinytra.adapter.analysis.params.ParamsEditDistance; import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.param.MethodParameters; -import org.sinytra.adapter.env.param.MethodParameters.ParamGroup; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.processor.Processor; +import org.sinytra.adapter.edit.param.InjectParameterTransform; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.MethodParameters.ParamGroup; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; -import org.sinytra.adapter.transform.param.InjectParameterTransform; -import org.sinytra.adapter.analysis.method.MethodAnalyzer; import java.util.List; -public class ParameterUsageProcessor implements Processor { +public class RedirectedCallStep implements SignatureStep { @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - String cleanTarget = recipe.clean().getAtData().getTarget().orElse(null); - if (cleanTarget == null) return TxResult.PASS; - String dirtyTarget = dirty.getAtData().getTarget().orElse(null); - if (dirtyTarget == null) return TxResult.PASS; - - MethodQualifier cleanTargetQual = MethodQualifier.parse(cleanTarget).orElse(null); - MethodQualifier dirtyTargetQual = MethodQualifier.parse(dirtyTarget).orElse(null); + public TxResult apply(MixinContext context, Configuration dirty, Recipe recipe) { + MethodQualifier cleanTargetQual = recipe.clean().getAtData().getTargetQualifier().orElse(null); + MethodQualifier dirtyTargetQual = dirty.getAtData().getTargetQualifier().orElse(null); if (cleanTargetQual == null || dirtyTargetQual == null) return TxResult.PASS; MethodParameters dirtyMethodParams = dirty.getParameters(); @@ -38,15 +32,14 @@ public TxResult process(MixinContext context, Configuration dirty, Recipe recipe List cleanParams = recipe.clean().getParameters().getTypes(ParamGroup.METHOD_PARAMS); List dirtyParams = dirtyMethodParams.getTypes(ParamGroup.METHOD_PARAMS); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(cleanParams, dirtyParams); - if (diff == null) return TxResult.PASS; + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(cleanParams, dirtyParams, context.typeRelation()); - List> callInsns = MethodAnalyzer.getAllMethodCallSrcInsnsInclusive(context.methodNode(), cleanTargetQual); + List> callInsns = DataFlow.of(context.methodNode(), DataFlow.Producers.COMPUTER).findCallsWithProducers(cleanTargetQual); if (callInsns.isEmpty()) { return TxResult.SUCCESS; } - LocalVariableLookup lookup = context.methods().getLVT(context.methodNode()); + LocalVariableLookup lookup = LocalVariableLookup.of(context.methodNode()); if (lookup == null || diff.insertions().stream().anyMatch(i -> lookup.getByParameterOrdinalOrNull(i.index()) == null)) { return TxResult.PASS; } diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ReturnTypeStep.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ReturnTypeStep.java new file mode 100644 index 00000000..5e4a6e68 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/ReturnTypeStep.java @@ -0,0 +1,41 @@ +package org.sinytra.adapter.pipeline.processor.signature; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.flow.DataFlow; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + + +public class ReturnTypeStep implements SignatureStep { + @Override + public TxResult apply(MixinContext context, Configuration dirty, Recipe recipe) { + Type cleanType = recipe.clean().getReturnType(); + Type dirtyType = dirty.getReturnType(); + + if (cleanType.equals(dirtyType)) { + return TxResult.PASS; + } + + TypeAdapter adapter = context.getTypeAdapter(cleanType, dirtyType); + if (adapter == null) { + return TxResult.PASS; + } + + MethodNode methodNode = context.methodNode(); + for (AbstractInsnNode insn : DataFlow.of(methodNode).findReturnedValues()) { + if (insn.getOpcode() != Opcodes.ACONST_NULL) { + adapter.apply(methodNode.instructions, insn); + } + } + + methodNode.desc = Type.getMethodDescriptor(dirtyType, Type.getArgumentTypes(methodNode.desc)); + + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/SignatureProcessor.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/SignatureProcessor.java new file mode 100644 index 00000000..4c2c3475 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/SignatureProcessor.java @@ -0,0 +1,52 @@ +package org.sinytra.adapter.pipeline.processor.signature; + +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.param.HandlerSignature; +import org.sinytra.adapter.model.param.HandlerSignature.ParamSource; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.processor.Processor; + +import java.util.ArrayList; +import java.util.List; + +public class SignatureProcessor implements Processor { + private static final SignatureStep PARAMETERS = new ParametersStep(); + private static final SignatureStep OPERATION_CALLS = new OperationCallStep(); + private static final SignatureStep REDIRECTED_CALLS = new RedirectedCallStep(); + private static final SignatureStep CASTS = new ParameterCastStep(); + private static final SignatureStep RETURN_TYPE = new ReturnTypeStep(); + private static final SignatureStep STATIC_ACCESS = new StaticAccessStep(); + + @Override + public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { + TxResult result = TxResult.PASS; + for (SignatureStep step : steps(context.getMixinType().signature())) { + TxResult stepResult = context.audited(step, () -> step.apply(context, dirty, recipe)); + if (stepResult == TxResult.FAIL || stepResult == TxResult.FINALIZE) { + return stepResult; + } + if (stepResult == TxResult.SUCCESS) { + result = TxResult.SUCCESS; + } + } + return result; + } + + private List steps(HandlerSignature signature) { + List steps = new ArrayList<>(); + steps.add(PARAMETERS); + if (hasSource(signature, ParamSource.CALL_OPERANDS)) { + steps.add(hasSource(signature, ParamSource.OPERATION) ? OPERATION_CALLS : REDIRECTED_CALLS); + } + steps.add(CASTS); + steps.add(RETURN_TYPE); + steps.add(STATIC_ACCESS); + return steps; + } + + private static boolean hasSource(HandlerSignature signature, ParamSource source) { + return signature.slots().stream().anyMatch(slot -> slot.source() == source); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/SignatureStep.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/SignatureStep.java new file mode 100644 index 00000000..187d0b98 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/SignatureStep.java @@ -0,0 +1,10 @@ +package org.sinytra.adapter.pipeline.processor.signature; + +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +public interface SignatureStep { + TxResult apply(MixinContext context, Configuration dirty, Recipe recipe); +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/StaticAccessStep.java b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/StaticAccessStep.java new file mode 100644 index 00000000..a2d91226 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/processor/signature/StaticAccessStep.java @@ -0,0 +1,60 @@ +package org.sinytra.adapter.pipeline.processor.signature; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.key.ControlKeys; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.TxResult; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.AdapterUtil; + +import java.util.List; +import java.util.stream.StreamSupport; + +public class StaticAccessStep implements SignatureStep { + @Override + public TxResult apply(MixinContext context, Configuration dirty, Recipe recipe) { + Boolean cleanStatic = recipe.clean().getProperty(ControlKeys.STATIC).orElse(null); + Boolean dirtyStatic = dirty.getProperty(ControlKeys.STATIC).orElse(null); + if (cleanStatic == null || dirtyStatic == null || cleanStatic.equals(dirtyStatic)) { + return TxResult.PASS; + } + + MethodNode method = context.methodNode(); + boolean actuallyStatic = MethodAnalyzer.isStatic(method); + if (dirtyStatic && !actuallyStatic) { + // Mixin types add the target instance as the first parameter, which replaces usages of "this" + Type[] args = Type.getArgumentTypes(method.desc); + List targetTypes = context.targetTypes(); + boolean hasInstanceParam = args.length > 0 && targetTypes.size() == 1 && args[0].equals(targetTypes.getFirst()); + boolean usesThis = StreamSupport.stream(method.instructions.spliterator(), false) + .map(AdapterUtil::handleLocalVarInsnValue) + .anyMatch(handle -> handle != null && handle.get() == 0); + if (usesThis && !hasInstanceParam) { + return TxResult.FAIL; + } + + context.recordCtxAudit("Made method static"); + AdapterUtil.makeStatic(method); + return TxResult.SUCCESS; + } + if (!dirtyStatic && actuallyStatic) { + context.recordCtxAudit("Made method non-static"); + method.access &= ~Opcodes.ACC_STATIC; + LocalVariableNode firstParam = method.localVariables == null ? null + : method.localVariables.stream().filter(lvn -> lvn.index == 0).findFirst().orElse(null); + AdapterUtil.shiftLocals(method, 1); + if (firstParam != null) { + method.localVariables.add(new LocalVariableNode("this", Type.getObjectType(context.classNode().name).getDescriptor(), null, firstParam.start, firstParam.end, 0)); + } + // TODO Must compute frames + return TxResult.SUCCESS; + } + return TxResult.PASS; + } + +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/InjectionPointResolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/InjectionPointResolver.java new file mode 100644 index 00000000..31ecfa28 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/InjectionPointResolver.java @@ -0,0 +1,117 @@ +package org.sinytra.adapter.pipeline.resolver; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.locate.Candidate; +import org.sinytra.adapter.pipeline.locate.Describer; +import org.sinytra.adapter.pipeline.locate.LocalLocator; +import org.sinytra.adapter.pipeline.locate.LocatorChain; +import org.sinytra.adapter.pipeline.locate.ReplacedCallLocator; +import org.sinytra.adapter.pipeline.locate.SameMethodLocator; +import org.sinytra.adapter.pipeline.locate.Scope; +import org.sinytra.adapter.pipeline.locate.ScopeSearch; +import org.sinytra.adapter.pipeline.locate.SelectedLocator; + +import java.util.ArrayList; +import java.util.List; + +public class InjectionPointResolver implements Resolver { + private final List chains = new ArrayList<>(); + + public InjectionPointResolver() { + addChain(new LocatorChain(List.of(new SameMethodLocator(), new SelectedLocator(), new ReplacedCallLocator()), ScopeSearch.Reach.CALLS)); + } + + public InjectionPointResolver addChain(LocatorChain chain) { + this.chains.add(chain); + return this; + } + + @Override + public ResolutionResult resolve(MixinContext context, Recipe recipe) { + if (recipe.dirty().getAtData() != null) { + return ResolutionResult.pass(); + } + + Configuration reused = tryReuse(context, recipe); + if (reused != null) { + return ResolutionResult.success(reused); + } + + for (LocatorChain chain : this.chains) { + Configuration result = context.audited(chain, () -> chain.resolve(context, recipe)); + if (result != null) { + return ResolutionResult.success(result); + } + } + return ResolutionResult.fail(); + } + + @Nullable + private static Configuration tryReuse(MixinContext context, Recipe recipe) { + Configuration dirty = recipe.dirty(); + + String targetClass = dirty.getTargetClass(); + if (targetClass == null) return null; + + MethodQualifier targetQualifier = dirty.getTargetMethod(); + if (targetQualifier == null) return null; + + TargetPair dirtyTarget = context.dirtyMethods().resolveDeclared(targetQualifier.withOwner(Type.getObjectType(targetClass))); + if (dirtyTarget == null) return null; + + // Try reusing the original + List insns = context.injections().find(dirtyTarget); + TargetPair cleanTarget = recipe.getCleanTarget(); + if (!insns.isEmpty() && (cleanTarget == null || selectsLocatedInsns(context, cleanTarget, dirtyTarget, insns)) + && selectsSameLocal(context, recipe, dirtyTarget, insns.getFirst()) + ) { + return dirty.copyClean() + .inheritAtData() + .inheritProperty(MixinKeys.TARGET_CONSTANT); + } + + return null; + } + + private static boolean selectsSameLocal(MixinContext context, Recipe recipe, TargetPair dirtyTarget, AbstractInsnNode dirtyInsn) { + TargetPair cleanTarget = recipe.getCleanTarget(); + AtData cleanAt = recipe.clean().getAtData(); + if (cleanTarget == null || cleanAt == null || !Describer.isLocalAt(context, cleanAt)) { + return true; + } + AbstractInsnNode cleanInsn = context.injections().resolveFirst(cleanTarget); + if (cleanInsn == null) { + return true; + } + Type type = Type.getReturnType(context.methodNode().desc); + int cleanSlot = context.locals().resolveSelectedLocal(cleanTarget, cleanInsn, type); + int expected = cleanSlot != -1 ? LocalLocator.resolve(context, recipe, cleanTarget, cleanSlot, cleanInsn, dirtyTarget) : -1; + return expected == -1 || context.locals().resolveSelectedLocal(dirtyTarget, dirtyInsn, type) == expected; + } + + public static boolean selectsLocatedInsns(MixinContext context, TargetPair cleanTarget, TargetPair dirtyTarget, List dirtyInsns) { + if (!cleanTarget.methodNode().name.equals(dirtyTarget.methodNode().name) + || !cleanTarget.methodNode().desc.equals(dirtyTarget.methodNode().desc) + ) { + return true; + } + Scope scope = Scope.of(new Targets(cleanTarget, dirtyTarget)); + for (AbstractInsnNode cleanInsn : context.injections().find(cleanTarget)) { + List candidates = SameMethodLocator.find(context, scope, cleanInsn); + if (!candidates.isEmpty() && candidates.stream().noneMatch(c -> dirtyInsns.contains(c.insn()))) { + return false; + } + } + return true; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/LocalResolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/LocalResolver.java new file mode 100644 index 00000000..733e2ae2 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/LocalResolver.java @@ -0,0 +1,55 @@ +package org.sinytra.adapter.pipeline.resolver; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.locate.Describer; +import org.sinytra.adapter.pipeline.locate.LocalLocator; + +public final class LocalResolver implements Resolver { + @Override + public ResolutionResult resolve(MixinContext context, Recipe recipe) { + Configuration dirty = recipe.dirty(); + AtData at = dirty.getAtData(); + if (at == null || !recipe.clean().hasProperty(MixinKeys.ORDINAL) && !recipe.clean().hasProperty(MixinKeys.INDEX) + || dirty.hasProperty(MixinKeys.ORDINAL) || dirty.hasProperty(MixinKeys.INDEX) + ) { + return ResolutionResult.pass(); + } + + TargetPair cleanTarget = recipe.getCleanTarget(); + TargetPair dirtyTarget = recipe.getDirtyTarget(); + if (cleanTarget == null || dirtyTarget == null) { + return ResolutionResult.pass(); + } + AbstractInsnNode cleanInsn = context.injections().resolveFirst(cleanTarget); + AbstractInsnNode dirtyInsn = context.injections().resolveFirst(dirtyTarget, InjectionQuery.of(at)); + if (cleanInsn == null || dirtyInsn == null) { + return ResolutionResult.pass(); + } + + Type type = Type.getReturnType(context.methodNode().desc); + int cleanSlot = context.locals().resolveSelectedLocal(cleanTarget, cleanInsn, type); + if (cleanSlot < 0) { + return ResolutionResult.pass(); + } + int dirtySlot = LocalLocator.resolve(context, recipe, cleanTarget, cleanSlot, cleanInsn, dirtyTarget); + if (dirtySlot < 0 || context.locals().resolveSelectedLocal(dirtyTarget, dirtyInsn, type) == dirtySlot) { + return ResolutionResult.pass(); + } + + MutableConfiguration local = Describer.describeLocal(context, recipe, dirtyTarget, dirtyInsn, dirtySlot); + if (local == null) { + return ResolutionResult.pass(); + } + context.recordCtxAudit("Select local %s instead of %s", dirtySlot, cleanSlot); + return ResolutionResult.success(local); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/Resolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/Resolver.java similarity index 84% rename from core/src/main/java/org/sinytra/adapter/patch/resolver/Resolver.java rename to core/src/main/java/org/sinytra/adapter/pipeline/resolver/Resolver.java index 36b4a174..2a80ebeb 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/Resolver.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/Resolver.java @@ -1,10 +1,10 @@ -package org.sinytra.adapter.patch.resolver; +package org.sinytra.adapter.pipeline.resolver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.config.Configuration; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; import java.util.Optional; diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/Resolvers.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/Resolvers.java similarity index 57% rename from core/src/main/java/org/sinytra/adapter/patch/resolver/Resolvers.java rename to core/src/main/java/org/sinytra/adapter/pipeline/resolver/Resolvers.java index 05a95668..a39acb76 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/Resolvers.java +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/Resolvers.java @@ -1,9 +1,6 @@ -package org.sinytra.adapter.patch.resolver; +package org.sinytra.adapter.pipeline.resolver; -import org.sinytra.adapter.env.util.OrderedRegistry; -import org.sinytra.adapter.patch.resolver.injection.InjectionPointResolver; -import org.sinytra.adapter.patch.resolver.special.SliceBoundaryResolver; -import org.sinytra.adapter.patch.resolver.target.TargetMethodResolver; +import org.sinytra.adapter.util.OrderedRegistry; public class Resolvers extends OrderedRegistry { diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/SliceBoundaryResolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/SliceBoundaryResolver.java new file mode 100644 index 00000000..24dba56f --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/SliceBoundaryResolver.java @@ -0,0 +1,81 @@ +package org.sinytra.adapter.pipeline.resolver; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.injection.InjectionQuery; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.ann.SliceData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.locate.Describer; +import org.sinytra.adapter.pipeline.locate.LocatorChain; +import org.sinytra.adapter.pipeline.locate.Locator; +import org.sinytra.adapter.pipeline.locate.ReplacedCallLocator; +import org.sinytra.adapter.pipeline.locate.SameMethodLocator; +import org.sinytra.adapter.pipeline.locate.Scope; +import org.sinytra.adapter.pipeline.locate.ScopeSearch; + +import java.util.ArrayList; +import java.util.List; + +import static org.sinytra.adapter.model.config.key.MixinKeys.SLICE; +import static org.sinytra.adapter.model.config.key.MixinKeys.SLICES; + +public class SliceBoundaryResolver implements Resolver { + private static final List LOCATORS = List.of(new SameMethodLocator(), new ReplacedCallLocator()); + + @Override + public ResolutionResult resolve(MixinContext context, Recipe recipe) { + Targets targets = recipe.targets(); + if (targets == null) return ResolutionResult.pass(); + + SliceData slice = recipe.clean().getProperty(SLICE).orElse(null); + if (slice != null) { + SliceData fixed = fixSlice(context, recipe, targets, slice); + return fixed != null && fixed != slice ? ResolutionResult.success(MutableConfiguration.create().setProperty(SLICE, fixed)) : ResolutionResult.pass(); + } + + List slices = recipe.clean().getProperty(SLICES).orElse(null); + if (slices == null || slices.isEmpty()) return ResolutionResult.pass(); + + List fixedSlices = new ArrayList<>(); + boolean changed = false; + for (SliceData s : slices) { + SliceData fixed = fixSlice(context, recipe, targets, s); + if (fixed == null) return ResolutionResult.pass(); + fixedSlices.add(fixed); + changed |= fixed != s; + } + return changed ? ResolutionResult.success(MutableConfiguration.create().setProperty(SLICES, fixedSlices)) : ResolutionResult.pass(); + } + + @Nullable + private static SliceData fixSlice(MixinContext context, Recipe recipe, Targets targets, SliceData slice) { + AtData from = fixBoundary(context, recipe, targets, slice.from()); + AtData to = fixBoundary(context, recipe, targets, slice.to()); + if (slice.from() != null && from == null || slice.to() != null && to == null) { + return null; + } + return from == slice.from() && to == slice.to() ? slice : slice.withBoundaries(from, to); + } + + @Nullable + private static AtData fixBoundary(MixinContext context, Recipe recipe, Targets targets, @Nullable AtData boundary) { + if (boundary == null || context.injections().matches(targets.dirty(), InjectionQuery.sliceBoundary(boundary))) { + return boundary; + } + + List cleanInsns = context.injections().find(targets.clean(), InjectionQuery.of(boundary).ignoringSlice()); + Configuration located = LocatorChain.locateAll(context, recipe, ScopeSearch.of(context, Scope.of(targets)), cleanInsns, LOCATORS, candidate -> { + if (candidate.target() != targets.dirty()) { + return null; + } + AtData at = Describer.describeAt(context, boundary, candidate, a -> InjectionQuery.of(a).ignoringSlice()); + return at != null ? MutableConfiguration.create().setAtData(at) : null; + }); + return located != null ? located.getAtData() : null; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/TargetMethodResolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/TargetMethodResolver.java new file mode 100644 index 00000000..70f4dacd --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/TargetMethodResolver.java @@ -0,0 +1,37 @@ +package org.sinytra.adapter.pipeline.resolver; + +import org.sinytra.adapter.model.MethodQualifier; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.locate.OriginLocator; + +import java.util.List; + +public class TargetMethodResolver implements Resolver { + @Override + public ResolutionResult resolve(MixinContext context, Recipe recipe) { + if (recipe.dirty().getTargetMethod() != null) { + return ResolutionResult.pass(); + } + if (recipe.getNewCleanTarget() != null) { + return ResolutionResult.success(recipe.dirty().copyClean().inheritTargetMethod()); + } + + TargetPair cleanTarget = recipe.getCleanTarget(); + MethodQualifier cleanQualifier = recipe.clean().getTargetMethod(); + List origins = cleanTarget != null && cleanQualifier != null + ? OriginLocator.locate(context, cleanTarget, recipe.clean().getTargetClass(), cleanQualifier) + : List.of(); + OriginLocator.Origin origin = origins.isEmpty() ? null : origins.getFirst(); + boolean hasAt = recipe.clean().getAtData() != null; + boolean ambiguousPiece = origin != null && origin.piece() && (!hasAt || origins.stream().filter(OriginLocator.Origin::matches).count() > 1); + if (origin == null || hasAt && !origin.matches() || ambiguousPiece) { + return hasAt ? ResolutionResult.pass() : ResolutionResult.fail(); + } + context.recordCtxAudit("Located target method %s (similarity %.2f%s)", MethodQualifier.create(origin.method().methodNode()).asDescriptor(), + origin.similarity(), origin.matches() ? ", injection point matches" : ""); + return ResolutionResult.success(MutableConfiguration.create().setTargetMethod(origin.method().methodNode())); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofMakesRedirectRedundant.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofMakesRedirectRedundant.java new file mode 100644 index 00000000..9383c7e7 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofMakesRedirectRedundant.java @@ -0,0 +1,41 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.sinytra.adapter.analysis.compare.InsnComparator; +import org.sinytra.adapter.analysis.compare.InstructionMatcher; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.ArrayList; +import java.util.List; + +public final class InstanceofMakesRedirectRedundant implements RewriteRule { + @Nullable + @Override + public Rewrite apply(MixinContext context, Recipe recipe) { + TypeInsnNode instanceOf = InstanceofReplacement.find(context, recipe); + if (instanceOf == null) { + return null; + } + + List handler = new ArrayList<>(); + for (AbstractInsnNode insn : context.methodNode().instructions) { + if (insn.getOpcode() >= 0) { + handler.add(insn); + } + } + if (handler.isEmpty() || !OpcodeUtil.isReturnOpcode(handler.getLast().getOpcode())) { + return null; + } + handler.removeLast(); + + if (InstructionMatcher.test(InstanceofReplacement.findCondition(instanceOf), handler, InsnComparator.IGNORE_VAR_INDEX)) { + return new Rewrite.Delete("the handler repeats the dirty instanceof check"); + } + + return null; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofReplacement.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofReplacement.java new file mode 100644 index 00000000..e5c07a10 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofReplacement.java @@ -0,0 +1,58 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.InsnTraversal; +import org.sinytra.adapter.analysis.compare.MethodAlignment; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.ArrayList; +import java.util.List; + +public final class InstanceofReplacement { + @Nullable + public static TypeInsnNode find(MixinContext context, Recipe recipe) { + Targets targets = recipe.targets(); + if (targets == null + || !(recipe.getCleanInjectionInsn() instanceof MethodInsnNode cleanCall) + || !Type.getReturnType(cleanCall.desc).equals(Type.BOOLEAN_TYPE) + || !(nextReal(cleanCall) instanceof JumpInsnNode) + ) { + return null; + } + + List found = MethodAlignment.align(targets.clean().methodNode(), targets.dirty().methodNode()).findDirtyAround(cleanCall).stream() + .filter(insn -> insn.getOpcode() == Opcodes.INSTANCEOF && nextReal(insn) instanceof JumpInsnNode) + .map(TypeInsnNode.class::cast) + .toList(); + return found.size() == 1 ? found.getFirst() : null; + } + + public static List findCondition(TypeInsnNode instanceOf) { + List insns = new ArrayList<>(InsnTraversal.untilLabel(instanceOf, InsnTraversal.BACKWARD).reversed()); + insns.add(instanceOf); + return insns; + } + + public static List findChecks(MethodNode method, String type) { + List checks = new ArrayList<>(); + for (AbstractInsnNode insn : method.instructions) { + if (insn.getOpcode() == Opcodes.INSTANCEOF && ((TypeInsnNode) insn).desc.equals(type)) { + checks.add((TypeInsnNode) insn); + } + } + return checks; + } + + @Nullable + private static AbstractInsnNode nextReal(AbstractInsnNode insn) { + return InsnTraversal.find(insn, InsnTraversal.FORWARD, i -> i.getOpcode() >= 0); + } + + private InstanceofReplacement() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofToConstant.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofToConstant.java new file mode 100644 index 00000000..b6c23df1 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofToConstant.java @@ -0,0 +1,104 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import com.google.common.collect.Multimap; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.model.TargetPair; +import org.sinytra.adapter.model.ann.ConstantData; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.MutableConfiguration; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.constant.TypeConstants; +import org.sinytra.adapter.model.param.MethodParameters; +import org.sinytra.adapter.model.param.MethodParameters.ParamGroup; +import org.sinytra.adapter.model.param.Parameter; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.processor.WrapOpSurgeon; + +import java.util.List; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.PROPERTY_ORDINAL; + +public final class InstanceofToConstant implements RewriteRule { + @Nullable + @Override + public Rewrite apply(MixinContext context, Recipe recipe) { + TypeInsnNode instanceOfCall = InstanceofReplacement.find(context, recipe); + if (instanceOfCall == null || !(recipe.getCleanInjectionInsn() instanceof MethodInsnNode cleanCall)) return null; + + MethodNode methodNode = context.methodNode(); + LocalVariableLookup lvt = LocalVariableLookup.of(methodNode); + if (lvt == null) return null; + + LocalVariableNode instanceLocal = lvt.getByParameterOrdinalOrNull(0); + if (instanceLocal == null) return null; + + Configuration clean = recipe.clean(); + List inheritedParams = clean.getParameters().getTypes(ParamGroup.METHOD_PARAMS); + Multimap usedVars = WrapOpSurgeon.getUsedVars(lvt, inheritedParams, context); + + MutableConfiguration config = recipe.dirty().copy() + .removeProperty(MixinKeys.TARGET_AT) + .setProperty(MixinKeys.TARGET_CONSTANT, ConstantData.classValue(Type.getObjectType(instanceOfCall.desc))) + .inheritSignature(); + MethodParameters parameters = config.getParameters(); + Parameter newInstanceParam = Parameter.simple(TypeConstants.OBJECT_TYPE); + parameters.set(ParamGroup.METHOD_PARAMS, List.of(newInstanceParam)); + + if (usedVars.containsKey(instanceLocal.index)) { + TargetPair cleanTarget = recipe.getCleanTarget(); + LocalVariableLookup cleanLocals = recipe.cleanLocalsTable(); + LocalVariableLookup dirtyLocals = recipe.dirtyLocalsTable(); + if (cleanTarget == null || cleanLocals == null || dirtyLocals == null) return null; + + List originalCallArgs = context.dataFlow(cleanTarget).resolveCallOperandProducers(cleanCall); + if (originalCallArgs == null || originalCallArgs.isEmpty() || !(originalCallArgs.getFirst() instanceof VarInsnNode originalInstanceInsn)) + return null; + TargetPair dirtyTarget = recipe.getDirtyTarget(); + LocalVariableNode originalInstanceLocal = cleanLocals.getInScopeOrNull(originalInstanceInsn.var, cleanCall); + if (dirtyTarget == null || originalInstanceLocal == null) return null; + + int cleanOrdinal = context.locals().resolveLocalOrdinal(cleanTarget, cleanCall, Type.getType(originalInstanceLocal.desc), originalInstanceInsn.var); + if (cleanOrdinal == -1) return null; + + int dirtyIndex = context.locals().resolveLocalIndex(dirtyTarget, instanceOfCall, Type.getType(instanceLocal.desc), cleanOrdinal); + LocalVariableNode dirtyLocal = dirtyLocals.getInScopeOrNull(dirtyIndex, instanceOfCall); + if (dirtyLocal == null) return null; + + Parameter replacementInstanceParam = Parameter.builder(dirtyLocal.desc) + .annotate(MixinAnnotations.LOCAL, b -> b + .put(PROPERTY_ORDINAL, cleanOrdinal)) + .build(); + parameters.add(ParamGroup.LOCALS, replacementInstanceParam); + // TODO exclude original.call(...) insns from being mapped + parameters.mapParameter(newInstanceParam, replacementInstanceParam); + } + + // TODO Find a way to get rid of this ugly hack + // Use "state.getBlock()" as the new Block parameter + if (instanceOfCall.getPrevious() instanceof MethodInsnNode m) { + String loadedType = Type.getReturnType(m.desc).getDescriptor(); + for (int paramVar : usedVars.keySet()) { + if (paramVar == instanceLocal.index) { + continue; + } + + LocalVariableNode node = lvt.getByIndexOrNull(paramVar); + if (node == null || !loadedType.equals(node.desc)) return null; + + int paramOrdinal = lvt.getParameterOrdinal(node); + List cleanParams = clean.getParameters().get(ParamGroup.METHOD_PARAMS); + if (paramOrdinal < 0 || paramOrdinal >= cleanParams.size()) return null; + + Parameter oldParam = cleanParams.get(paramOrdinal); + parameters.mapParameter(oldParam, newInstanceParam); + } + } + + return new Rewrite.Replace(config); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofToExpression.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofToExpression.java new file mode 100644 index 00000000..f992493b --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/InstanceofToExpression.java @@ -0,0 +1,45 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.TypeInsnNode; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.ann.ExpressionData; +import org.sinytra.adapter.model.config.Configurations; +import org.sinytra.adapter.model.config.key.IntentKeys; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +import java.util.List; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_EXPRESSION; + +public final class InstanceofToExpression implements RewriteRule { + @Nullable + @Override + public Rewrite apply(MixinContext context, Recipe recipe) { + if (!Type.getReturnType(context.methodNode().desc).equals(Type.BOOLEAN_TYPE)) { + return null; + } + TypeInsnNode instanceOf = InstanceofReplacement.find(context, recipe); + if (instanceOf == null) { + return null; + } + + Type type = Type.getObjectType(instanceOf.desc); + String id = getIdentifier(type); + ExpressionData expression = new ExpressionData("? instanceof " + id, List.of(new ExpressionData.TypeDefinition(id, type))); + List checks = InstanceofReplacement.findChecks(recipe.getDirtyTarget().methodNode(), instanceOf.desc); + AtData at = AtData.builder(AT_VAL_EXPRESSION) + .ordinal(checks.size() == 1 ? null : checks.indexOf(instanceOf)) + .build(); + return new Rewrite.Replace(Configurations.changeMixinType(recipe.dirty(), MixinAnnotations.MODIFY_EXPR_VAL, at) + .setProperty(IntentKeys.EXPRESSION, expression)); + } + + private static String getIdentifier(Type type) { + String name = type.getInternalName(); + return name.substring(name.lastIndexOf('/') + 1).replace('$', '_'); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/Rewrite.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/Rewrite.java new file mode 100644 index 00000000..3363eddc --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/Rewrite.java @@ -0,0 +1,11 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import org.sinytra.adapter.model.config.Configuration; + +public sealed interface Rewrite { + record Replace(Configuration config) implements Rewrite { + } + + record Delete(String reason) implements Rewrite { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/RewriteResolver.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/RewriteResolver.java new file mode 100644 index 00000000..1fd7b1c0 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/RewriteResolver.java @@ -0,0 +1,36 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import org.sinytra.adapter.model.config.Configurations; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.resolver.Resolver; + +import java.util.List; + +public final class RewriteResolver implements Resolver { + private final List rules; + + public RewriteResolver(List rules) { + this.rules = rules; + } + + @Override + public ResolutionResult resolve(MixinContext context, Recipe recipe) { + for (RewriteRule rule : this.rules) { + Rewrite rewrite = context.audited(rule, () -> rule.apply(context, recipe)); + switch (rewrite) { + case null -> { + } + case Rewrite.Replace(var config) -> { + context.recordCtxAudit("Rewrite with %s", rule.getClass().getSimpleName()); + return ResolutionResult.replace(config); + } + case Rewrite.Delete(String reason) -> { + context.recordCtxAudit("Delete with %s: %s", rule.getClass().getSimpleName(), reason); + return ResolutionResult.replace(Configurations.DELETE); + } + } + } + return ResolutionResult.pass(); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/RewriteRule.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/RewriteRule.java new file mode 100644 index 00000000..9b74f66c --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/RewriteRule.java @@ -0,0 +1,10 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import org.jetbrains.annotations.Nullable; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; + +public interface RewriteRule { + @Nullable + Rewrite apply(MixinContext context, Recipe recipe); +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/ShiftedVariableToExpression.java b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/ShiftedVariableToExpression.java new file mode 100644 index 00000000..ea2d1c23 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/resolver/rewrite/ShiftedVariableToExpression.java @@ -0,0 +1,47 @@ +package org.sinytra.adapter.pipeline.resolver.rewrite; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.sinytra.adapter.analysis.InsnTraversal; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.config.Configurations; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.pipeline.Recipe; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.util.OpcodeUtil; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.List; + +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_INVOKE; +import static org.sinytra.adapter.model.constant.MixinAnnotationConstants.AT_VAL_INVOKE_ASSIGN; + +public final class ShiftedVariableToExpression implements RewriteRule { + @Nullable + @Override + public Rewrite apply(MixinContext context, Recipe recipe) { + AtData at = recipe.clean().getAtData(); + if (at == null || at.getTarget().isEmpty() || !AT_VAL_INVOKE.equals(at.getValue()) && !AT_VAL_INVOKE_ASSIGN.equals(at.getValue()) + || at.getProperty(AtData.Keys.SHIFT).orElse(null) != At.Shift.BY || at.getProperty(AtData.Keys.BY).orElse(0) != 2 + ) { + return null; + } + + List calls = context.injections().find(recipe.getCleanTarget()); + if (calls.isEmpty() || !calls.stream().allMatch(ShiftedVariableToExpression::isStored)) { + return null; + } + + AtData expression = AtData.builder(AT_VAL_INVOKE) + .target(at.getTargetOrThrow()) + .ordinal(at.getOrdinal().orElse(null)) + .build(); + + return new Rewrite.Replace(Configurations.changeMixinType(recipe.dirty(), MixinAnnotations.MODIFY_EXPR_VAL, expression)); + } + + private static boolean isStored(AbstractInsnNode insn) { + AbstractInsnNode next = InsnTraversal.find(insn, InsnTraversal.FORWARD, i -> i.getOpcode() >= 0); + return OpcodeUtil.isStoreOpcode(insn.getOpcode()) || next != null && OpcodeUtil.isStoreOpcode(next.getOpcode()); + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/transform/AnonymousTargetTransformer.java b/core/src/main/java/org/sinytra/adapter/pipeline/transform/AnonymousTargetTransformer.java new file mode 100644 index 00000000..71232d76 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/transform/AnonymousTargetTransformer.java @@ -0,0 +1,119 @@ +package org.sinytra.adapter.pipeline.transform; + +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.ClassPatchContext; +import org.sinytra.adapter.api.ClassTransformer; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.model.ann.ClassTarget; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.pipeline.MixinParser; +import org.sinytra.adapter.pipeline.ctx.PatchContext; +import org.sinytra.adapter.util.AdapterUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class AnonymousTargetTransformer implements ClassTransformer { + @Override + public PatchResult apply(ClassNode classNode, ClassPatchContext context) { + Type target = context.singleTarget(); + if (target == null || !AdapterUtil.isAnonymousClass(target.getInternalName())) { + return PatchResult.PASS; + } + + PatchResult result = PatchResult.PASS; + Type renumbered = findRenumbered(context.environment(), target.getInternalName()); + if (renumbered != null && context instanceof PatchContext patchContext && patchContext.classTarget() != null) { + retarget(classNode, patchContext, target, renumbered); + target = renumbered; + result = PatchResult.APPLY; + } + + ClassNode targetClass = context.environment().dirtyClassLookup().getClass(target.getInternalName()).orElse(null); + if (targetClass != null && renameShadowFields(classNode, targetClass, context)) { + result = PatchResult.APPLY; + } + return result; + } + + @Nullable + private static Type findRenumbered(PatchEnvironment environment, String target) { + ClassNode cleanClass = environment.cleanClassLookup().getClass(target).orElse(null); + if (cleanClass == null || cleanClass.outerClass == null || cleanClass.outerMethod == null) { + return null; + } + List cleanClasses = findAnonymousClasses(environment.cleanClassLookup(), cleanClass); + List dirtyClasses = findAnonymousClasses(environment.dirtyClassLookup(), cleanClass); + int index = cleanClasses.indexOf(target); + if (index == -1 || cleanClasses.size() != dirtyClasses.size() || dirtyClasses.get(index).equals(target)) { + return null; + } + return Type.getObjectType(dirtyClasses.get(index)); + } + + private static List findAnonymousClasses(ClassLookup lookup, ClassNode anonymousClass) { + ClassNode outerClass = lookup.getClass(anonymousClass.outerClass).orElse(null); + if (outerClass == null) { + return List.of(); + } + List classes = new ArrayList<>(); + for (InnerClassNode innerClass : outerClass.innerClasses) { + if (AdapterUtil.isAnonymousClass(innerClass.name)) { + lookup.getClass(innerClass.name) + .filter(inner -> anonymousClass.outerClass.equals(inner.outerClass) && anonymousClass.outerMethod.equals(inner.outerMethod) + && anonymousClass.outerMethodDesc.equals(inner.outerMethodDesc)) + .ifPresent(inner -> classes.add(inner.name)); + } + } + classes.sort(Comparator.comparingInt(name -> Integer.parseInt(name.substring(name.lastIndexOf('$') + 1)))); + return classes; + } + + private void retarget(ClassNode classNode, PatchContext context, Type from, Type to) { + ClassTarget classTarget = context.classTarget(); + classTarget.set(to); + for (MethodNode method : classNode.methods) { + MixinParser.MixinMethodHandle handle = MixinParser.parseMixin(classTarget, method, context, context.environment()); + if (handle == null) continue; + + handle.properties().getProperty(MixinKeys.TARGET_METHOD) + .filter(q -> from.getDescriptor().equals(q.owner())) + .map(q -> handle.properties().serialize(MixinKeys.TARGET_METHOD, q.withOwner(to))) + .ifPresent(q -> handle.methodAnnotation().setOrAppendNonNull(MixinKeys.TARGET_METHOD.name(), q)); + } + context.environment().auditTrail().recordAudit(this, classNode, "Retarget anonymous class %s to %s", from.getInternalName(), to.getInternalName()); + } + + private boolean renameShadowFields(ClassNode classNode, ClassNode targetClass, ClassPatchContext context) { + Map> targetFields = targetClass.fields.stream().collect(Collectors.groupingBy(field -> field.desc)); + Map renames = new HashMap<>(); + for (FieldNode field : classNode.fields) { + List candidates = targetFields.getOrDefault(field.desc, List.of()); + if (AdapterUtil.isShadowField(field) && candidates.size() == 1 && !candidates.getFirst().name.equals(field.name)) { + renames.put(field.name, candidates.getFirst().name); + field.name = candidates.getFirst().name; + } + } + if (renames.isEmpty()) { + return false; + } + + renames.forEach((from, to) -> context.environment().auditTrail().recordAudit(this, classNode, "Rename anonymous class field %s to %s", from, to)); + for (MethodNode method : classNode.methods) { + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof FieldInsnNode fieldInsn && fieldInsn.owner.equals(classNode.name)) { + fieldInsn.name = renames.getOrDefault(fieldInsn.name, fieldInsn.name); + } + } + } + return true; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/pipeline/transform/LocalCaptureUpgradeTransformer.java b/core/src/main/java/org/sinytra/adapter/pipeline/transform/LocalCaptureUpgradeTransformer.java new file mode 100644 index 00000000..454f0ede --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/pipeline/transform/LocalCaptureUpgradeTransformer.java @@ -0,0 +1,88 @@ +package org.sinytra.adapter.pipeline.transform; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.analysis.locals.LocalVariable; +import org.sinytra.adapter.analysis.locals.LocalsAnalyzer; +import org.sinytra.adapter.api.AuditTrail; +import org.sinytra.adapter.api.MethodPatchContext; +import org.sinytra.adapter.api.MethodTransformer; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.model.Targets; +import org.sinytra.adapter.model.config.Configuration; +import org.sinytra.adapter.model.config.key.MixinKeys; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.param.Parameters; +import org.sinytra.adapter.pipeline.ctx.MixinContext; +import org.sinytra.adapter.pipeline.locate.Describer; +import org.sinytra.adapter.pipeline.locate.LocalLocator; +import org.sinytra.adapter.util.MixinBridge; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LocalCaptureUpgradeTransformer implements MethodTransformer { + @Override + public PatchResult apply(MethodPatchContext patchContext) { + MixinContext context = (MixinContext) patchContext; + return apply(context, context.configuration()); + } + + public PatchResult apply(MixinContext context, Configuration config) { + MethodNode handler = context.methodNode(); + Targets targets = context.targets(config.getTargetMethod()); + if (targets == null || !config.hasProperty(MixinKeys.LOCALS) || Parameters.parse(handler).stream().anyMatch(p -> p.hasAnnotation(MixinAnnotations.LOCAL))) { + return PatchResult.PASS; + } + + LocalsAnalyzer.CapturedLocalsInfo info = context.locals().resolveCapturedLocalsInfo(targets.dirty()); + if (info == null || info.diff().isEmpty()) { + return PatchResult.PASS; + } + + AbstractInsnNode cleanInsn = context.injections().resolveFirst(targets.clean()); + AbstractInsnNode dirtyInsn = context.injections().resolveFirst(targets.dirty()); + List cleanLocals = context.locals().resolveAvailableLocals(targets.clean()); + if (cleanInsn == null || dirtyInsn == null || cleanLocals == null) { + return PatchResult.PASS; + } + + LocalsAnalyzer.CapturedLocals captured = info.capturedLocals(); + LocalsAnalyzer.CapturedLocalsTransform transform = LocalsAnalyzer.analyzeCapturedLocals(captured, handler); + MixinBridge.Session dirtySession = MixinBridge.session(context.classNode().name, handler, targets.dirty().classNode(), targets.dirty().methodNode(), context.environment()); + Type[] params = Type.getArgumentTypes(handler.desc); + Map selectors = new HashMap<>(); + + for (int param : transform.used()) { + Type type = params[param]; + long rank = Arrays.stream(params, captured.paramLocalStart(), param).filter(type::equals).count(); + LocalVariable cleanLocal = cleanLocals.stream().filter(local -> local.type().equals(type)).skip(rank).findFirst().orElse(null); + if (cleanLocal == null) { + return PatchResult.PASS; + } + + int slot = LocalLocator.resolve(context, targets.clean(), targets.dirty(), cleanLocal.index(), cleanInsn, targets.dirty()); + AnnotationNode selector = slot == -1 ? null : Describer.describeLocalSelector(context, dirtySession, new AnnotationNode(MixinAnnotations.LOCAL), targets.dirty(), dirtyInsn, slot, type); + if (selector == null) { + return PatchResult.PASS; + } + + selectors.put(param, selector); + } + + selectors.forEach((param, selector) -> selector.accept(handler.visitParameterAnnotation(param, MixinAnnotations.LOCAL, false))); + PatchResult result = transform.remover().isEmpty() ? PatchResult.APPLY : transform.remover().apply(context); + if (result == PatchResult.PASS) { + return PatchResult.PASS; + } + + context.methodAnnotation().setOrAppendNonNull(MixinKeys.TARGET_METHOD.name(), config.serialize(MixinKeys.TARGET_METHOD, config.getTargetMethod())); + context.recordCtxAudit("Upgrade captured locals"); + context.environment().auditTrail().recordResult(context, config, AuditTrail.Match.FULL); + return result; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/transform/ClassTransformer.java b/core/src/main/java/org/sinytra/adapter/transform/ClassTransformer.java deleted file mode 100644 index 18ebf08c..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/ClassTransformer.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.sinytra.adapter.transform; - -import org.objectweb.asm.tree.ClassNode; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.env.ctx.PatchContext; -import org.sinytra.adapter.env.ctx.PatchResult; - -public interface ClassTransformer { - PatchResult apply(ClassNode classNode, ClassTarget classTarget, PatchContext context); -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/MethodTransformer.java b/core/src/main/java/org/sinytra/adapter/transform/MethodTransformer.java deleted file mode 100644 index 0c97b1d9..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/MethodTransformer.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.sinytra.adapter.transform; - -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.env.ctx.PatchResult; - -public interface MethodTransformer { - PatchResult apply(MixinContext context, Configuration config); -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/cls/DynamicAnonClassIndexPatch.java b/core/src/main/java/org/sinytra/adapter/transform/cls/DynamicAnonClassIndexPatch.java deleted file mode 100644 index 5ae53db0..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/cls/DynamicAnonClassIndexPatch.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.sinytra.adapter.transform.cls; - -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InnerClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.env.ctx.PatchContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.patch.MixinParser; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.transform.ClassTransformer; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.MethodQualifier; - - -/** - * Handle cases where a method targets an anonymous class whose index has changed. - */ -public class DynamicAnonClassIndexPatch implements ClassTransformer { - @Override - public PatchResult apply(ClassNode classNode, ClassTarget classTarget, PatchContext context) { - Type singleTarget = classTarget.getSingle(); - if (singleTarget == null) return PatchResult.PASS; - - String target = singleTarget.getInternalName(); - if (!AdapterUtil.isAnonymousClass(target)) { - return PatchResult.PASS; - } - - ClassNode cleanClass = context.environment().cleanClassLookup().getClass(target).orElse(null); - if (cleanClass == null || cleanClass.outerClass == null || cleanClass.outerMethod == null) { - return PatchResult.PASS; - } - ClassNode outerDirtyClass = context.environment().dirtyClassLookup().getClass(cleanClass.outerClass).orElse(null); - if (outerDirtyClass == null) { - return PatchResult.PASS; - } - - // FIXME Cannot use remap here! - MethodQualifier cleanOuterMethod = MethodQualifier.parse(context.remap(cleanClass.outerMethod + cleanClass.outerMethodDesc)).orElse(null); - if (cleanOuterMethod == null) { - return PatchResult.PASS; - } - - for (InnerClassNode innerClass : outerDirtyClass.innerClasses) { - if (AdapterUtil.isAnonymousClass(innerClass.name)) { - ClassNode inner = context.environment().dirtyClassLookup().getClass(innerClass.name).orElse(null); - if (inner == null || inner.outerMethod == null || inner.outerMethodDesc == null) { - continue; - } - - if (!inner.name.equals(target) && cleanOuterMethod.matches(null, inner.outerMethod, inner.outerMethodDesc)) { - classTarget.set(Type.getObjectType(inner.name)); - updateMixinTargetOwners(classNode, classTarget, context, inner.name); - return PatchResult.APPLY; - } - } - } - - return PatchResult.PASS; - } - - private static void updateMixinTargetOwners(ClassNode classNode, ClassTarget classTarget, PatchContext context, String newOwner) { - for (MethodNode method : classNode.methods) { - MixinParser.MixinMethodHandle handle = MixinParser.parseMixin(classTarget, method, context, context.environment()); - if (handle == null) continue; - - handle.properties().getProperty(MixinKeys.TARGET_METHOD) - .map(q -> handle.properties().serialize(MixinKeys.TARGET_METHOD, q.withOwner(Type.getObjectType(newOwner)))) - .ifPresent(q -> handle.methodAnnotation().setOrAppendNonNull(MixinKeys.TARGET_METHOD.name(), q)); - } - } -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/cls/DynamicAnonymousShadowFieldTypePatch.java b/core/src/main/java/org/sinytra/adapter/transform/cls/DynamicAnonymousShadowFieldTypePatch.java deleted file mode 100644 index 10a57712..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/cls/DynamicAnonymousShadowFieldTypePatch.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.sinytra.adapter.transform.cls; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.env.ctx.PatchContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.transform.ClassTransformer; -import org.sinytra.adapter.util.AdapterUtil; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -public class DynamicAnonymousShadowFieldTypePatch implements ClassTransformer { - @Override - public PatchResult apply(ClassNode classNode, ClassTarget classTarget, PatchContext context) { - Type singleTarget = classTarget.getSingle(); - if (singleTarget == null) return PatchResult.PASS; - - String target = singleTarget.getInternalName(); - if (!AdapterUtil.isAnonymousClass(target)) { - return PatchResult.PASS; - } - - ClassNode targetClass = context.environment().dirtyClassLookup().getClass(target).orElse(null); - if (targetClass == null) { - return PatchResult.PASS; - } - - Map renames = new HashMap<>(); - Multimap fields = HashMultimap.create(); - for (FieldNode targetField : targetClass.fields) { - fields.put(targetField.desc, targetField); - } - - for (FieldNode field : classNode.fields) { - if (field.visibleAnnotations != null) { - for (AnnotationNode ann : field.visibleAnnotations) { - if (MixinAnnotations.SHADOW.equals(ann.desc)) { - Collection targetFields = fields.get(field.desc); - if (targetFields.size() == 1) { - FieldNode targetField = targetFields.iterator().next(); - if (!field.name.equals(targetField.name)) { - renames.put(field.name, targetField.name); - field.name = targetField.name; - } - } - } - } - } - } - - if (!renames.isEmpty()) { - renames.forEach((from, to) -> context.environment().auditTrail().recordAudit(this, classNode, "Rename anonymous class field %s to %s", from, to)); - for (MethodNode method : classNode.methods) { - for (AbstractInsnNode insn : method.instructions) { - if (insn instanceof FieldInsnNode finsn && finsn.owner.equals(classNode.name)) { - finsn.name = renames.getOrDefault(finsn.name, finsn.name); - } - } - } - return PatchResult.APPLY; - } - - return PatchResult.PASS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/InlineParameterTransformer.java b/core/src/main/java/org/sinytra/adapter/transform/param/InlineParameterTransformer.java deleted file mode 100644 index cf68d073..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/param/InlineParameterTransformer.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.sinytra.adapter.transform.param; - -import com.mojang.logging.LogUtils; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.InstructionAdapter; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.analysis.locals.LVTSnapshot; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.util.AdapterUtil; -import org.slf4j.Logger; - -import java.util.Comparator; -import java.util.List; -import java.util.function.Consumer; - -import static org.sinytra.adapter.util.AdapterUtil.MIXINPATCH; - -public record InlineParameterTransformer(int target, Consumer adapter) implements ParameterTransformer { - private static final Logger LOGGER = LogUtils.getLogger(); - - @Override - public InlineParameterTransformer offset(int offset) { - return new InlineParameterTransformer(this.target + offset, this.adapter); - } - - @Override - public PatchResult apply(ClassNode classNode, MethodNode methodNode, MixinContext context, List parameters, int offset) { - final int index = this.target + offset; - LOGGER.info(MIXINPATCH, "Inlining parameter {} of method {}.{}", index, classNode.name, methodNode.name); - final int replaceIndex = -999 + index; - - LVTSnapshot.with(methodNode, () -> { - methodNode.localVariables.sort(Comparator.comparingInt(lvn -> lvn.index)); - LocalVariableNode lvn = methodNode.localVariables.remove(index + (context.isStatic() ? 0 : 1)); - AdapterUtil.replaceLVT(methodNode, idx -> idx == lvn.index ? replaceIndex : idx); - }); - - ParamTransformationUtil.updateParameterAnnotationArrays(methodNode, parameters.size(), annotations -> annotations.remove(index)); - parameters.remove(index); - - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof VarInsnNode varInsn && varInsn.var == replaceIndex) { - InsnList replacementInsns = AdapterUtil.insnsWithAdapter(adapter); - methodNode.instructions.insert(varInsn, replacementInsns); - methodNode.instructions.remove(varInsn); - } - } - - return PatchResult.COMPUTE_FRAMES; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/ParamTransformationUtil.java b/core/src/main/java/org/sinytra/adapter/transform/param/ParamTransformationUtil.java deleted file mode 100644 index 521f05f0..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/param/ParamTransformationUtil.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.sinytra.adapter.transform.param; - -import it.unimi.dsi.fastutil.ints.Int2IntMap; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; -import org.sinytra.adapter.util.AdapterUtil; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; - -public final class ParamTransformationUtil { - public static void updateParameterAnnotationArrays(MethodNode methodNode, int paramCount, Consumer>> action) { - methodNode.visibleParameterAnnotations = updateParameterAnnotations(methodNode.visibleParameterAnnotations, paramCount, action); - methodNode.invisibleParameterAnnotations = updateParameterAnnotations(methodNode.invisibleParameterAnnotations, paramCount, action); - // Derive the count from the final descriptor later - methodNode.visibleAnnotableParameterCount = 0; - methodNode.invisibleAnnotableParameterCount = 0; - } - - @Nullable - @SuppressWarnings("unchecked") - private static List[] updateParameterAnnotations(@Nullable List[] annotations, int paramCount, Consumer>> action) { - if (annotations == null) { - return null; - } - List> list = new ArrayList<>(Arrays.asList(annotations)); - while (list.size() < paramCount) { - list.add(null); - } - action.accept(list); - return list.toArray(List[]::new); - } - - public static void reorderParameters(MethodNode methodNode, List parameters, boolean nonStatic, int[] order) { - List permuted = Arrays.stream(order).mapToObj(parameters::get).toList(); - - Int2IntMap slots = new Int2IntOpenHashMap(); - for (int pos = 0; pos < order.length; pos++) { - slots.put(getLVTIndex(parameters, nonStatic, order[pos]), getLVTIndex(permuted, nonStatic, pos)); - } - AdapterUtil.replaceLVT(methodNode, idx -> slots.getOrDefault(idx, idx)); - if (methodNode.localVariables != null) { - for (LocalVariableNode lvn : methodNode.localVariables) { - lvn.index = slots.getOrDefault(lvn.index, lvn.index); - } - } - - updateParameterAnnotationArrays(methodNode, parameters.size(), annotations -> { - List> original = new ArrayList<>(annotations); - for (int pos = 0; pos < order.length; pos++) { - annotations.set(pos, original.get(order[pos])); - } - }); - - for (int pos = 0; pos < order.length; pos++) { - parameters.set(pos, permuted.get(pos)); - } - } - - public static int getLVTIndex(List parameters, boolean nonStatic, int index) { - int lvt = nonStatic ? 1 : 0; - for (int i = 0; i < index; i++) { - lvt += parameters.get(i).getSize(); - } - return lvt; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/param/SubstituteParameterTransformer.java b/core/src/main/java/org/sinytra/adapter/transform/param/SubstituteParameterTransformer.java deleted file mode 100644 index a7eae0c5..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/param/SubstituteParameterTransformer.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.sinytra.adapter.transform.param; - -import com.mojang.logging.LogUtils; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.locals.LVTSnapshot; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.util.AdapterUtil; -import org.slf4j.Logger; - -import java.util.List; - -public record SubstituteParameterTransformer(int target, int substitute) implements ParameterTransformer { - private static final Logger LOGGER = LogUtils.getLogger(); - - @Override - public SubstituteParameterTransformer offset(int offset) { - return new SubstituteParameterTransformer(this.target + offset, this.substitute + offset); - } - - @Override - public PatchResult apply(ClassNode classNode, MethodNode methodNode, MixinContext context, List parameters, int offset) { - int paramIndex = this.target + offset; - int substituteParamIndex = this.substitute + offset; - boolean isNonStatic = !context.isStatic(); - if (paramIndex >= parameters.size() || substituteParamIndex >= parameters.size() || paramIndex == substituteParamIndex) { - return PatchResult.PASS; - } - - int localIndex = ParamTransformationUtil.getLVTIndex(parameters, isNonStatic, paramIndex); - int substituteIndex = ParamTransformationUtil.getLVTIndex(parameters, isNonStatic, substituteParamIndex); - - LVTSnapshot.with(methodNode, () -> { - LOGGER.info("Substituting parameter {} for {} in {}.{}", paramIndex, substituteParamIndex, classNode.name, methodNode.name); - ParamTransformationUtil.updateParameterAnnotationArrays(methodNode, parameters.size(), annotations -> annotations.remove(paramIndex)); - parameters.remove(paramIndex); - methodNode.localVariables.removeIf(lvn -> lvn.index == localIndex); - - AdapterUtil.replaceLVT(methodNode, idx -> idx == localIndex ? substituteIndex : idx); - }); - - return PatchResult.COMPUTE_FRAMES; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchProcessor.java b/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchProcessor.java deleted file mode 100644 index 20dbf53f..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchProcessor.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sinytra.adapter.transform.patch; - -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.patch.Recipe; -import org.sinytra.adapter.patch.TxResult; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.processor.Processor; -import org.sinytra.adapter.transform.MethodTransformer; - -import java.util.List; - -public record MethodPatchProcessor(List transforms) implements Processor { - @Override - public TxResult process(MixinContext context, Configuration dirty, Recipe recipe) { - for (MethodTransformer transformer : this.transforms) { - transformer.apply(context, recipe.clean()); - } - return TxResult.SUCCESS; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/transform/preprocess/LocalCaptureUpgradeTransformer.java b/core/src/main/java/org/sinytra/adapter/transform/preprocess/LocalCaptureUpgradeTransformer.java deleted file mode 100644 index 7c1e2531..00000000 --- a/core/src/main/java/org/sinytra/adapter/transform/preprocess/LocalCaptureUpgradeTransformer.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.sinytra.adapter.transform.preprocess; - -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.LocalVariableNode; -import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.locals.LocalVariableLookup; -import org.sinytra.adapter.env.ctx.MixinContext; -import org.sinytra.adapter.env.ctx.AuditTrail; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.patch.config.Configuration; -import org.sinytra.adapter.patch.config.key.MixinKeys; -import org.sinytra.adapter.transform.MethodTransformer; -import org.sinytra.adapter.analysis.locals.LocalVarAnalyzer; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.env.ctx.Targets; -import org.sinytra.adapter.env.param.Parameters; - -import java.util.*; - -import static org.sinytra.adapter.env.util.MixinAnnotationConstants.PROPERTY_ORDINAL; - -public class LocalCaptureUpgradeTransformer implements MethodTransformer { - @Override - public PatchResult apply(MixinContext context, Configuration config) { - // Check requirements - Targets targets = context.methods().findTargets(config.getTargetMethod()); - if (targets == null) return PatchResult.PASS; - - if (!config.hasProperty(MixinKeys.LOCALS)) return PatchResult.PASS; - - // Analyze locals - MethodNode methodNode = context.methodNode(); - // Already upgraded - if (Parameters.parse(methodNode).stream().anyMatch(p -> p.hasAnnotation(MixinAnnotations.LOCAL))) return PatchResult.PASS; - - LocalVarAnalyzer.CapturedLocalsInfo info = LocalVarAnalyzer.getCapturedLocals(context, targets.dirty()); - if (info == null || info.diff().isEmpty()) return PatchResult.PASS; - - LocalVarAnalyzer.CapturedLocalsTransform transform = LocalVarAnalyzer.analyzeCapturedLocals(info.capturedLocals(), methodNode); - - LocalVariableLookup cleanLookup = context.methods().getLVT(targets.clean().methodNode()); - LocalVariableLookup dirtyLookup = context.methods().getLVT(targets.dirty().methodNode()); - LocalVariableLookup lookup = info.capturedLocals().lvt(); - if (cleanLookup == null || dirtyLookup == null) return PatchResult.PASS; - - Map parameterToOrdinal = new HashMap<>(); - Set usedOrdinals = new HashSet<>(); - for (LocalVariableNode node : transform.usedLocalNodes()) { - Type expected = Type.getType(node.desc); - boolean addOrdinal = true; - - List cleanLocals = cleanLookup.getForType(expected); - List dirtyLocals = dirtyLookup.getForType(expected); - if (cleanLocals.size() != dirtyLocals.size()) { - long matching = info.availableTypes().stream() - .filter(expected::equals) - .count(); - if (matching != 1) - return PatchResult.PASS; - - addOrdinal = false; - } - - int localOrdinal = lookup.getTypedOrdinal(node).orElse(-1); - if (localOrdinal == -1) return PatchResult.PASS; - - int paramOrdinal = lookup.getParameterOrdinal(node); - usedOrdinals.add(paramOrdinal); - - if (addOrdinal && cleanLocals.size() > 1) { - parameterToOrdinal.put(paramOrdinal, localOrdinal); - } - } - - Type[] args = Type.getArgumentTypes(methodNode.desc); - int start = info.capturedLocals().paramLocalStart(); - for (int i = start; i < args.length; i++) { - if (!usedOrdinals.contains(i)) continue; - - AnnotationVisitor visitor = methodNode.visitParameterAnnotation(i, MixinAnnotations.LOCAL, false); - if (parameterToOrdinal.containsKey(i)) { - visitor.visit(PROPERTY_ORDINAL, parameterToOrdinal.get(i)); - } - } - - PatchResult result = transform.remover().isEmpty() ? PatchResult.APPLY : transform.remover().apply(context); - if (result == PatchResult.PASS) return PatchResult.PASS; - - context.recordCtxAudit("Upgrade captured locals"); - context.environment().auditTrail().recordResult(context, config, AuditTrail.Match.FULL); - return result; - } -} diff --git a/core/src/main/java/org/sinytra/adapter/types/AccessorAdapterGenerator.java b/core/src/main/java/org/sinytra/adapter/types/AccessorAdapterGenerator.java new file mode 100644 index 00000000..6b21546d --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/types/AccessorAdapterGenerator.java @@ -0,0 +1,54 @@ +package org.sinytra.adapter.types; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.model.constant.TypeConstants; +import org.sinytra.adapter.util.OpcodeUtil; + +import java.util.ArrayList; +import java.util.List; + +public final class AccessorAdapterGenerator { + private static final String PREFIX = "adapter$"; + private static final int PRIORITY_MAX = 9999; + + public static String getOrCreateGetter(BytecodeFixerJarGenerator generator, Type owner, String field, boolean isStatic, TypeAdapter adapter) { + ClassNode mixin = generator.getOrCreateClass(owner.getInternalName().replace('/', '_'), name -> createMixin(name, owner)); + String name = PREFIX + field; + String desc = Type.getMethodDescriptor(adapter.to()); + synchronized (mixin) { + if (mixin.methods.stream().noneMatch(m -> m.name.equals(name))) { + mixin.methods.add(createGetter(owner, field, isStatic, adapter, name, desc)); + } + } + return name + desc; + } + + private static MethodNode createGetter(Type owner, String field, boolean isStatic, TypeAdapter adapter, String name, String desc) { + MethodNode getter = new MethodNode(Opcodes.ACC_PUBLIC | (isStatic ? Opcodes.ACC_STATIC : 0), name, desc, null, null); + getter.visibleAnnotations = new ArrayList<>(List.of(new AnnotationNode(MixinAnnotations.UNIQUE))); + if (!isStatic) { + getter.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + } + getter.instructions.add(new FieldInsnNode(isStatic ? Opcodes.GETSTATIC : Opcodes.GETFIELD, owner.getInternalName(), field, adapter.from().getDescriptor())); + adapter.apply(getter.instructions, getter.instructions.getLast()); + getter.instructions.add(new InsnNode(OpcodeUtil.getReturnOpcode(adapter.to()))); + return getter; + } + + private static ClassNode createMixin(String name, Type target) { + ClassNode mixin = new ClassNode(); + mixin.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, name, null, TypeConstants.OBJECT_TYPE.getInternalName(), null); + AnnotationNode annotation = new AnnotationNode(MixinAnnotations.MIXIN); + annotation.visit("value", List.of(target)); + annotation.visit("priority", PRIORITY_MAX); + mixin.invisibleAnnotations = new ArrayList<>(List.of(annotation)); + return mixin; + } + + private AccessorAdapterGenerator() { + } +} diff --git a/core/src/main/java/org/sinytra/adapter/types/FieldAccessorTypeTransformer.java b/core/src/main/java/org/sinytra/adapter/types/FieldAccessorTypeTransformer.java deleted file mode 100644 index cf301ad0..00000000 --- a/core/src/main/java/org/sinytra/adapter/types/FieldAccessorTypeTransformer.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.sinytra.adapter.types; - -import com.mojang.datafixers.util.Pair; -import org.objectweb.asm.AnnotationVisitor; -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.MethodNode; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.env.ctx.MethodHelper; -import org.sinytra.adapter.env.ctx.PatchContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.env.util.MixinAnnotations; -import org.sinytra.adapter.transform.ClassTransformer; -import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.OpcodeUtil; -import org.sinytra.adapter.env.util.TypeConstants; - -public class FieldAccessorTypeTransformer implements ClassTransformer { - private static final String PREFIX = "adapter$"; - private static final int PRIORITY_MAX = 9999; - - @Override - public PatchResult apply(ClassNode classNode, ClassTarget classTarget, PatchContext context) { - BytecodeFixerUpper bfu = context.environment().bytecodeFixerUpper(); - if (bfu == null || context.targetTypes().size() != 1) return PatchResult.PASS; - - Type owner = context.targetTypes().getFirst(); - PatchResult result = PatchResult.PASS; - for (MethodNode methodNode : classNode.methods) { - AnnotationNode accessor = methodNode.visibleAnnotations == null ? null : methodNode.visibleAnnotations.stream() - .filter(a -> MixinAnnotations.ACCESSOR.equals(a.desc)) - .findFirst() - .orElse(null); - if (accessor == null || Type.getArgumentCount(methodNode.desc) != 0 || Type.getReturnType(methodNode.desc) == Type.VOID_TYPE) { - continue; - } - result = result.or(redirectAccessor(classNode, methodNode, accessor, owner, bfu, context)); - } - return result; - } - - private PatchResult redirectAccessor(ClassNode classNode, MethodNode methodNode, AnnotationNode accessor, Type owner, BytecodeFixerUpper bfu, PatchContext context) { - String fieldFqn = AdapterUtil.getAccessorTargetFieldName(classNode.name, methodNode, new AnnotationHandle(accessor), context.environment()) - .orElse(null); - if (fieldFqn == null) return PatchResult.PASS; - - String fieldName = getFieldName(fieldFqn); - Pair updatedTypes = bfu.getFieldTypeChange(owner.getInternalName(), fieldName); - if (updatedTypes == null) return PatchResult.PASS; - - TypeAdapter typeAdapter = bfu.getTypeAdapter(updatedTypes.getSecond(), updatedTypes.getFirst()); - if (typeAdapter == null) return PatchResult.PASS; - - String targetMethod = addRedirectAcceptorField(owner, methodNode, fieldName, typeAdapter, bfu.getGenerator()); - - // Change Accessor to Invoker - methodNode.visibleAnnotations.remove(accessor); - AnnotationVisitor invokerAnn = methodNode.visitAnnotation(MixinAnnotations.INVOKER, true); - invokerAnn.visit("value", targetMethod); - context.environment().auditTrail().recordAudit(this, classNode, "Redirect accessor %s%s to invoker %s", methodNode.name, methodNode.desc, targetMethod); - - return PatchResult.APPLY; - } - - private String addRedirectAcceptorField(Type owner, MethodNode methodNode, String field, TypeAdapter adapter, BytecodeFixerJarGenerator generator) { - ClassNode node = getOrCreateMixinClass(owner, generator); - - String methodName = PREFIX + field; - Type to = adapter.to(); - String methodDesc = Type.getMethodDescriptor(to); - synchronized (node) { - addRedirectAcceptorMethod(node, owner, methodNode, field, adapter, methodName, to, methodDesc); - } - - return methodName + methodDesc; - } - - private static void addRedirectAcceptorMethod(ClassNode node, Type owner, MethodNode methodNode, String field, TypeAdapter adapter, String methodName, Type to, String methodDesc) { - if (node.methods.stream().noneMatch(m -> m.name.equals(methodName))) { - boolean isStatic = MethodHelper.isStatic(methodNode); - MethodNode method = (MethodNode) node.visitMethod(Opcodes.ACC_PUBLIC | (isStatic ? Opcodes.ACC_STATIC : 0), methodName, methodDesc, null, null); - { - AnnotationVisitor annotationVisitor = method.visitAnnotation(MixinAnnotations.UNIQUE, true); - annotationVisitor.visitEnd(); - } - { - method.visitCode(); - if (!isStatic) { - method.visitVarInsn(Opcodes.ALOAD, 0); - } - method.visitFieldInsn(isStatic ? Opcodes.GETSTATIC : Opcodes.GETFIELD, owner.getInternalName(), field, adapter.from().getDescriptor()); - adapter.apply(method.instructions, method.instructions.getLast()); - method.visitInsn(OpcodeUtil.getReturnOpcode(to)); - method.visitEnd(); - } - method.visitEnd(); - } - } - - private ClassNode getOrCreateMixinClass(Type targetClass, BytecodeFixerJarGenerator generator) { - String className = targetClass.getInternalName().replace('/', '_'); - return generator.getOrCreateClass(className, s -> generateFieldAdapterMixin(s, targetClass)); - } - - private ClassNode generateFieldAdapterMixin(String className, Type targetClass) { - ClassNode node = new ClassNode(); - node.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, className, null, TypeConstants.OBJECT_TYPE.getInternalName(), null); - - { - AnnotationVisitor mixinAnnotation = node.visitAnnotation(MixinAnnotations.MIXIN, false); - { - AnnotationVisitor valueVisitor = mixinAnnotation.visitArray("value"); - valueVisitor.visit(null, targetClass); - valueVisitor.visitEnd(); - } - mixinAnnotation.visit("priority", PRIORITY_MAX); - mixinAnnotation.visitEnd(); - } - - return node; - } - - private static String getFieldName(String desc) { - int descIndex = desc.indexOf(':'); - return descIndex == -1 ? desc : desc.substring(0, descIndex); - } -} diff --git a/core/src/main/java/org/sinytra/adapter/types/FieldTypeUsageTransformer.java b/core/src/main/java/org/sinytra/adapter/types/FieldTypeUsageTransformer.java index a094b9ec..383676bc 100644 --- a/core/src/main/java/org/sinytra/adapter/types/FieldTypeUsageTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/types/FieldTypeUsageTransformer.java @@ -1,102 +1,117 @@ package org.sinytra.adapter.types; import com.mojang.datafixers.util.Pair; -import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; -import org.sinytra.adapter.env.ann.ClassTarget; -import org.sinytra.adapter.transform.ClassTransformer; -import org.sinytra.adapter.env.ctx.PatchContext; -import org.sinytra.adapter.env.ctx.PatchResult; -import org.sinytra.adapter.util.AdapterUtil; -import org.slf4j.Logger; +import org.sinytra.adapter.analysis.flow.DataFlow; import org.sinytra.adapter.analysis.method.MethodAnalyzer; +import org.sinytra.adapter.api.BytecodeFixerUpper; +import org.sinytra.adapter.api.ClassPatchContext; +import org.sinytra.adapter.api.ClassTransformer; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.constant.MixinAnnotations; +import org.sinytra.adapter.util.AdapterUtil; +import org.sinytra.adapter.util.MixinBridge; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.sinytra.adapter.util.AdapterUtil.MIXINPATCH; public class FieldTypeUsageTransformer implements ClassTransformer { - private static final Logger LOGGER = LogUtils.getLogger(); - @Override - public PatchResult apply(ClassNode classNode, ClassTarget classTarget, PatchContext context) { + public PatchResult apply(ClassNode classNode, ClassPatchContext context) { BytecodeFixerUpper bfu = context.environment().bytecodeFixerUpper(); - boolean applied = false; - if (bfu != null) { - Map> classUpdatedTypes = new HashMap<>(); - // Update class field types - if (context.targetTypes().size() == 1) { - Type targetType = context.targetTypes().getFirst(); - for (FieldNode field : classNode.fields) { - if (AdapterUtil.isShadowField(field)) { - Pair updatedTypes = bfu.getFieldTypeChange(targetType.getInternalName(), field.name); - if (updatedTypes != null) { - field.desc = updatedTypes.getSecond().getDescriptor(); - // Update shadow field usages - classUpdatedTypes.put(field.name, updatedTypes); - applied = true; - } - } - } - } - // Update field insn types - for (MethodNode method : classNode.methods) { - for (AbstractInsnNode insn : method.instructions) { - if (insn instanceof FieldInsnNode finsn) { - // Update usages of fields that belong to this class - if (finsn.owner.equals(classNode.name)) { - Pair classUpdatedType = classUpdatedTypes.get(finsn.name); - if (classUpdatedType != null) { - applied |= runFieldFix(bfu, classUpdatedType, method, finsn); - continue; - } - } + if (bfu == null) { + return PatchResult.PASS; + } - // Update used fields of other classes - Pair updatedTypes = bfu.getFieldTypeChange(finsn.owner, finsn.name); - if (updatedTypes != null) { - applied |= runFieldFix(bfu, updatedTypes, method, finsn); - } - } - } - // Search for method calls made on modified class fields and update their owners to match the new field types - if (!classUpdatedTypes.isEmpty()) { - AtomicBoolean retargeted = new AtomicBoolean(); - MethodAnalyzer.forEachReceiver(method, (call, receiver) -> { - if (call.getOpcode() == Opcodes.INVOKEVIRTUAL - && AdapterUtil.getSingleOrNull(receiver) instanceof FieldInsnNode finsn - && finsn.owner.equals(classNode.name) - ) { - Pair classUpdatedType = classUpdatedTypes.get(finsn.name); - if (classUpdatedType != null) { - call.owner = classUpdatedType.getSecond().getInternalName(); - retargeted.set(true); - } - } - }); - applied |= retargeted.get(); + Type target = context.singleTarget(); + Map> shadowChanges = target != null ? retypeShadowFields(classNode, target, bfu) : Map.of(); + boolean applied = !shadowChanges.isEmpty(); + for (MethodNode method : classNode.methods) { + applied |= target != null && redirectAccessor(classNode, method, target, bfu, context); + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof FieldInsnNode fieldInsn) { + Pair change = fieldInsn.owner.equals(classNode.name) ? shadowChanges.get(fieldInsn.name) + : bfu.getFieldTypeChange(fieldInsn.owner, fieldInsn.name); + applied |= change != null && adaptFieldInsn(bfu, change, method, fieldInsn, context, classNode); } } + if (!shadowChanges.isEmpty()) { + applied |= retargetReceivers(classNode, method, shadowChanges); + } } return applied ? PatchResult.APPLY : PatchResult.PASS; } - private static boolean runFieldFix(BytecodeFixerUpper bfu, Pair updatedTypes, MethodNode method, FieldInsnNode finsn) { - boolean isStore = finsn.getOpcode() == Opcodes.PUTFIELD || finsn.getOpcode() == Opcodes.PUTSTATIC; - TypeAdapter typeAdapter = isStore - ? bfu.getTypeAdapter(updatedTypes.getFirst(), updatedTypes.getSecond()) - : bfu.getTypeAdapter(updatedTypes.getSecond(), updatedTypes.getFirst()); - if (typeAdapter == null || isStore && finsn.getPrevious() == null) { + private static Map> retypeShadowFields(ClassNode classNode, Type target, BytecodeFixerUpper bfu) { + Map> changes = new HashMap<>(); + for (FieldNode field : classNode.fields) { + Pair change = AdapterUtil.isShadowField(field) ? bfu.getFieldTypeChange(target.getInternalName(), field.name) : null; + if (change != null && field.desc.equals(change.getFirst().getDescriptor())) { + field.desc = change.getSecond().getDescriptor(); + changes.put(field.name, change); + } + } + return changes; + } + + private boolean adaptFieldInsn(BytecodeFixerUpper bfu, Pair change, MethodNode method, FieldInsnNode fieldInsn, ClassPatchContext context, + ClassNode classNode) { + boolean isStore = fieldInsn.getOpcode() == Opcodes.PUTFIELD || fieldInsn.getOpcode() == Opcodes.PUTSTATIC; + TypeAdapter adapter = isStore ? bfu.getTypeAdapter(change.getFirst(), change.getSecond()) : bfu.getTypeAdapter(change.getSecond(), change.getFirst()); + if (adapter == null || !fieldInsn.desc.equals(change.getFirst().getDescriptor()) || isStore && fieldInsn.getPrevious() == null) { return false; } - LOGGER.debug(MIXINPATCH, "Running fixup for field {}.{}{} in method {}{}", finsn.owner, finsn.name, finsn.desc, method.name, method.desc); - finsn.desc = updatedTypes.getSecond().getDescriptor(); - typeAdapter.apply(method.instructions, isStore ? finsn.getPrevious() : finsn); + fieldInsn.desc = change.getSecond().getDescriptor(); + adapter.apply(method.instructions, isStore ? fieldInsn.getPrevious() : fieldInsn); + context.environment().auditTrail().recordAudit(this, classNode, "Adapt %s of field %s.%s in %s%s to type %s", isStore ? "store" : "load", + fieldInsn.owner, fieldInsn.name, method.name, method.desc, change.getSecond()); return true; } + + private static boolean retargetReceivers(ClassNode classNode, MethodNode method, Map> shadowChanges) { + boolean[] retargeted = {false}; + DataFlow.of(method).forEachReceiver((call, receiver) -> { + if (call.getOpcode() == Opcodes.INVOKEVIRTUAL && AdapterUtil.getSingleOrNull(receiver) instanceof FieldInsnNode fieldInsn + && fieldInsn.owner.equals(classNode.name) && shadowChanges.containsKey(fieldInsn.name) + ) { + call.owner = shadowChanges.get(fieldInsn.name).getSecond().getInternalName(); + retargeted[0] = true; + } + }); + return retargeted[0]; + } + + private boolean redirectAccessor(ClassNode classNode, MethodNode method, Type target, BytecodeFixerUpper bfu, ClassPatchContext context) { + AnnotationNode accessor = AdapterUtil.getAnnotation(method.visibleAnnotations, MixinAnnotations.ACCESSOR); + String field = accessor != null && Type.getArgumentCount(method.desc) == 0 && Type.getReturnType(method.desc) != Type.VOID_TYPE + ? getAccessorField(classNode, method, accessor, context) : null; + Pair change = field != null ? bfu.getFieldTypeChange(target.getInternalName(), field) : null; + TypeAdapter adapter = change != null ? bfu.getTypeAdapter(change.getSecond(), change.getFirst()) : null; + if (adapter == null) { + return false; + } + + String getter = AccessorAdapterGenerator.getOrCreateGetter(bfu.getGenerator(), target, field, MethodAnalyzer.isStatic(method), adapter); + method.visibleAnnotations.remove(accessor); + method.visitAnnotation(MixinAnnotations.INVOKER, true).visit("value", getter); + context.environment().auditTrail().recordAudit(this, classNode, "Redirect accessor %s%s to invoker %s", method.name, method.desc, getter); + return true; + } + + @Nullable + private static String getAccessorField(ClassNode classNode, MethodNode method, AnnotationNode accessor, ClassPatchContext context) { + String value = AdapterUtil.getAnnotationValue(accessor, "value"); + String name = value != null && !value.isEmpty() ? value : MixinBridge.getAccessorFieldName(method.name); + if (name == null) { + return null; + } + String remapped = context.environment().refmapHolder().remap(classNode.name, name); + int descIndex = remapped.indexOf(':'); + return descIndex == -1 ? remapped : remapped.substring(0, descIndex); + } } diff --git a/core/src/main/java/org/sinytra/adapter/types/ObjectTypeAdapter.java b/core/src/main/java/org/sinytra/adapter/types/ObjectTypeAdapter.java index 2d65210b..460d9fd9 100644 --- a/core/src/main/java/org/sinytra/adapter/types/ObjectTypeAdapter.java +++ b/core/src/main/java/org/sinytra/adapter/types/ObjectTypeAdapter.java @@ -5,7 +5,8 @@ import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.TypeInsnNode; -import org.sinytra.adapter.env.util.TypeConstants; +import org.sinytra.adapter.api.TypeAdapter; +import org.sinytra.adapter.model.constant.TypeConstants; public class ObjectTypeAdapter implements TypeAdapterProvider { public static final TypeAdapterProvider INSTANCE = new ObjectTypeAdapter(); diff --git a/core/src/main/java/org/sinytra/adapter/types/SupplierTypeAdapter.java b/core/src/main/java/org/sinytra/adapter/types/SupplierTypeAdapter.java index 7336bf2a..1064e74b 100644 --- a/core/src/main/java/org/sinytra/adapter/types/SupplierTypeAdapter.java +++ b/core/src/main/java/org/sinytra/adapter/types/SupplierTypeAdapter.java @@ -6,6 +6,7 @@ import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.TypeInsnNode; +import org.sinytra.adapter.api.TypeAdapter; import java.util.function.Supplier; diff --git a/core/src/main/java/org/sinytra/adapter/types/TypeAdapterProvider.java b/core/src/main/java/org/sinytra/adapter/types/TypeAdapterProvider.java index 2d8b4ceb..9b714a5d 100644 --- a/core/src/main/java/org/sinytra/adapter/types/TypeAdapterProvider.java +++ b/core/src/main/java/org/sinytra/adapter/types/TypeAdapterProvider.java @@ -1,6 +1,7 @@ package org.sinytra.adapter.types; import org.objectweb.asm.Type; +import org.sinytra.adapter.api.TypeAdapter; public interface TypeAdapterProvider { TypeAdapter provide(Type from, Type to); diff --git a/core/src/main/java/org/sinytra/adapter/util/AdapterUtil.java b/core/src/main/java/org/sinytra/adapter/util/AdapterUtil.java index 01b4ec85..d8afe9bd 100644 --- a/core/src/main/java/org/sinytra/adapter/util/AdapterUtil.java +++ b/core/src/main/java/org/sinytra/adapter/util/AdapterUtil.java @@ -10,18 +10,13 @@ import org.objectweb.asm.tree.analysis.SourceValue; import org.objectweb.asm.util.Textifier; import org.objectweb.asm.util.TraceMethodVisitor; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.analysis.selector.AnnotationValueHandle; -import org.sinytra.adapter.env.ctx.PatchEnvironment; -import org.sinytra.adapter.env.util.MixinAnnotations; +import org.sinytra.adapter.model.constant.MixinAnnotations; import org.slf4j.Marker; import org.slf4j.MarkerFactory; -import org.spongepowered.asm.mixin.gen.AccessorInfo; import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; -import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; @@ -44,31 +39,11 @@ public static MethodNode copyMethod(MethodNode original) { return copy; } - public static String randomString(int length) { - int leftLimit = 48; // numeral '0' - int rightLimit = 122; // letter 'z' - Random random = new Random(); - - return random.ints(leftLimit, rightLimit + 1) - .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) - .limit(length) - .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) - .toString(); - } - public static boolean isAnonymousClass(String name) { // Regex: second to last char in class name must be '$', and the class name must end with a number return name.matches("^.+\\$\\d+$"); } - public static Optional getAccessorTargetFieldName(String owner, MethodNode method, AnnotationHandle annotationHandle, PatchEnvironment environment) { - return annotationHandle.getValue("value") - .map(AnnotationValueHandle::get) - .filter(str -> !str.isEmpty()) - .or(() -> Optional.ofNullable(AccessorInfo.AccessorName.of(method.name)) - .map(name -> environment.refmapHolder().remap(owner, name.name))); - } - @Nullable public static SingleValueHandle handleLocalVarInsnValue(AbstractInsnNode insn) { if (insn instanceof VarInsnNode varInsn) { @@ -93,13 +68,23 @@ public static void replaceLVT(MethodNode methodNode, Int2IntFunction operator) { } } - public static boolean canHandleLocalVarInsnValue(AbstractInsnNode insn) { - return insn instanceof VarInsnNode || insn instanceof IincInsnNode; + public static void shiftLocals(MethodNode methodNode, int delta) { + if (methodNode.localVariables != null) { + methodNode.localVariables.forEach(lvn -> lvn.index += delta); + } + replaceLVT(methodNode, index -> Math.max(index + delta, 0)); } - public static int getInsnIntConstValue(AbstractInsnNode insn) { - return getIntConstValue(insn) - .orElseThrow(() -> new IllegalArgumentException("Not an int constant opcode: " + insn.getOpcode())); + public static void makeStatic(MethodNode methodNode) { + methodNode.access |= Opcodes.ACC_STATIC; + if (methodNode.localVariables != null) { + methodNode.localVariables.removeIf(lvn -> lvn.index == 0); + } + shiftLocals(methodNode, -1); + } + + public static boolean canHandleLocalVarInsnValue(AbstractInsnNode insn) { + return insn instanceof VarInsnNode || insn instanceof IincInsnNode; } public static OptionalInt getIntConstValue(AbstractInsnNode insn) { @@ -143,8 +128,33 @@ public static boolean isShadowField(FieldNode field) { return AdapterUtil.hasAnnotation(field.visibleAnnotations, MixinAnnotations.SHADOW); } - public static boolean hasAnnotation(List annotations, String desc) { - return annotations != null && annotations.stream().anyMatch(an -> desc.equals(an.desc)); + public static boolean hasAnnotation(@Nullable List annotations, String desc) { + return getAnnotation(annotations, desc) != null; + } + + @Nullable + public static AnnotationNode getAnnotation(@Nullable List annotations, String desc) { + if (annotations != null) { + for (AnnotationNode annotation : annotations) { + if (desc.equals(annotation.desc)) { + return annotation; + } + } + } + return null; + } + + @Nullable + @SuppressWarnings("unchecked") + public static T getAnnotationValue(AnnotationNode annotation, String key) { + if (annotation.values != null) { + for (int i = 0; i + 1 < annotation.values.size(); i += 2) { + if (key.equals(annotation.values.get(i))) { + return (T) annotation.values.get(i + 1); + } + } + } + return null; } // Adapted from org.spongepowered.asm.mixin.injection.callback.CallbackInjector summariseLocals @@ -216,20 +226,6 @@ public static Optional findOne(Collection candidates, Predicate boolean allElementsEqual(Collection list, BiPredicate equalityFn) { - Iterator iterator = list.iterator(); - if (!iterator.hasNext()) { - return true; - } - T first = iterator.next(); - while (iterator.hasNext()) { - if (!equalityFn.test(first, iterator.next())) { - return false; - } - } - return true; - } - public static void replaceRangeInclusive(InsnList list, AbstractInsnNode start, AbstractInsnNode end, List replacement) { InsnList newInsns = AdapterUtil.insnList(replacement); list.insertBefore(start, newInsns); diff --git a/core/src/main/java/org/sinytra/adapter/util/GeneratedVariables.java b/core/src/main/java/org/sinytra/adapter/util/GeneratedVariables.java index 7d82ebdd..71d83282 100644 --- a/core/src/main/java/org/sinytra/adapter/util/GeneratedVariables.java +++ b/core/src/main/java/org/sinytra/adapter/util/GeneratedVariables.java @@ -1,16 +1,13 @@ package org.sinytra.adapter.util; -import com.google.common.base.Strings; import com.mojang.datafixers.util.Pair; import org.objectweb.asm.Type; import java.util.*; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; public class GeneratedVariables { - private static final String MOJMAP_PARAM_NAME_PREFIX = "p_"; // Obfuscated variable names consist of '$$' followed by a number private static final String OBF_VAR_PATTERN = "^\\$\\$\\d+$"; private static final Map GENERATED_VAR_NAMES = new HashMap<>(); @@ -44,8 +41,8 @@ public class GeneratedVariables { }); } - public static boolean isGeneratedVariableName(String name, Type type) { - if (name.startsWith(MOJMAP_PARAM_NAME_PREFIX) || name.matches(OBF_VAR_PATTERN)) { + public static boolean isPositionalName(String name, Type type) { + if (name.matches(OBF_VAR_PATTERN)) { return true; } GeneratedVarName generator = GENERATED_VAR_NAMES.get(type); @@ -60,11 +57,6 @@ public static boolean isGeneratedVariableName(String name, Type type) { return knownGenerated; } - public static OptionalInt getGeneratedVariableOrdinal(String name, Type type) { - GeneratedVarName generator = GENERATED_VAR_NAMES.get(type); - return generator != null ? generator.getOrdinal(name) : OptionalInt.empty(); - } - public static class GeneratedVarName { private final Set types; private final Pattern pattern; @@ -87,18 +79,5 @@ public Set getTypes() { public boolean test(String str) { return this.pattern.matcher(str).matches(); } - - public OptionalInt getOrdinal(String str) { - Matcher matcher = this.pattern.matcher(str); - if (matcher.matches()) { - String ordinal = matcher.group("index"); - if (Strings.isNullOrEmpty(ordinal)) { - return OptionalInt.of(0); - } - int intOrdinal = Integer.parseInt(ordinal); - return OptionalInt.of(intOrdinal); - } - return OptionalInt.empty(); - } } } diff --git a/core/src/main/java/org/sinytra/adapter/util/MixinBridge.java b/core/src/main/java/org/sinytra/adapter/util/MixinBridge.java new file mode 100644 index 00000000..a35ad4d1 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/util/MixinBridge.java @@ -0,0 +1,553 @@ +package org.sinytra.adapter.util; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.api.RefmapHolder; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.FabricUtil; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.extensibility.IMixinConfig; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigSource; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import org.spongepowered.asm.mixin.gen.AccessorInfo; +import org.spongepowered.asm.mixin.injection.Group; +import org.spongepowered.asm.mixin.injection.InjectionPoint; +import org.spongepowered.asm.mixin.injection.code.ISliceContext; +import org.spongepowered.asm.mixin.injection.code.MethodSlice; +import org.spongepowered.asm.mixin.injection.modify.InvalidImplicitDiscriminatorException; +import org.spongepowered.asm.mixin.injection.modify.LocalVariableDiscriminator; +import org.spongepowered.asm.mixin.injection.points.BeforeConstant; +import org.spongepowered.asm.mixin.injection.selectors.ISelectorContext; +import org.spongepowered.asm.mixin.injection.selectors.InvalidSelectorException; +import org.spongepowered.asm.mixin.injection.struct.*; +import org.spongepowered.asm.mixin.refmap.IMixinContext; +import org.spongepowered.asm.mixin.refmap.IReferenceMapper; +import org.spongepowered.asm.mixin.struct.AnnotatedMethodInfo; +import org.spongepowered.asm.mixin.struct.SpecialMethodInfo; +import org.spongepowered.asm.mixin.transformer.ClassInfo; +import org.spongepowered.asm.mixin.transformer.ext.Extensions; +import org.spongepowered.asm.util.Annotations; +import org.spongepowered.asm.util.Locals; +import org.spongepowered.asm.util.asm.IAnnotationHandle; +import sun.misc.Unsafe; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class MixinBridge { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final MethodHandles.Lookup TRUSTED_LOOKUP; + private static final Unsafe UNSAFE; + private static final MethodHandle TARGET_CTR; + private static final Class LOCAL_VARIABLE_INJECTION_POINT_CLASS; + private static final VarHandle ANNOTATED_METHOD_INFO_CONTEXT; + private static final MethodHandle LOCAL_VARIABLE_INJECTION_POINT_FIND; + private static final VarHandle INJECTION_POINT_TYPES; + private static final String CLS_REGEX = "^([A-Za-z_][A-Za-z0-9_]*[\\.\\$])+[A-Za-z_][A-Za-z0-9_]*$"; + private static final String EXPRESSION_POINT_CLASS = "com.llamalad7.mixinextras.expression.impl.point.ExpressionInjectionPoint"; + private static final String CONFIG_NAME = "adapter.mixinbridge.json"; + private static final Object LOCALS_LOCK = new Object(); + + static { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + UNSAFE = (Unsafe) theUnsafe.get(null); + Field hackfield = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + TRUSTED_LOOKUP = (MethodHandles.Lookup) UNSAFE.getObject(UNSAFE.staticFieldBase(hackfield), UNSAFE.staticFieldOffset(hackfield)); + + TARGET_CTR = MethodHandles.privateLookupIn(Target.class, MethodHandles.lookup()).findConstructor(Target.class, MethodType.methodType(void.class, ClassInfo.class, ClassNode.class, MethodNode.class)); + + LOCAL_VARIABLE_INJECTION_POINT_CLASS = Class.forName("org.spongepowered.asm.mixin.injection.modify.ModifyVariableInjector$LocalVariableInjectionPoint"); + ANNOTATED_METHOD_INFO_CONTEXT = TRUSTED_LOOKUP.findVarHandle(AnnotatedMethodInfo.class, "context", IMixinContext.class); + INJECTION_POINT_TYPES = MethodHandles.privateLookupIn(InjectionPoint.class, MethodHandles.lookup()).findStaticVarHandle(InjectionPoint.class, "types", Map.class); + LOCAL_VARIABLE_INJECTION_POINT_FIND = MethodHandles.privateLookupIn(LOCAL_VARIABLE_INJECTION_POINT_CLASS, MethodHandles.lookup()).findVirtual(LOCAL_VARIABLE_INJECTION_POINT_CLASS, "find", MethodType.methodType(boolean.class, InjectionInfo.class, InsnList.class, Collection.class, Target.class)); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public static Session session(String mixinClass, MethodNode mixinMethod, ClassNode targetClass, MethodNode targetMethod, PatchEnvironment environment) { + return session(mixinClass, mixinMethod, targetClass, targetMethod, environment.refmapHolder(), environment.fabricLVTCompatibility()); + } + + public static Session session(String mixinClass, MethodNode mixinMethod, ClassNode targetClass, MethodNode targetMethod, RefmapHolder refmapHolder, int lvtCompatibility) { + return new Session(new ClassMixinContext(mixinClass, targetClass.name, refmapHolder, lvtCompatibility), mixinMethod, targetClass, targetMethod, lvtCompatibility); + } + + public static LocalVariableNode[] getLocalsAt(ClassNode targetClass, MethodNode targetMethod, AbstractInsnNode insn, int lvtCompatLevel) { + // Synchronize to avoid issues in mixin. This is necessary. + synchronized (LOCALS_LOCK) { + return Locals.getLocalsAt(targetClass, targetMethod, insn, lvtCompatLevel); + } + } + + @SuppressWarnings("unchecked") + public static boolean isKnownInjectionPointType(String value) { + Map> types = (Map>) INJECTION_POINT_TYPES.get(); + return types.containsKey(value.toUpperCase(Locale.ROOT)) || value.matches(CLS_REGEX); + } + + @Nullable + public static String getAccessorFieldName(String methodName) { + AccessorInfo.AccessorName name = AccessorInfo.AccessorName.of(methodName); + return name != null ? name.name : null; + } + + @Nullable + public static String getGroupName(MethodNode method) { + AnnotationNode node = Annotations.getInvisible(method, Group.class); + if (node == null) return null; + + String name = Annotations.getValue(node, "name"); + return name == null || name.isEmpty() ? null : name; + } + + public static List findInjectorGroups(List methods) { + InjectorGroupInfo.Map infos = new InjectorGroupInfo.Map(); + Multimap members = HashMultimap.create(); + + for (MethodNode method : methods) { + InjectorGroupInfo info = infos.parseGroup(method, "default"); + if (info.isDefault() || info.getName().equals("default")) continue; + + members.put(info.getName(), method); + } + + List groups = new ArrayList<>(); + members.asMap().forEach((name, mems) -> + groups.add(new InjectorGroup(name, List.copyOf(mems), infos.forName(name).getMinRequired()))); + return groups; + } + + public record InjectorGroup(String name, List members, int minRequired) { + } + + public record MemberSelector(@Nullable String owner, @Nullable String name, @Nullable String desc, int maxMatches, boolean field) { + } + + @Nullable + public static MemberSelector parseMemberSelector(String input) { + String selector = input.strip(); + if (selector.startsWith("/") || selector.startsWith("@")) { + return null; + } + try { + MemberInfo info = MemberInfo.parse(selector, null).validate(); + return new MemberSelector(info.getOwner(), info.getName(), info.getDesc(), info.getMaxMatchCount(), info.isField()); + } catch (InvalidSelectorException e) { + return null; + } + } + + public record LocalCandidate(int ordinal, int index) { + } + + public static final class Session { + private final IMixinContext mixinContext; + private final int lvtCompatibility; + private final MethodNode mixinMethod; + private final ClassNode targetClass; + private final MethodNode targetMethod; + @Nullable + private Target target; + + private Session(IMixinContext mixinContext, MethodNode mixinMethod, ClassNode targetClass, MethodNode targetMethod, int lvtCompatibility) { + this.mixinContext = mixinContext; + this.lvtCompatibility = lvtCompatibility; + this.mixinMethod = mixinMethod; + this.targetClass = targetClass; + this.targetMethod = targetMethod; + } + + public List findInjectionPoint(AnnotationNode parent, AnnotationNode at, @Nullable AnnotationNode slice) { + try { + InjectionPoint point = InjectionPoint.parse(this.mixinContext, this.mixinMethod, parent, at); + return find(point, parent, slice); + } catch (Throwable e) { + LOGGER.error("Error finding injection insns", e); + return List.of(); + } + } + + public List findConstant(AnnotationNode constant, String returnDesc, @Nullable AnnotationNode slice) { + try { + return find(new BeforeConstant(this.mixinContext, constant, returnDesc), null, slice); + } catch (Throwable e) { + LOGGER.error("Error finding injection insns", e); + return List.of(); + } + } + + public InsnList slice(AnnotationNode slice) { + ISliceContext sliceContext = new MethodSliceContext(this.mixinContext, this.mixinMethod); + return MethodSlice.parse(sliceContext, slice).getSlice(target()); + } + + public int findLocal(AnnotationNode selector, Type type, AbstractInsnNode insn) { + LocalVariableDiscriminator discriminator = LocalVariableDiscriminator.parse(selector); + try { + return discriminator.findLocal(localsContext(type, discriminator.isArgsOnly(), insn)); + } catch (InvalidImplicitDiscriminatorException e) { + return -1; + } + } + + public List findLocalCandidates(Type type, AbstractInsnNode insn, boolean argsOnly) { + LocalVariableDiscriminator.Context context = localsContext(type, argsOnly, insn); + List candidates = new ArrayList<>(); + // Ordinals start at slot 0, so the first ordinal of a type may belong to `this` and select nothing + for (int ordinal = 0; ordinal < this.targetMethod.maxLocals; ordinal++) { + int index = new LocalVariableDiscriminator(argsOnly, ordinal, -1, Set.of(), false).findLocal(context); + if (index != -1) { + candidates.add(new LocalCandidate(ordinal, index)); + } + } + return candidates; + } + + private LocalVariableDiscriminator.Context localsContext(Type type, boolean argsOnly, AbstractInsnNode insn) { + InjectionInfo injectionInfo = createInjectionInfo(CallbackInjectionInfo.class, this.mixinContext, this.lvtCompatibility); + // The context reads the locals through Locals.getLocalsAt, which isn't thread safe + synchronized (LOCALS_LOCK) { + return new LocalVariableDiscriminator.Context(injectionInfo, type, argsOnly, target(), insn); + } + } + + private List find(InjectionPoint point, @Nullable AnnotationNode parent, @Nullable AnnotationNode slice) throws Throwable { + List targetInsns = new ArrayList<>(); + if (LOCAL_VARIABLE_INJECTION_POINT_CLASS.isInstance(point)) { + // Local variable injection points need a ModifyVariable injector, and always search the whole method + InjectionInfo info = createInjectionInfo(ModifyVariableInjectionInfo.class, this.mixinContext, this.lvtCompatibility); + LOCAL_VARIABLE_INJECTION_POINT_FIND.invoke(point, info, this.targetMethod.instructions, targetInsns, target()); + } else { + InsnList instructions = slice != null ? slice(slice) : this.targetMethod.instructions; + if (parent != null && point.getClass().getName().equals(EXPRESSION_POINT_CLASS)) { + Expressions.find(this, point, parent, instructions, targetInsns); + } else { + point.find(this.targetMethod.desc, instructions, targetInsns); + } + } + return targetInsns; + } + + private Target target() { + if (this.target == null) { + ClassInfo info = ClassInfo.forName(this.targetClass.name); + try { + this.target = (Target) TARGET_CTR.invoke(info, this.targetClass, this.targetMethod); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + return this.target; + } + } + + private static final class Expressions { + private static final Object LOCK = new Object(); + private static final String MIN_VERSION = "0.5.0"; + private static final Map INJECTION_INFOS = Map.ofEntries( + Map.entry("Lorg/spongepowered/asm/mixin/injection/Inject;", "org.spongepowered.asm.mixin.injection.struct.CallbackInjectionInfo"), + Map.entry("Lorg/spongepowered/asm/mixin/injection/ModifyArg;", "org.spongepowered.asm.mixin.injection.struct.ModifyArgInjectionInfo"), + Map.entry("Lorg/spongepowered/asm/mixin/injection/ModifyArgs;", "org.spongepowered.asm.mixin.injection.struct.ModifyArgsInjectionInfo"), + Map.entry("Lorg/spongepowered/asm/mixin/injection/ModifyConstant;", "org.spongepowered.asm.mixin.injection.struct.ModifyConstantInjectionInfo"), + Map.entry("Lorg/spongepowered/asm/mixin/injection/ModifyVariable;", "org.spongepowered.asm.mixin.injection.struct.ModifyVariableInjectionInfo"), + Map.entry("Lorg/spongepowered/asm/mixin/injection/Redirect;", "org.spongepowered.asm.mixin.injection.struct.RedirectInjectionInfo"), + Map.entry("Lcom/llamalad7/mixinextras/injector/ModifyExpressionValue;", "com.llamalad7.mixinextras.injector.ModifyExpressionValueInjectionInfo"), + Map.entry("Lcom/llamalad7/mixinextras/injector/ModifyReceiver;", "com.llamalad7.mixinextras.injector.ModifyReceiverInjectionInfo"), + Map.entry("Lcom/llamalad7/mixinextras/injector/ModifyReturnValue;", "com.llamalad7.mixinextras.injector.ModifyReturnValueInjectionInfo"), + Map.entry("Lcom/llamalad7/mixinextras/injector/wrapoperation/WrapOperation;", "com.llamalad7.mixinextras.injector.wrapoperation.WrapOperationInjectionInfo"), + Map.entry("Lcom/llamalad7/mixinextras/injector/WrapWithCondition;", "com.llamalad7.mixinextras.injector.v2.WrapWithConditionInjectionInfo"), + Map.entry("Lcom/llamalad7/mixinextras/injector/v2/WrapWithCondition;", "com.llamalad7.mixinextras.injector.v2.WrapWithConditionInjectionInfo") + ); + private static final VarHandle INFO_METHOD; + private static final VarHandle INFO_ANNOTATION; + private static final VarHandle INFO_CLASS_NODE; + private static final Map, Handles> HANDLES = new ConcurrentHashMap<>(); + + private record Handles(VarHandle currentInfo, VarHandle currentTargets, Map> injectionInfos) { + } + + static { + try { + INFO_METHOD = TRUSTED_LOOKUP.findVarHandle(AnnotatedMethodInfo.class, "method", MethodNode.class); + INFO_ANNOTATION = TRUSTED_LOOKUP.findVarHandle(AnnotatedMethodInfo.class, "annotation", AnnotationNode.class); + INFO_CLASS_NODE = TRUSTED_LOOKUP.findVarHandle(SpecialMethodInfo.class, "classNode", ClassNode.class); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + public static void find(Session session, InjectionPoint point, AnnotationNode parent, InsnList instructions, List found) { + Handles handles = HANDLES.computeIfAbsent(point.getClass(), Expressions::createHandles); + Class infoType = handles.injectionInfos().getOrDefault(parent.desc, CallbackInjectionInfo.class); + InjectionInfo info = createInjectionInfo(infoType, session.mixinContext, session.lvtCompatibility); + INFO_METHOD.set(info, session.mixinMethod); + INFO_ANNOTATION.set(info, parent); + INFO_CLASS_NODE.set(info, session.targetClass); + synchronized (LOCK) { + Object previousInfo = handles.currentInfo().get(); + Object previousTargets = handles.currentTargets().get(); + try { + handles.currentInfo().set(info); + handles.currentTargets().set(new ArrayList<>(List.of(session.target()))); + point.find(session.targetMethod.desc, instructions, found); + } finally { + handles.currentInfo().set(previousInfo); + handles.currentTargets().set(previousTargets); + } + } + } + + @SuppressWarnings("unchecked") + private static Handles createHandles(Class pointClass) { + try { + ClassLoader loader = pointClass.getClassLoader(); + Class configUtils = Class.forName("com.llamalad7.mixinextras.utils.MixinConfigUtils", true, loader); + Class config = Class.forName("com.llamalad7.mixinextras.config.MixinExtrasConfig", true, loader); + Map configs = (Map) TRUSTED_LOOKUP.unreflectVarHandle(configUtils.getDeclaredField("CONFIG_CACHE")).get(); + Object declared = config.getConstructor(String.class, config, String.class).newInstance(CONFIG_NAME, null, MIN_VERSION); + synchronized (configs) { + configs.putIfAbsent(CONFIG_NAME, declared); + } + + Map> injectionInfos = new HashMap<>(); + for (Map.Entry entry : INJECTION_INFOS.entrySet()) { + try { + injectionInfos.put(entry.getKey(), (Class) Class.forName(entry.getValue(), false, loader)); + } catch (ClassNotFoundException ignored) { + } + } + return new Handles( + TRUSTED_LOOKUP.unreflectVarHandle(pointClass.getDeclaredField("CURRENT_INFO")), + TRUSTED_LOOKUP.unreflectVarHandle(pointClass.getDeclaredField("CURRENT_TARGETS")), + injectionInfos + ); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } + + private static InjectionInfo createInjectionInfo(Class type, IMixinContext mixinContext, int lvtCompatibility) { + try { + InjectionInfo info = (InjectionInfo) UNSAFE.allocateInstance(type); + ANNOTATED_METHOD_INFO_CONTEXT.set(info, mixinContext); + CompatibilityContexts.attach(info, lvtCompatibility); + return info; + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + } + + private static final class CompatibilityContexts { + @Nullable + private static final VarHandle[] HANDLES; + @Nullable + private static final Class[] CLASSES; + private static final Map CONTEXTS = new ConcurrentHashMap<>(); + + static { + VarHandle[] handles = null; + Class[] classes = null; + try { + Class targetContext = Class.forName("org.spongepowered.asm.mixin.transformer.MixinTargetContext"); + Class mixinInfo = Class.forName("org.spongepowered.asm.mixin.transformer.MixinInfo"); + Class mixinConfig = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig"); + handles = new VarHandle[]{ + TRUSTED_LOOKUP.findVarHandle(SpecialMethodInfo.class, "mixin", targetContext), + TRUSTED_LOOKUP.findVarHandle(targetContext, "mixin", mixinInfo), + TRUSTED_LOOKUP.findVarHandle(mixinInfo, "parent", mixinConfig), + TRUSTED_LOOKUP.findVarHandle(mixinConfig, "decorations", Map.class), + TRUSTED_LOOKUP.findVarHandle(mixinConfig, "name", String.class) + }; + classes = new Class[]{targetContext, mixinInfo, mixinConfig}; + } catch (Throwable t) { + LOGGER.debug("Mixin target contexts are unavailable, local variable compatibility uses the selector context", t); + } + HANDLES = handles; + CLASSES = classes; + } + + public static void attach(InjectionInfo info, int lvtCompatibility) { + if (HANDLES != null) { + HANDLES[0].set(info, CONTEXTS.computeIfAbsent(lvtCompatibility, CompatibilityContexts::create)); + } + } + + private static Object create(int lvtCompatibility) { + try { + Object config = UNSAFE.allocateInstance(CLASSES[2]); + HANDLES[3].set(config, new HashMap<>(Map.of(FabricUtil.KEY_COMPATIBILITY, lvtCompatibility))); + HANDLES[4].set(config, CONFIG_NAME); + Object mixinInfo = UNSAFE.allocateInstance(CLASSES[1]); + HANDLES[2].set(mixinInfo, config); + Object context = UNSAFE.allocateInstance(CLASSES[0]); + HANDLES[1].set(context, mixinInfo); + return context; + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + } + } + + private record MethodSliceContext(IMixinContext context, MethodNode methodNode) implements ISliceContext { + @Override + public IMixinContext getMixin() { + return this.context; + } + + @Override + public String remap(String reference) { + return this.context.getReferenceMapper().remap(this.context.getClassName(), reference); + } + + //@formatter:off + @Override public String getElementDescription() {return "dummy";} + @Override public MethodSlice getSlice(String id) {throw new UnsupportedOperationException();} + @Override public MethodNode getMethod() {return this.methodNode;} + @Override public AnnotationNode getAnnotationNode() {throw new UnsupportedOperationException();} + @Override public ISelectorContext getParent() {throw new UnsupportedOperationException();} + @Override public IAnnotationHandle getAnnotation() {throw new UnsupportedOperationException();} + @Override public IAnnotationHandle getSelectorAnnotation() {throw new UnsupportedOperationException();} + @Override public String getSelectorCoordinate(boolean leaf) {throw new UnsupportedOperationException();} + @Override public void addMessage(String format, Object... args) {} + //@formatter:on + } + + private record ReferenceRemapper(RefmapHolder refmapHolder) implements IReferenceMapper { + @Override + public String remapWithContext(String context, String className, String reference) { + return this.refmapHolder.remap(className, reference); + } + + //@formatter:off + @Override public boolean isDefault() {return false;} + @Override public String getResourceName() {return null;} + @Override public String getStatus() {return null;} + @Override public String getContext() {return null;} + @Override public void setContext(String context) {} + @Override public String remap(String className, String reference) {return remapWithContext(null, className, reference);} + //@formatter:on + } + + private record DummyMixinConfig(int lvtCompatibility) implements IMixinConfig { + @Override + public MixinEnvironment getEnvironment() { + return MixinEnvironment.getCurrentEnvironment(); + } + + @Override + public boolean hasDecoration(String key) { + return FabricUtil.KEY_COMPATIBILITY.equals(key); + } + + @Override + @SuppressWarnings("unchecked") + public V getDecoration(String key) { + return FabricUtil.KEY_COMPATIBILITY.equals(key) ? (V) (Integer) this.lvtCompatibility : null; + } + + //@formatter:off + @Override public IMixinConfigSource getSource() {throw new UnsupportedOperationException();} + @Override public String getCleanSourceId() {return "dummy";} + @Override public String getName() {return CONFIG_NAME;} + @Override public String getMixinPackage() {throw new UnsupportedOperationException();} + @Override public int getPriority() {return 0;} + @Override public IMixinConfigPlugin getPlugin() {return null;} + @Override public boolean isRequired() {throw new UnsupportedOperationException();} + @Override public Set getTargets() {throw new UnsupportedOperationException();} + @Override public void decorate(String key, V value) {} + //@formatter:on + } + + private record DummyMixinInfo(IMixinConfig config) implements IMixinInfo { + @Override + public IMixinConfig getConfig() { + return config; + } + + //@formatter:off + @Override public String getName() {throw new UnsupportedOperationException();} + @Override public String getClassName() {throw new UnsupportedOperationException();} + @Override public String getClassRef() {throw new UnsupportedOperationException();} + @Override public byte[] getClassBytes() {throw new UnsupportedOperationException();} + @Override public boolean isDetachedSuper() {throw new UnsupportedOperationException();} + @Override public ClassNode getClassNode(int flags) {throw new UnsupportedOperationException();} + @Override public List getTargetClasses() {throw new UnsupportedOperationException();} + @Override public int getPriority() {throw new UnsupportedOperationException();} + @Override public MixinEnvironment.Phase getPhase() {throw new UnsupportedOperationException();} + //@formatter:on + } + + private static final class ClassMixinContext implements IMixinContext { + private final String className; + private final String targetClass; + private final ReferenceRemapper referenceRemapper; + private final IMixinInfo mixinInfo; + + public ClassMixinContext(String className, String targetClass, RefmapHolder refmapHolder, int lvtCompatibility) { + this.className = className; + this.targetClass = targetClass; + this.referenceRemapper = new ReferenceRemapper(refmapHolder); + this.mixinInfo = new DummyMixinInfo(new DummyMixinConfig(lvtCompatibility)); + } + + @Override + public IMixinInfo getMixin() { + return this.mixinInfo; + } + + @Override + public Extensions getExtensions() { + throw new UnsupportedOperationException(); + } + + @Override + public String getClassName() { + return this.className.replace('/', '.'); + } + + @Override + public String getClassRef() { + return this.className; + } + + @Override + public String getTargetClassName() { + return this.targetClass.replace('/', '.'); + } + + @Override + public String getTargetClassRef() { + return this.targetClass; + } + + @Override + public IReferenceMapper getReferenceMapper() { + return this.referenceRemapper; + } + + @Override + public boolean getOption(MixinEnvironment.Option option) { + return false; + } + + @Override + public int getPriority() { + return 0; + } + } +} diff --git a/core/src/main/java/org/sinytra/adapter/util/OpcodeUtil.java b/core/src/main/java/org/sinytra/adapter/util/OpcodeUtil.java index 99055010..f54a2298 100644 --- a/core/src/main/java/org/sinytra/adapter/util/OpcodeUtil.java +++ b/core/src/main/java/org/sinytra/adapter/util/OpcodeUtil.java @@ -22,10 +22,6 @@ public static int getLoadOpcode(Type type) { return type.getOpcode(Opcodes.ILOAD); } - public static int getStoreOpcode(Type type) { - return type.getOpcode(Opcodes.ISTORE); - } - public static int getReturnOpcode(MethodNode methodNode) { return getReturnOpcode(Type.getReturnType(methodNode.desc)); } diff --git a/core/src/main/java/org/sinytra/adapter/env/util/OrderedRegistry.java b/core/src/main/java/org/sinytra/adapter/util/OrderedRegistry.java similarity index 98% rename from core/src/main/java/org/sinytra/adapter/env/util/OrderedRegistry.java rename to core/src/main/java/org/sinytra/adapter/util/OrderedRegistry.java index 4a433131..9c7bebbf 100644 --- a/core/src/main/java/org/sinytra/adapter/env/util/OrderedRegistry.java +++ b/core/src/main/java/org/sinytra/adapter/util/OrderedRegistry.java @@ -1,4 +1,4 @@ -package org.sinytra.adapter.env.util; +package org.sinytra.adapter.util; import org.jetbrains.annotations.Nullable; diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts deleted file mode 100644 index 33011ba0..00000000 --- a/runtime/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -val runtimeVersion = project.property("runtimeVersion") as String -val versionMc = project.property("versionMc") as String - -version = "$runtimeVersion+$versionMc" - -println("Runtime version: $version") - -dependencies { - implementation("net.fabricmc:sponge-mixin:0.14.0+mixin.0.8.6") - implementation(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.6")!!) -} - -tasks.jar { - manifest { - manifest.attributes( - "Implementation-Version" to project.version, - "MixinConfigs" to "adapter.init.mixins.json", - "FMLModType" to "GAMELIBRARY" - ) - } -} diff --git a/runtime/src/main/java/org/sinytra/adapter/runtime/AdapterMixinPlugin.java b/runtime/src/main/java/org/sinytra/adapter/runtime/AdapterMixinPlugin.java deleted file mode 100644 index 19dac980..00000000 --- a/runtime/src/main/java/org/sinytra/adapter/runtime/AdapterMixinPlugin.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.sinytra.adapter.runtime; - -import org.objectweb.asm.tree.ClassNode; -import org.sinytra.adapter.runtime.inject.InstanceOfInjectionPoint; -import org.sinytra.adapter.runtime.inject.ModifyInstanceofValueInjectionInfo; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; -import org.spongepowered.asm.mixin.extensibility.IMixinInfo; -import org.spongepowered.asm.mixin.injection.InjectionPoint; -import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; - -import java.util.List; -import java.util.Set; - -public class AdapterMixinPlugin implements IMixinConfigPlugin { - - static { - InjectionPoint.register(InstanceOfInjectionPoint.class, null); - InjectionInfo.register(ModifyInstanceofValueInjectionInfo.class); - } - - //@formatter:off - @Override public void onLoad(String mixinPackage) {} - @Override public String getRefMapperConfig() {return "";} - @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {return true;} - @Override public void acceptTargets(Set myTargets, Set otherTargets) {} - @Override public List getMixins() {return List.of();} - @Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} - @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} - //@formatter:on -} diff --git a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/InstanceOfInjectionPoint.java b/runtime/src/main/java/org/sinytra/adapter/runtime/inject/InstanceOfInjectionPoint.java deleted file mode 100644 index aafd3a1e..00000000 --- a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/InstanceOfInjectionPoint.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.sinytra.adapter.runtime.inject; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.TypeInsnNode; -import org.spongepowered.asm.mixin.injection.InjectionPoint; -import org.spongepowered.asm.mixin.injection.selectors.ITargetSelectorByName; -import org.spongepowered.asm.mixin.injection.struct.InjectionPointData; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -@InjectionPoint.AtCode(value = "INSTANCEOF", namespace = "sinytra") -public class InstanceOfInjectionPoint extends InjectionPoint { - private final String target; - - public InstanceOfInjectionPoint(InjectionPointData data) { - super(data); - - this.target = ((ITargetSelectorByName) data.getTarget()).getOwner(); - } - - @Override - public boolean find(String s, InsnList insns, Collection nodes) { - List found = new ArrayList<>(); - - for (AbstractInsnNode insn : insns) { - if (insn instanceof TypeInsnNode type && insn.getOpcode() == Opcodes.INSTANCEOF && type.desc.equals(this.target)) { - found.add(insn); - } - } - - nodes.addAll(found); - return !found.isEmpty(); - } -} diff --git a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValue.java b/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValue.java deleted file mode 100644 index 5c1437c2..00000000 --- a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.sinytra.adapter.runtime.inject; - -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Slice; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface ModifyInstanceofValue { - String[] method(); - - At[] at(); - - Slice[] slice() default {}; - - boolean remap() default true; - - int require() default -1; - - int expect() default 1; - - int allow() default -1; -} diff --git a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValueInjectionInfo.java b/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValueInjectionInfo.java deleted file mode 100644 index d8516449..00000000 --- a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValueInjectionInfo.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.sinytra.adapter.runtime.inject; - -import com.llamalad7.mixinextras.injector.MixinExtrasInjectionInfo; -import org.objectweb.asm.tree.AnnotationNode; -import org.objectweb.asm.tree.MethodNode; -import org.spongepowered.asm.mixin.injection.code.Injector; -import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; -import org.spongepowered.asm.mixin.transformer.MixinTargetContext; - -@InjectionInfo.AnnotationType(ModifyInstanceofValue.class) -@InjectionInfo.HandlerPrefix("modifyInstanceofValue") -public class ModifyInstanceofValueInjectionInfo extends MixinExtrasInjectionInfo { - - public ModifyInstanceofValueInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { - super(mixin, method, annotation); - } - - @Override - protected Injector parseInjector(AnnotationNode injectAnnotation) { - return new ModifyInstanceofValueInjector(this); - } -} diff --git a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValueInjector.java b/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValueInjector.java deleted file mode 100644 index a3d9612c..00000000 --- a/runtime/src/main/java/org/sinytra/adapter/runtime/inject/ModifyInstanceofValueInjector.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.sinytra.adapter.runtime.inject; - -import com.llamalad7.mixinextras.injector.StackExtension; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.InsnNode; -import org.objectweb.asm.tree.VarInsnNode; -import org.spongepowered.asm.mixin.injection.code.Injector; -import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; -import org.spongepowered.asm.mixin.injection.struct.InjectionNodes; -import org.spongepowered.asm.mixin.injection.struct.Target; - -public class ModifyInstanceofValueInjector extends Injector { - - public ModifyInstanceofValueInjector(InjectionInfo info) { - super(info, "@ModifyInstanceofValue"); - } - - @Override - protected void inject(Target target, InjectionNodes.InjectionNode node) { - AbstractInsnNode valueNode = node.getCurrentTarget(); - StackExtension stack = new StackExtension(target); - - InjectorData handler = new InjectorData(target, "instanceof value modifier"); - validateParams(handler, Type.BOOLEAN_TYPE, Type.BOOLEAN_TYPE); - - InsnList insns = new InsnList(); - - if (!this.isStatic) { - insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); - insns.add(new InsnNode(Opcodes.SWAP)); - } - - if (handler.captureTargetArgs > 0) { - pushArgs(target.arguments, insns, target.getArgIndices(), 0, handler.captureTargetArgs); - } - - stack.receiver(this.isStatic); - stack.capturedArgs(target.arguments, handler.captureTargetArgs); - - invokeHandler(insns); - - target.insns.insert(valueNode, insns); - } -} diff --git a/runtime/src/main/resources/adapter.init.mixins.json b/runtime/src/main/resources/adapter.init.mixins.json deleted file mode 100644 index 1b02b285..00000000 --- a/runtime/src/main/resources/adapter.init.mixins.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "minVersion": "0.8.5", - "plugin": "org.sinytra.adapter.runtime.AdapterMixinPlugin", - "package": "org.sinytra.adapter.runtime.mixin" -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 136b29bd..d96ad6b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,7 @@ pluginManagement { rootProject.name = "Adapter" includeBuild("core") -include("runtime", "test") +include("test") include("test:mc-1.21.1") project(":test:mc-1.21.1").projectDir = file("test/mc-1.21.1") diff --git a/test/build.gradle.kts b/test/build.gradle.kts index 82799222..41d48221 100644 --- a/test/build.gradle.kts +++ b/test/build.gradle.kts @@ -36,7 +36,6 @@ allprojects { testImplementation(group = "org.sinytra.adapter", name = "core") - testImplementation(project(":runtime")) testImplementation(platform("org.junit:junit-bom:5.9.1")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.assertj:assertj-core:3.25.1") diff --git a/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/BytecodeFixerUpperTestFrontend.java b/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/BytecodeFixerUpperTestFrontend.java index 22447150..fd04addd 100644 --- a/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/BytecodeFixerUpperTestFrontend.java +++ b/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/BytecodeFixerUpperTestFrontend.java @@ -6,10 +6,10 @@ import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.TypeInsnNode; -import org.sinytra.adapter.types.BytecodeFixerUpper; -import org.sinytra.adapter.types.SimpleTypeAdapter; -import org.sinytra.adapter.types.TypeAdapter; -import org.sinytra.adapter.util.provider.ClassLookup; +import org.sinytra.adapter.api.BytecodeFixerUpper; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.SimpleTypeAdapter; +import org.sinytra.adapter.api.TypeAdapter; import java.util.List; diff --git a/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/DynamicMixinPatchTest.java b/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/DynamicMixinPatchTest.java index 29c60233..2ac40644 100644 --- a/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/DynamicMixinPatchTest.java +++ b/test/mc-1.21.1/src/test/java/org/sinytra/adapter/patch/test/mc_1_21_1/mixin/DynamicMixinPatchTest.java @@ -1,22 +1,34 @@ package org.sinytra.adapter.patch.test.mc_1_21_1.mixin; +import com.google.common.collect.ImmutableMultimap; import com.mojang.logging.LogUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.assertj.core.api.Assertions; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; -import org.sinytra.adapter.env.ctx.PatchEnvironment; -import org.sinytra.adapter.env.ctx.RefmapHolder; -import org.sinytra.adapter.patch.DynamicPatches; -import org.sinytra.adapter.patch.Patcher; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.DynamicPatches; +import org.sinytra.adapter.api.MethodTransformer; +import org.sinytra.adapter.api.MixinClassGenerator; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.api.PatchResult; +import org.sinytra.adapter.api.Patcher; +import org.sinytra.adapter.api.RefmapHolder; +import org.sinytra.adapter.api.TxPhase; +import org.sinytra.adapter.model.ann.AnnotationHandle; import org.sinytra.adapter.patch.test_main.mixin.MinecraftMixinPatchTest; -import org.sinytra.adapter.types.FieldTypeUsageTransformer; -import org.sinytra.adapter.util.provider.ClassLookup; import org.slf4j.Logger; import org.spongepowered.asm.mixin.FabricUtil; import java.util.List; +import java.util.stream.StreamSupport; public class DynamicMixinPatchTest extends MinecraftMixinPatchTest { private static final Logger LOGGER = LogUtils.getLogger(); @@ -45,8 +57,16 @@ public void copyEntries(String from, String to) { FabricUtil.COMPATIBILITY_LATEST ); patcher = Patcher.builder(patchEnvironment) - .classTransformer(new FieldTypeUsageTransformer()) - .methodTransformers(DynamicPatches.methodTransformers(List.of())) + .classTransformers(DynamicPatches.CLASS_PATCHES) + .methodTransformers(ImmutableMultimap.builder() + .putAll(DynamicPatches.methodTransformers(List.of())) + .put(TxPhase.VALIDATED, context -> { + if (context.methodNode().name.startsWith("failAfter")) { + throw new IllegalStateException("Simulated failure"); + } + return PatchResult.PASS; + }) + .build()) .build(); } @@ -255,6 +275,16 @@ void testUpdatedInjectionPointModifyExprVal() throws Exception { ); } + @Test + void testDescriptorOnlyTargetSelector() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/FarmLandBlockMixin", + "isNearWaterByDescriptor", + assertTargetMethod(), + assertInjectionPoint() + ); + } + @Test void testUpdatedInjectionPointModifyExprValConst() throws Exception { assertSameCode( @@ -298,6 +328,59 @@ void testSplitMethodInjectionTarget() throws Exception { ); } + @Test + void testSplitMethodCancellation() throws Exception { + assertPatched("org/sinytra/adapter/test/mixin/GuiMixin", "cancelAfterArmor", (patched, expected, env) -> { + Assertions.assertThat(getTargetMethods(patched)).containsExactly("renderArmorLevel(Lnet/minecraft/client/gui/GuiGraphics;)V"); + + ClassNode generated = env.classGenerator().getGeneratedMixinClasses().get("org/sinytra/adapter/test/mixin/adapter_generated_net_minecraft_client_gui_Gui").node(); + String tracker = "adapter$cancelled$GuiMixin$cancelAfterArmor"; + Assertions.assertThat(generated.fields).anyMatch(f -> f.name.equals(tracker) && f.desc.equals("Z")); + + AbstractInsnNode cancel = StreamSupport.stream(patched.instructions.spliterator(), false) + .filter(insn -> insn instanceof MethodInsnNode call && call.name.equals("cancel")) + .findFirst() + .orElseThrow(); + Assertions.assertThat(cancel.getPrevious()).matches(insn -> insn instanceof FieldInsnNode set + && set.getOpcode() == Opcodes.PUTFIELD && set.owner.equals("net/minecraft/client/gui/Gui") && set.name.equals(tracker)); + + List cancelled = generated.methods.stream() + .filter(m -> m.name.startsWith("cancelAfterArmor$adapter$canceller$")) + .flatMap(m -> getTargetMethods(m).stream()) + .toList(); + Assertions.assertThat(cancelled).containsExactly( + "renderFoodLevel(Lnet/minecraft/client/gui/GuiGraphics;)V", + "renderAirLevel(Lnet/minecraft/client/gui/GuiGraphics;)V" + ); + }); + } + + @Test + void testRollbackSplitMethodCancellation() throws Exception { + assertPatched("org/sinytra/adapter/test/mixin/GuiMixin", "failAfterCancelling", (patched, expected, env) -> { + Assertions.assertThat(getTargetMethods(patched)).containsExactly("renderPlayerHealth"); + MixinClassGenerator.GeneratedClass generated = env.classGenerator().getGeneratedMixinClasses().get("org/sinytra/adapter/test/mixin/adapter_generated_net_minecraft_client_gui_Gui"); + if (generated != null) { + Assertions.assertThat(generated.node().fields).noneMatch(f -> f.name.contains("failAfterCancelling")); + Assertions.assertThat(generated.node().methods).noneMatch(m -> m.name.startsWith("failAfterCancelling")); + } + }); + } + + @Test + void testMultiTargetSplit() throws Exception { + LoadResult result = load("org/sinytra/adapter/test/mixin/GuiMixin", List.of("modifyStatusBarY")); + MethodNode original = result.patched().methods.stream().filter(m -> m.name.equals("modifyStatusBarY")).findFirst().orElseThrow(); + MethodNode copy = result.patched().methods.stream().filter(m -> m.name.equals("modifyStatusBarY$adapter$target$1")).findFirst().orElseThrow(); + Assertions.assertThat(getTargetMethods(original)).containsExactly("renderAirLevel(Lnet/minecraft/client/gui/GuiGraphics;)V"); + Assertions.assertThat(getTargetMethods(copy)).containsExactly("renderVehicleHealth(Lnet/minecraft/client/gui/GuiGraphics;)V"); + Assertions.assertThat(new AnnotationHandle(copy.visibleAnnotations.getFirst()).getValue("require").orElseThrow().get()).isZero(); + } + + private static List getTargetMethods(MethodNode method) { + return new AnnotationHandle(method.visibleAnnotations.getFirst()).>getValue("method").orElseThrow().get(); + } + @Test void testMethodInjectionTargetParamsChanged() throws Exception { assertSameCode( @@ -325,7 +408,46 @@ void testCompareModifiedMethod() throws Exception { "org/sinytra/adapter/test/mixin/LivingEntityMixin", "onUnderwater", assertUnique(), - assertHasGeneratedMethod("org/sinytra/adapter/test/mixin/adapter_generated_net_neoforged_neoforge_common_CommonHooks") + assertHasGeneratedMethod("org/sinytra/adapter/test/mixin/adapter_generated_net_neoforged_neoforge_common_CommonHooks"), + (patched, expected, env) -> { + ClassNode generated = env.classGenerator().getGeneratedMixinClasses().get("org/sinytra/adapter/test/mixin/adapter_generated_net_neoforged_neoforge_common_CommonHooks").node(); + MethodNode mirror = generated.methods.stream().filter(m -> m.name.startsWith("onUnderwater$adapter$mirror")).findFirst().orElseThrow(); + List slices = new AnnotationHandle(mirror.visibleAnnotations.getFirst()).>getValue("slice").map(v -> v.get()).orElse(List.of()); + Assertions.assertThat(slices).isEmpty(); + } + ); + } + + @Test + void testRedirectFieldMirrored() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/LivingEntityMixin", + "redirectClientSide", + assertUnique(), + assertHasGeneratedMethod("org/sinytra/adapter/test/mixin/adapter_generated_net_neoforged_neoforge_common_CommonHooks"), + (patched, expected, env) -> { + ClassNode generated = env.classGenerator().getGeneratedMixinClasses().get("org/sinytra/adapter/test/mixin/adapter_generated_net_neoforged_neoforge_common_CommonHooks").node(); + MethodNode mirror = generated.methods.stream().filter(m -> m.name.startsWith("redirectClientSide$adapter$mirror")).findFirst().orElseThrow(); + Assertions.assertThat(mirror.desc).isEqualTo("(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/LivingEntity;)Z"); + } + ); + } + + @Test + void testRedirectChangedConstructor() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/FaceBakeryMixin", + "createQuad", + assertInjectionPoint() + ); + } + + @Test + void testAppliedLocalWarning() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/LivingEntityMixin", + "modifyHealthDamage", + assertAudit("Warning: The variable selector selects local 2") ); } @@ -417,6 +539,17 @@ void testDynamicLocalCaptureupgrade() throws Exception { ); } + @Test + void testLocalCaptureUpgradeOrdinal() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/GuiMixin", + "captureSecondInt", + assertTargetMethod(), + assertInjectionPoint(), + assertLocalSelectors() + ); + } + @Test void testSyntheticInstanceof() throws Exception { assertSameCode( @@ -432,7 +565,8 @@ void testSyntheticInstanceofMEV() throws Exception { "renderFirstPersonItemMEV", assertType(), assertTargetMethod(), - assertInjectionPoint() + assertInjectionPoint(), + assertExpression() ); } @@ -495,14 +629,121 @@ void testExtractByCodePath() throws Exception { "org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin", "replaceDyeColor", "org/sinytra/adapter/test/mixin/pipeline/adapter_generated_net_neoforged_neoforge_common_extensions_IBlockExtension", - assertTargetMethod() + assertTargetMethod(), + assertLocalSelectors() + ); + } + + @Test + void testExtractWidenedLocal() throws Exception { + assertSameCodeExtracted( + "org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin", + "replaceDyeColorOnClient", + "org/sinytra/adapter/test/mixin/pipeline/adapter_generated_net_neoforged_neoforge_common_extensions_IBlockExtension", + assertTargetMethod(), + assertLocalSelectors() + ); + } + + @Test + void testExtractCapturingLocals() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin", + "captureBeamBlock", + assertAudit("Can't relocate") + ); + } + + @Test + void testMirrorStaticHandler() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/BlockMixin", + "onSpawnAfterBreak", + assertUnique(), + assertHasGeneratedMethod("org/sinytra/adapter/test/mixin/pipeline/adapter_generated_net_neoforged_neoforge_common_CommonHooks") + ); + } + + @Test + void testRollbackMirror() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/LivingEntityMixin", + "failAfterMirror", + assertNotRelocated("org/sinytra/adapter/test/mixin/adapter_generated_net_neoforged_neoforge_common_CommonHooks") + ); + } + + @Test + void testRollbackMove() throws Exception { + LoadResult result = load("org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin", List.of("failAfterMove")); + MethodNode lambda = result.patched().methods.stream().filter(m -> m.name.startsWith("lambda$failAfterMove$")).findFirst().orElseThrow(); + MethodNode expected = result.expected().methods.stream().filter(m -> m.name.equals(lambda.name)).findFirst().orElseThrow(); + Assertions.assertThat(StreamSupport.stream(lambda.instructions.spliterator(), false).toList()) + .usingElementComparator(new InsnComparator()) + .isEqualTo(StreamSupport.stream(expected.instructions.spliterator(), false).toList()); + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin", + "failAfterMove", + assertNotRelocated("org/sinytra/adapter/test/mixin/pipeline/adapter_generated_net_neoforged_neoforge_common_extensions_IBlockExtension") + ); + } + + // The call's owner was narrowed to a subclass + @Test + void testNarrowedCallOwner() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/FlowerPotBlockMixin", + "emptyPotState", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + // The call's owner was widened to a superclass + @Test + void testWidenedCallOwnerRedirect() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/RangedBowAttackGoalMixin", + "redirectTarget", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testWidenedCallOwnerWrapOperation() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/RangedBowAttackGoalMixin", + "wrapTarget", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testTargetInheritedDelegatingDefault() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/MultiPartBakedModelMixin", + "onGetQuads", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testMovedAndChangedCall() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/pipeline/GuiMixin", + "beforeSelectedItemName", + assertTargetMethod(), + assertInjectionPoint() ); } @Override protected LoadResult load(String className, List allowedMethods) throws Exception { ClassNode patched = loadClass(className); - patched.methods.removeIf(m -> !allowedMethods.contains(m.name)); + patched.methods.removeIf(m -> !allowedMethods.contains(m.name) && allowedMethods.stream().noneMatch(name -> m.name.startsWith("lambda$" + name + "$"))); patcher.process(patched); return new LoadResult(patchEnvironment, patched, loadClass(className)); } diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/FarmLandBlockMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/FarmLandBlockMixin.java index 3e47fab6..d8b262d5 100644 --- a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/FarmLandBlockMixin.java +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/FarmLandBlockMixin.java @@ -26,4 +26,20 @@ private static boolean isFarmlandNearWater(boolean original, @Local(ordinal = 0) private static boolean isFarmlandNearWaterExpected(boolean original, @Local(ordinal = 0) LevelReader world, @Local(ordinal = 1) BlockPos blockPos) { return original; } + + @ModifyExpressionValue( + method = "(Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/material/FluidState;is(Lnet/minecraft/tags/TagKey;)Z") + ) + private static boolean isNearWaterByDescriptor(boolean original) { + return original; + } + + @ModifyExpressionValue( + method = "(Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;canBeHydrated(Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/material/FluidState;Lnet/minecraft/core/BlockPos;)Z") + ) + private static boolean isNearWaterByDescriptorExpected(boolean original) { + return original; + } } diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java index 61c95cc1..dfa1bbc6 100644 --- a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.Iterator; +import java.util.List; @Mixin(Gui.class) public class GuiMixin { @@ -99,6 +100,34 @@ private void cozyBackgroundExpected(GuiGraphics context, DeltaTracker tickCounte boolean something = registryEntry == null; } + @Inject( + method = "renderEffects", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/effect/MobEffectInstance;getEffect()Lnet/minecraft/core/Holder;", + shift = At.Shift.AFTER + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private void captureSecondInt(GuiGraphics context, DeltaTracker tickCounter, CallbackInfo ci, Collection collection, int i, int j, + MobEffectTextureManager statusEffectSpriteManager, List list, Iterator var8, MobEffectInstance statusEffectInstance, Holder registryEntry + ) { + int y = j; + } + + @Inject( + method = "renderEffects(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/client/DeltaTracker;)V", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/effect/MobEffectInstance;getEffect()Lnet/minecraft/core/Holder;", + shift = At.Shift.AFTER + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private void captureSecondIntExpected(GuiGraphics context, DeltaTracker tickCounter, CallbackInfo ci, @Local(ordinal = 1) int j) { + int y = j; + } + // https://github.com/SkyblockerMod/Skyblocker/blob/fa67224da0cdcd2325b24f41732e820ff7ef04e8/src/main/java/de/hysky/skyblocker/mixins/InGameHudMixin.java#L97 @ModifyExpressionValue( method = "renderPlayerHealth(Lnet/minecraft/client/gui/GuiGraphics;)V", @@ -164,4 +193,31 @@ private int moveAirUp(int y) { private int moveAirUpExpected(int y) { return y; } + + @Inject( + method = "renderPlayerHealth", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Gui;renderArmor(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/entity/player/Player;IIII)V"), + cancellable = true + ) + private void cancelAfterArmor(GuiGraphics guiGraphics, CallbackInfo ci) { + ci.cancel(); + } + + @Inject( + method = "renderPlayerHealth", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Gui;renderArmor(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/entity/player/Player;IIII)V"), + cancellable = true + ) + private void failAfterCancelling(GuiGraphics guiGraphics, CallbackInfo ci) { + ci.cancel(); + } + + @ModifyArg( + method = {"renderPlayerHealth", "renderVehicleHealth"}, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;blitSprite(Lnet/minecraft/resources/ResourceLocation;IIII)V"), + index = 2 + ) + public int modifyStatusBarY(int y) { + return y; + } } diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/LivingEntityMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/LivingEntityMixin.java index 2490f84c..8760c804 100644 --- a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/LivingEntityMixin.java +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/LivingEntityMixin.java @@ -4,11 +4,13 @@ import com.llamalad7.mixinextras.sugar.Local; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Slice; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -73,6 +75,38 @@ public void testFrostWalkerExpected(CallbackInfo ci) { ourUniqueMethod(); } + @Inject(method = "baseTick", at = @At(value = "FIELD", target = "Lnet/minecraft/world/level/Level;isClientSide:Z", ordinal = 2)) + public void failAfterMirror(CallbackInfo ci) { + ourUniqueMethod(); + } + + @Inject(method = "baseTick", at = @At(value = "FIELD", target = "Lnet/minecraft/world/level/Level;isClientSide:Z", ordinal = 2)) + public void failAfterMirrorExpected(CallbackInfo ci) { + ourUniqueMethod(); + } + + @Redirect(method = "baseTick", at = @At(value = "FIELD", target = "Lnet/minecraft/world/level/Level;isClientSide:Z", ordinal = 2)) + public boolean redirectClientSide(Level level) { + ourUniqueMethod(); + return level.isClientSide; + } + + @Unique + public boolean redirectClientSideExpected(Level level) { + ourUniqueMethod(); + return level.isClientSide; + } + + @ModifyVariable(method = "actuallyHurt", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getHealth()F"), argsOnly = true) + private float modifyHealthDamage(float amount) { + return amount; + } + + @ModifyVariable(method = "actuallyHurt", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getHealth()F"), argsOnly = true) + private float modifyHealthDamageExpected(float amount) { + return amount; + } + @Unique private void ourUniqueMethod() { // Noop diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/MilkBucketItemMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/MilkBucketItemMixin.java index 060e750f..fe204e32 100644 --- a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/MilkBucketItemMixin.java +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/MilkBucketItemMixin.java @@ -28,7 +28,8 @@ private void onClearStatusEffect(ItemStack stack, Level level, LivingEntity user method = "finishUsingItem", at = @At( value = "INVOKE", - target = "Lnet/minecraft/world/entity/LivingEntity;removeEffectsCuredBy(Lnet/neoforged/neoforge/common/EffectCure;)Z" + target = "Lnet/minecraft/world/entity/LivingEntity;removeEffectsCuredBy(Lnet/neoforged/neoforge/common/EffectCure;)Z", + shift = At.Shift.BEFORE ) ) private void onClearStatusEffectExpected(ItemStack stack, Level level, LivingEntity user, CallbackInfoReturnable cir) { diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin.java index 469d0637..1f7df2c5 100644 --- a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin.java +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/BeaconBlockEntityMixin.java @@ -4,10 +4,20 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.item.DyeColor; import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.BeaconBeamBlock; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BeaconBlockEntity; +import net.minecraft.world.level.block.state.BlockState; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Coerce; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.function.IntSupplier; @Mixin(BeaconBlockEntity.class) public abstract class BeaconBlockEntityMixin { @@ -24,7 +34,59 @@ private static int replaceDyeColor(DyeColor instance, @Local(ordinal = 1) BlockP method = "getBeaconColorMultiplier(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;)Ljava/lang/Integer;", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/DyeColor;getTextureDiffuseColor()I") ) - private static int replaceDyeColorExpected(DyeColor instance, @Local(ordinal = 1) BlockPos pos, @Local(argsOnly = true) Level level) { + private static int replaceDyeColorExpected(DyeColor instance, @Local(ordinal = 0) BlockPos pos, @Local(argsOnly = true) LevelReader level) { return instance.getTextureDiffuseColor(); } + + @Redirect( + method = "tick(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/entity/BeaconBlockEntity;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/DyeColor;getTextureDiffuseColor()I") + ) + private static int replaceDyeColorOnClient(DyeColor instance, @Local(argsOnly = true) Level level) { + return level.isClientSide ? 0 : instance.getTextureDiffuseColor(); + } + + @Redirect( + method = "getBeaconColorMultiplier(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;)Ljava/lang/Integer;", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/DyeColor;getTextureDiffuseColor()I") + ) + private static int replaceDyeColorOnClientExpected(DyeColor instance, @Local(argsOnly = true) LevelReader level) { + return ((Level) level).isClientSide ? 0 : instance.getTextureDiffuseColor(); + } + + @Redirect( + method = "tick(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/entity/BeaconBlockEntity;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/DyeColor;getTextureDiffuseColor()I") + ) + private static int failAfterMove(DyeColor instance) { + IntSupplier color = () -> instance.getTextureDiffuseColor(); + return color.getAsInt(); + } + + @Redirect( + method = "tick(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/entity/BeaconBlockEntity;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/DyeColor;getTextureDiffuseColor()I") + ) + private static int failAfterMoveExpected(DyeColor instance) { + IntSupplier color = () -> instance.getTextureDiffuseColor(); + return color.getAsInt(); + } + + @Inject( + method = "tick(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/entity/BeaconBlockEntity;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/DyeColor;getTextureDiffuseColor()I"), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private static void captureBeamBlock(Level level, BlockPos pos, BlockState state, BeaconBlockEntity blockEntity, CallbackInfo ci, int i, int j, int k, BlockPos blockPos, @Coerce Object section, int l, int m, BlockState blockState, Block block, BeaconBeamBlock beamBlock) { + System.out.println(beamBlock); + } + + @Inject( + method = "tick(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/entity/BeaconBlockEntity;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/DyeColor;getTextureDiffuseColor()I"), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private static void captureBeamBlockExpected(Level level, BlockPos pos, BlockState state, BeaconBlockEntity blockEntity, CallbackInfo ci, int i, int j, int k, BlockPos blockPos, @Coerce Object section, int l, int m, BlockState blockState, Block block, BeaconBeamBlock beamBlock) { + System.out.println(beamBlock); + } } diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/BlockMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/BlockMixin.java new file mode 100644 index 00000000..a2e1a8d5 --- /dev/null +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/BlockMixin.java @@ -0,0 +1,28 @@ +package org.sinytra.adapter.test.mixin.pipeline; + +import net.minecraft.world.level.block.Block; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Block.class) +public abstract class BlockMixin { + @Inject( + method = "dropResources(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;spawnAfterBreak(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/item/ItemStack;Z)V") + ) + private static void onSpawnAfterBreak(CallbackInfo ci) { + countDrop(); + } + + @Unique + public static void onSpawnAfterBreakExpected(CallbackInfo ci) { + countDrop(); + } + + @Unique + private static void countDrop() { + } +} diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/FaceBakeryMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/FaceBakeryMixin.java new file mode 100644 index 00000000..9f50b0aa --- /dev/null +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/FaceBakeryMixin.java @@ -0,0 +1,28 @@ +package org.sinytra.adapter.test.mixin.pipeline; + +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.FaceBakery; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.Direction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(FaceBakery.class) +public class FaceBakeryMixin { + @Redirect( + method = "bakeQuad", + at = @At(value = "NEW", target = "([IILnet/minecraft/core/Direction;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;Z)Lnet/minecraft/client/renderer/block/model/BakedQuad;") + ) + private BakedQuad createQuad(int[] vertices, int tintIndex, Direction direction, TextureAtlasSprite sprite, boolean shade) { + return null; + } + + @Redirect( + method = "bakeQuad", + at = @At(value = "NEW", target = "([IILnet/minecraft/core/Direction;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;ZZ)Lnet/minecraft/client/renderer/block/model/BakedQuad;") + ) + private BakedQuad createQuadExpected(int[] vertices, int tintIndex, Direction direction, TextureAtlasSprite sprite, boolean shade, boolean adapter_injected_5) { + return null; + } +} diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/FlowerPotBlockMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/FlowerPotBlockMixin.java new file mode 100644 index 00000000..a9ecdc89 --- /dev/null +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/FlowerPotBlockMixin.java @@ -0,0 +1,28 @@ +package org.sinytra.adapter.test.mixin.pipeline; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.FlowerPotBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(FlowerPotBlock.class) +public class FlowerPotBlockMixin { + // NeoForge calls defaultBlockState on the empty pot, a FlowerPotBlock, instead of on Blocks.FLOWER_POT + @Redirect( + method = "useWithoutItem(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;defaultBlockState()Lnet/minecraft/world/level/block/state/BlockState;") + ) + private BlockState emptyPotState(Block instance) { + return instance.defaultBlockState(); + } + + @Redirect( + method = "useWithoutItem(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/FlowerPotBlock;defaultBlockState()Lnet/minecraft/world/level/block/state/BlockState;") + ) + private BlockState emptyPotStateExpected(FlowerPotBlock instance) { + return instance.defaultBlockState(); + } +} diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/GuiMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/GuiMixin.java index cb4b60ae..6783fa7b 100644 --- a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/GuiMixin.java +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/GuiMixin.java @@ -24,4 +24,18 @@ public void renderSelectedItemName(GuiGraphics guiGraphics, CallbackInfo ci) { public void renderSelectedItemNameExpected(GuiGraphics guiGraphics, int adapter_injected_1, CallbackInfo ci) { } + + @Inject( + method = "renderHotbarAndDecorations", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Gui;renderSelectedItemName(Lnet/minecraft/client/gui/GuiGraphics;)V") + ) + private void beforeSelectedItemName(CallbackInfo ci) { + } + + @Inject( + method = "maybeRenderSelectedItemName(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/client/DeltaTracker;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Gui;renderSelectedItemName(Lnet/minecraft/client/gui/GuiGraphics;I)V") + ) + private void beforeSelectedItemNameExpected(CallbackInfo ci) { + } } diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/ItemInHandRendererMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/ItemInHandRendererMixin.java index 35a93e8a..b8acdf05 100644 --- a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/ItemInHandRendererMixin.java +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/ItemInHandRendererMixin.java @@ -1,11 +1,12 @@ package org.sinytra.adapter.test.mixin.pipeline; +import com.llamalad7.mixinextras.expression.Definition; +import com.llamalad7.mixinextras.expression.Expression; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import net.minecraft.client.renderer.ItemInHandRenderer; import net.minecraft.world.item.CrossbowItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import org.sinytra.adapter.runtime.inject.ModifyInstanceofValue; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; @@ -37,12 +38,11 @@ private boolean renderFirstPersonItemMEV(boolean original) { return original; } - @ModifyInstanceofValue( + @Definition(id = "CrossbowItem", type = CrossbowItem.class) + @Expression("? instanceof CrossbowItem") + @ModifyExpressionValue( method = "renderArmWithItem(Lnet/minecraft/client/player/AbstractClientPlayer;FFLnet/minecraft/world/InteractionHand;FLnet/minecraft/world/item/ItemStack;FLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", - at = @At( - value = "sinytra:INSTANCEOF", - target = "net/minecraft/world/item/CrossbowItem" - ) + at = @At("MIXINEXTRAS:EXPRESSION") ) private boolean renderFirstPersonItemMEVExpected(boolean original) { return original; diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/MultiPartBakedModelMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/MultiPartBakedModelMixin.java new file mode 100644 index 00000000..cd7e0b16 --- /dev/null +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/MultiPartBakedModelMixin.java @@ -0,0 +1,31 @@ +package org.sinytra.adapter.test.mixin.pipeline; + +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.resources.model.MultiPartBakedModel; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.client.model.data.ModelData; +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.CallbackInfoReturnable; + +import java.util.List; + +@Mixin(MultiPartBakedModel.class) +public class MultiPartBakedModelMixin { + @Inject(method = "getQuads", at = @At("HEAD"), cancellable = true) + private void onGetQuads(BlockState state, Direction side, RandomSource random, CallbackInfoReturnable> cir) { + cir.setReturnValue(List.of()); + } + + @Inject( + method = "getQuads(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/Direction;Lnet/minecraft/util/RandomSource;Lnet/neoforged/neoforge/client/model/data/ModelData;Lnet/minecraft/client/renderer/RenderType;)Ljava/util/List;", + at = @At("HEAD"), + cancellable = true + ) + private void onGetQuadsExpected(BlockState state, Direction side, RandomSource random, ModelData adapter_injected_3, RenderType adapter_injected_4, CallbackInfoReturnable> cir) { + cir.setReturnValue(List.of()); + } +} diff --git a/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/RangedBowAttackGoalMixin.java b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/RangedBowAttackGoalMixin.java new file mode 100644 index 00000000..06d5f297 --- /dev/null +++ b/test/mc-1.21.1/src/testClasses/java/org/sinytra/adapter/test/mixin/pipeline/RangedBowAttackGoalMixin.java @@ -0,0 +1,47 @@ +package org.sinytra.adapter.test.mixin.pipeline; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal; +import net.minecraft.world.entity.monster.Monster; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(RangedBowAttackGoal.class) +public class RangedBowAttackGoalMixin { + // NeoForge widened the goal's mob from Monster to Mob, so the calls now go to Mob + @Redirect( + method = "tick()V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/monster/Monster;getTarget()Lnet/minecraft/world/entity/LivingEntity;") + ) + private LivingEntity redirectTarget(Monster instance) { + return instance.getTarget(); + } + + @Redirect( + method = "tick()V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Mob;getTarget()Lnet/minecraft/world/entity/LivingEntity;") + ) + private LivingEntity redirectTargetExpected(Mob instance) { + return instance.getTarget(); + } + + @WrapOperation( + method = "tick()V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/monster/Monster;getTarget()Lnet/minecraft/world/entity/LivingEntity;") + ) + private LivingEntity wrapTarget(Monster instance, Operation original) { + return original.call(instance); + } + + @WrapOperation( + method = "tick()V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Mob;getTarget()Lnet/minecraft/world/entity/LivingEntity;") + ) + private LivingEntity wrapTargetExpected(Mob instance, Operation original) { + return original.call(instance); + } +} diff --git a/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/BytecodeFixerUpperTestFrontend.java b/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/BytecodeFixerUpperTestFrontend.java index b6976a5a..6f871929 100644 --- a/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/BytecodeFixerUpperTestFrontend.java +++ b/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/BytecodeFixerUpperTestFrontend.java @@ -6,10 +6,10 @@ import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.TypeInsnNode; -import org.sinytra.adapter.types.BytecodeFixerUpper; -import org.sinytra.adapter.types.SimpleTypeAdapter; -import org.sinytra.adapter.types.TypeAdapter; -import org.sinytra.adapter.util.provider.ClassLookup; +import org.sinytra.adapter.api.BytecodeFixerUpper; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.SimpleTypeAdapter; +import org.sinytra.adapter.api.TypeAdapter; import java.util.List; @@ -17,6 +17,8 @@ public class BytecodeFixerUpperTestFrontend { private static final List FIELD_TYPE_ADAPTERS = List.of( + new SimpleTypeAdapter(Type.getObjectType("net/minecraft/world/entity/Mob"), Type.getObjectType("net/minecraft/world/entity/monster/Monster"), (list, insn) -> + list.insert(insn, new TypeInsnNode(Opcodes.CHECKCAST, "net/minecraft/world/entity/monster/Monster"))), new SimpleTypeAdapter(Type.getObjectType("net/minecraft/world/item/ItemStack"), Type.getObjectType("net/minecraft/world/item/Item"), (list, insn) -> list.insert(insn, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "net/minecraft/world/item/ItemStack", "getItem", "()Lnet/minecraft/world/item/Item;"))), new SimpleTypeAdapter( diff --git a/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java b/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java index 6cc36bb4..b4fdca9d 100644 --- a/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java +++ b/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java @@ -1,21 +1,38 @@ package org.sinytra.adapter.patch.test.mc_26_1_2.mixin; import com.mojang.logging.LogUtils; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; -import org.sinytra.adapter.env.ctx.PatchEnvironment; -import org.sinytra.adapter.env.ctx.RefmapHolder; -import org.sinytra.adapter.patch.DynamicPatches; -import org.sinytra.adapter.patch.Patcher; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicVerifier; +import org.objectweb.asm.util.Textifier; +import org.objectweb.asm.util.TraceMethodVisitor; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.DynamicPatches; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.api.Patcher; +import org.sinytra.adapter.api.RefmapHolder; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.constant.MixinAnnotations; import org.sinytra.adapter.patch.test_main.mixin.MinecraftMixinPatchTest; -import org.sinytra.adapter.types.FieldTypeUsageTransformer; -import org.sinytra.adapter.util.provider.ClassLookup; +import org.sinytra.adapter.util.AdapterUtil; import org.slf4j.Logger; import org.spongepowered.asm.mixin.FabricUtil; import java.util.List; +import java.util.stream.StreamSupport; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; public class DynamicMixinPatchTest extends MinecraftMixinPatchTest { private static final Logger LOGGER = LogUtils.getLogger(); @@ -44,7 +61,7 @@ public void copyEntries(String from, String to) { FabricUtil.COMPATIBILITY_LATEST ); patcher = Patcher.builder(patchEnvironment) - .classTransformer(new FieldTypeUsageTransformer()) + .classTransformers(DynamicPatches.CLASS_PATCHES) .methodTransformers(DynamicPatches.methodTransformers(List.of())) .build(); } @@ -54,6 +71,63 @@ static void postTest() { LOGGER.info("Complete report:\n\n{}", patchEnvironment.auditTrail().getCompleteReport()); } + @Test + void testModifyConstantMovedIntoExtension() throws Exception { + assertSameCodeExtracted( + "org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityMixin", + "sinkSpeed", + "org/sinytra/adapter/test/mc_26_1_2/mixin/adapter_generated_net_neoforged_neoforge_common_extensions_ILivingEntityExtension", + assertTargetMethod() + ); + } + + @Test + void testWrapOperationMovedIntoExtension() throws Exception { + assertSameCodeExtracted( + "org/sinytra/adapter/test/mc_26_1_2/mixin/PowderSnowBlockMixin", + "walkOnSnow", + "org/sinytra/adapter/test/mc_26_1_2/mixin/adapter_generated_net_neoforged_neoforge_common_extensions_IItemExtension", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testAnonymousTargetUnchanged() throws Exception { + assertMixinTarget("org/sinytra/adapter/test/mc_26_1_2/mixin/DispenseItemBehaviorMixin", "dispense", "net/minecraft/core/dispenser/DispenseItemBehavior$10"); + } + + @Test + void testAnonymousTargetRenumbered() throws Exception { + assertMixinTarget("org/sinytra/adapter/test/mc_26_1_2/mixin/FunctionCommandExecutorMixin", "readArguments", "net/minecraft/server/commands/FunctionCommand$2"); + } + + @Test + void testFieldTypeAdaptedOnce() throws Exception { + ClassNode patched = load("org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalMixin", List.of("readMob")).patched(); + String once = describeMethod(patched, "readMob"); + patcher.process(patched); + assertEquals(once, describeMethod(patched, "readMob")); + assertEquals(1, once.split("CHECKCAST net/minecraft/world/entity/monster/Monster", -1).length - 1, once); + } + + @Test + void testFieldTypeChangedAccessor() throws Exception { + ClassNode patched = load("org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalAccessor", List.of("getMob")).patched(); + MethodNode accessor = patched.methods.stream().filter(m -> m.name.equals("getMob")).findFirst().orElseThrow(); + assertNull(AdapterUtil.getAnnotation(accessor.visibleAnnotations, MixinAnnotations.ACCESSOR)); + AnnotationNode invoker = AdapterUtil.getAnnotation(accessor.visibleAnnotations, MixinAnnotations.INVOKER); + assertNotNull(invoker); + assertEquals("adapter$mob()Lnet/minecraft/world/entity/monster/Monster;", AdapterUtil.getAnnotationValue(invoker, "value")); + } + + private static String describeMethod(ClassNode classNode, String name) { + MethodNode method = classNode.methods.stream().filter(m -> m.name.equals(name)).findFirst().orElseThrow(); + Textifier textifier = new Textifier(); + method.accept(new TraceMethodVisitor(textifier)); + return String.join("", textifier.getText().stream().map(String::valueOf).toList()); + } + @Test void testExtractUsingCodePath() throws Exception { assertSameCodeExtracted( @@ -74,10 +148,203 @@ void testModifyArgChangedTargetDesc() throws Exception { ); } + @Test + void testShiftedVariableToExpression() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/MinecraftMixin", + "modifyAttackedPos", + assertType(), + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testTargetInheritedDelegatingDefault() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/MultiPartModelMixin", + "onSetSeed", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testModifyVariableCapturedTargetArgs() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/MultiPartModelMixin", + "modifyRandom", + assertTargetMethod() + ); + } + + @Test + void testAppliedOrdinalShift() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/CameraMixin", + "addRoll1", + assertTargetMethod(), + assertInjectionPoint(), + assertAudit("ordinal 1 now selects another instruction") + ); + } + + @Test + void testShiftedOrdinal() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/CameraMixin", + "addRoll3", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testWrapWithConditionReplacedCall() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/CameraMixin", + "addRoll2", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testMovedAndChangedCall() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/LevelRendererMixin", + "beforeExtractRenderState", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testSplitPieceOfRemovedTarget() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/GuiMixin", + "skipContextualBar", + assertTargetMethod(), + assertInjectionPoint() + ); + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/GuiMixin", + "skipContextualBarBackground", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testMirrorIntoInstanceCallee() throws Exception { + assertMirroredStepSound("adjustStepSoundVolume"); + } + + @Test + void testMirrorFromChangedTarget() throws Exception { + assertMirroredStepSound("adjustMuffledStepSoundVolume"); + } + + @Test + void testHandlerUsingThisNotMoved() throws Exception { + assertMirroredStepSound("adjustStepSoundVolumeOfThis"); + } + + @Test + void testWrapOperationMirrorGuarded() throws Exception { + String generatedName = "org/sinytra/adapter/test/mc_26_1_2/mixin/adapter_generated_net_neoforged_neoforge_common_extensions_IBlockExtension"; + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/EntityMixin", + "wrapStepSound", + assertUnique(), + assertHasGeneratedMethod(generatedName), + (patched, expected, env) -> { + ClassNode generated = env.classGenerator().getGeneratedMixinClasses().get(generatedName).node(); + MethodNode mirror = generated.methods.stream().filter(m -> m.name.startsWith("wrapStepSound$adapter$mirror")).findFirst().orElseThrow(); + assertVerifies(generated, mirror); + Assertions.assertThat(AdapterUtil.methodNodeToString(mirror)) + .as("Mirror calls the original operation when it doesn't run the handler") + .contains("INVOKEVIRTUAL net/minecraft/world/entity/Entity.adapter$via$EntityMixin$wrapStepSound ()Z") + .contains("INVOKEINTERFACE com/llamalad7/mixinextras/injector/wrapoperation/Operation.call ([Ljava/lang/Object;)Ljava/lang/Object;"); + } + ); + } + + private static void assertVerifies(ClassNode owner, MethodNode method) { + try { + new Analyzer<>(new BasicVerifier()).analyze(owner.name, method); + } catch (AnalyzerException e) { + throw new AssertionError("Invalid bytecode in " + method.name + "\n" + AdapterUtil.methodNodeToString(method), e); + } + } + + @Test + void testMirrorGuardedByOriginCall() throws Exception { + assertMirroredStepSound("adjustCombinationStepSoundVolume"); + ClassNode guards = patchEnvironment.classGenerator().getGeneratedMixinClasses() + .get("org/sinytra/adapter/test/mc_26_1_2/mixin/adapter_generated_net_minecraft_world_entity_Entity").node(); + MethodNode enter = guards.methods.stream().filter(m -> m.name.equals("adapter$via$EntityMixin$adjustCombinationStepSoundVolume$enter")).findFirst().orElseThrow(); + AnnotationHandle inject = new AnnotationHandle(enter.visibleAnnotations.getFirst()); + Assertions.assertThat(inject.>getValue("method").orElseThrow().get()) + .containsExactly("playCombinationStepSounds(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;)V"); + Assertions.assertThat(AdapterUtil.methodNodeToString(enter)).contains("target=\"Lnet/minecraft/world/level/block/state/BlockState;playStepSound("); + } + + private void assertMirroredStepSound(String handler) throws Exception { + String generatedName = "org/sinytra/adapter/test/mc_26_1_2/mixin/adapter_generated_net_neoforged_neoforge_common_extensions_IBlockExtension"; + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/EntityMixin", + handler, + assertUnique(), + assertHasGeneratedMethod(generatedName), + (patched, expected, env) -> { + ClassNode generated = env.classGenerator().getGeneratedMixinClasses().get(generatedName).node(); + MethodNode mirror = generated.methods.stream().filter(m -> m.name.startsWith(handler + "$adapter$mirror")).findFirst().orElseThrow(); + assertVerifies(generated, mirror); + Assertions.assertThat(mirror.desc).isEqualTo("(FLnet/minecraft/world/entity/Entity;)F"); + AnnotationHandle modifyArg = new AnnotationHandle(mirror.visibleAnnotations.getFirst()); + Assertions.assertThat(modifyArg.>getValue("method").orElseThrow().get()) + .containsExactly("playStepSound(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/entity/Entity;FF)V"); + AnnotationNode local = mirror.invisibleParameterAnnotations[1].getFirst(); + Assertions.assertThat(local.values).containsExactly("argsOnly", true, "index", 4); + MethodInsnNode active = (MethodInsnNode) StreamSupport.stream(mirror.instructions.spliterator(), false) + .filter(MethodInsnNode.class::isInstance) + .findFirst() + .orElseThrow(); + Assertions.assertThat(active.owner + "." + active.name + active.desc) + .as("Mirror runs only for calls from the original method") + .isEqualTo("net/minecraft/world/entity/Entity.adapter$via$EntityMixin$" + handler + "()Z"); + } + ); + } + + @Test + void testModifyExpressionValueReplacedHook() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityItemMixin", + "cancelItemUsing", + assertTargetMethod(), + assertInjectionPoint() + ); + } + + @Test + void testModifyArgsMovedIntoHook() throws Exception { + LoadResult result = load("org/sinytra/adapter/test/mc_26_1_2/mixin/FogRendererMixin", List.of("darkenFogColor")); + ClassNode generated = result.env().classGenerator().getGeneratedMixinClasses() + .get("org/sinytra/adapter/test/mc_26_1_2/mixin/adapter_generated_net_neoforged_neoforge_client_ClientHooks").node(); + MethodNode moved = generated.methods.stream().filter(m -> m.name.equals("darkenFogColor")).findFirst().orElseThrow(); + Assertions.assertThat(moved.access & Opcodes.ACC_STATIC).as("Static handler").isNotZero(); + Assertions.assertThat(moved.access & Opcodes.ACC_PRIVATE).as("Private static handler").isNotZero(); + AnnotationHandle modifyArgs = new AnnotationHandle(moved.visibleAnnotations.getFirst()); + Assertions.assertThat(modifyArgs.>getValue("method").orElseThrow().get()) + .containsExactly("getFogColor(Lnet/minecraft/client/Camera;FLnet/minecraft/client/multiplayer/ClientLevel;IFFFFLorg/joml/Vector4f;)V"); + } + @Override protected LoadResult load(String className, List allowedMethods) throws Exception { ClassNode patched = loadClass(className); - patched.methods.removeIf(m -> !allowedMethods.contains(m.name)); + patched.methods.removeIf(m -> !allowedMethods.contains(m.name) && allowedMethods.stream().noneMatch(name -> m.name.startsWith("lambda$" + name + "$"))); patcher.process(patched); return new LoadResult(patchEnvironment, patched, loadClass(className)); } diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/CameraMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/CameraMixin.java new file mode 100644 index 00000000..7e173a16 --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/CameraMixin.java @@ -0,0 +1,39 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Camera.class) +public class CameraMixin { + @WrapWithCondition(method = "alignWithEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 2)) + private boolean addRoll2(Camera instance, float yRot, float xRot) { + return true; + } + + @WrapWithCondition(method = "alignWithEntity(F)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FFF)V", ordinal = 1)) + private boolean addRoll2Expected(Camera instance, float yRot, float xRot, float adapter_injected_3) { + return true; + } + + @WrapWithCondition(method = "alignWithEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 1)) + private boolean addRoll1(Camera instance, float yRot, float xRot) { + return true; + } + + @WrapWithCondition(method = "alignWithEntity(F)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FFF)V", ordinal = 0)) + private boolean addRoll1Expected(Camera instance, float yRot, float xRot, float adapter_injected_3) { + return true; + } + + @WrapWithCondition(method = "alignWithEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 3)) + private boolean addRoll3(Camera instance, float yRot, float xRot) { + return true; + } + + @WrapWithCondition(method = "alignWithEntity(F)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 1)) + private boolean addRoll3Expected(Camera instance, float yRot, float xRot) { + return true; + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/DispenseItemBehaviorMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/DispenseItemBehaviorMixin.java new file mode 100644 index 00000000..501f0742 --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/DispenseItemBehaviorMixin.java @@ -0,0 +1,15 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.world.item.ItemStack; +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.CallbackInfoReturnable; + +@Mixin(targets = "net.minecraft.core.dispenser.DispenseItemBehavior$10") +public class DispenseItemBehaviorMixin { + @Inject(method = "execute", at = @At("HEAD")) + private void dispense(BlockSource source, ItemStack dispensed, CallbackInfoReturnable cir) { + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/EntityMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/EntityMixin.java new file mode 100644 index 00000000..d2cd014c --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/EntityMixin.java @@ -0,0 +1,70 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import net.minecraft.sounds.SoundEvent; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(Entity.class) +public class EntityMixin { + @Unique + private final Entity self = (Entity) (Object) this; + + @ModifyArg(method = "playStepSound", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;playSound(Lnet/minecraft/sounds/SoundEvent;FF)V"), index = 1) + private float adjustStepSoundVolume(float original) { + return original * self.getBbWidth(); + } + + @Unique + public float adjustStepSoundVolumeExpected(float original) { + return original * self.getBbWidth(); + } + + @ModifyArg(method = "playMuffledStepSound", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;playSound(Lnet/minecraft/sounds/SoundEvent;FF)V"), index = 1) + private float adjustMuffledStepSoundVolume(float original) { + return original * self.getBbWidth(); + } + + @Unique + public float adjustMuffledStepSoundVolumeExpected(float original) { + return original * self.getBbWidth(); + } + + @ModifyArg(method = "playStepSound", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;playSound(Lnet/minecraft/sounds/SoundEvent;FF)V"), index = 1) + private float adjustStepSoundVolumeOfThis(float original) { + return original * ((Entity) (Object) this).getBbWidth(); + } + + @Unique + public float adjustStepSoundVolumeOfThisExpected(float original) { + return original * ((Entity) (Object) this).getBbWidth(); + } + + @ModifyArg(method = "playCombinationStepSounds", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;playSound(Lnet/minecraft/sounds/SoundEvent;FF)V"), index = 1) + private float adjustCombinationStepSoundVolume(float original) { + return original * self.getBbWidth(); + } + + @Unique + public float adjustCombinationStepSoundVolumeExpected(float original) { + return original * self.getBbWidth(); + } + + @WrapOperation(method = "playStepSound", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;playSound(Lnet/minecraft/sounds/SoundEvent;FF)V")) + private void wrapStepSound(Entity instance, SoundEvent sound, float volume, float pitch, Operation original) { + if (!self.isSilent()) { + original.call(instance, sound, volume, pitch); + } + } + + @Unique + public void wrapStepSoundExpected(Entity instance, SoundEvent sound, float volume, float pitch, Operation original) { + if (!self.isSilent()) { + original.call(instance, sound, volume, pitch); + } + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/FogRendererMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/FogRendererMixin.java new file mode 100644 index 00000000..b855a4da --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/FogRendererMixin.java @@ -0,0 +1,18 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.fog.FogRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +@Mixin(FogRenderer.class) +public class FogRendererMixin { + @ModifyArgs(method = "computeFogColor", at = @At(value = "INVOKE", target = "Lorg/joml/Vector4f;set(FFFF)Lorg/joml/Vector4f;")) + public void darkenFogColor(Args args) { + if (Minecraft.getInstance().level != null) { + args.set(0, (float) args.get(0) * 0.5F); + } + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/FunctionCommandExecutorMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/FunctionCommandExecutorMixin.java new file mode 100644 index 00000000..b4e3f57b --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/FunctionCommandExecutorMixin.java @@ -0,0 +1,16 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.nbt.CompoundTag; +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.CallbackInfoReturnable; + +@Mixin(targets = "net.minecraft.server.commands.FunctionCommand$1") +public class FunctionCommandExecutorMixin { + @Inject(method = "arguments", at = @At("HEAD")) + private void readArguments(CommandContext context, CallbackInfoReturnable cir) { + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/GuiMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/GuiMixin.java new file mode 100644 index 00000000..2cde3288 --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/GuiMixin.java @@ -0,0 +1,57 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.contextualbar.ContextualBarRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Gui.class) +public class GuiMixin { + @WrapOperation( + method = "extractHotbarAndDecorations", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/contextualbar/ContextualBarRenderer;extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V" + ) + ) + private void skipContextualBar(ContextualBarRenderer instance, GuiGraphicsExtractor graphics, DeltaTracker tracker, Operation original) { + original.call(instance, graphics, tracker); + } + + @WrapOperation( + method = "extractContextualInfoBar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/contextualbar/ContextualBarRenderer;extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V" + ) + ) + private void skipContextualBarExpected(ContextualBarRenderer instance, GuiGraphicsExtractor graphics, DeltaTracker tracker, Operation original) { + original.call(instance, graphics, tracker); + } + + @WrapOperation( + method = "extractHotbarAndDecorations", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/contextualbar/ContextualBarRenderer;extractBackground(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V" + ) + ) + private void skipContextualBarBackground(ContextualBarRenderer instance, GuiGraphicsExtractor graphics, DeltaTracker tracker, Operation original) { + original.call(instance, graphics, tracker); + } + + @WrapOperation( + method = "extractContextualInfoBarBackground(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/contextualbar/ContextualBarRenderer;extractBackground(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V" + ) + ) + private void skipContextualBarBackgroundExpected(ContextualBarRenderer instance, GuiGraphicsExtractor graphics, DeltaTracker tracker, Operation original) { + original.call(instance, graphics, tracker); + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LevelRendererMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LevelRendererMixin.java index 0c2fb24d..9053f644 100644 --- a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LevelRendererMixin.java +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LevelRendererMixin.java @@ -4,7 +4,9 @@ import net.minecraft.client.renderer.LevelRenderer; 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.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LevelRenderer.class) public class LevelRendererMixin { @@ -29,4 +31,18 @@ private GpuBufferSlice clampMaxValue(GpuBufferSlice fogBuffer) { private GpuBufferSlice clampMaxValueExpected(GpuBufferSlice fogBuffer) { return fogBuffer; } + + @Inject( + method = "extractVisibleBlockEntities(Lnet/minecraft/client/Camera;FLnet/minecraft/client/renderer/state/level/LevelRenderState;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;tryExtractRenderState(Lnet/minecraft/world/level/block/entity/BlockEntity;FLnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;)Lnet/minecraft/client/renderer/blockentity/state/BlockEntityRenderState;") + ) + private void beforeExtractRenderState(CallbackInfo ci) { + } + + @Inject( + method = "extractVisibleBlockEntities(Lnet/minecraft/client/Camera;FLnet/minecraft/client/renderer/state/level/LevelRenderState;Lnet/minecraft/client/renderer/culling/Frustum;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;tryExtractRenderState(Lnet/minecraft/world/level/block/entity/BlockEntity;FLnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;Lnet/minecraft/client/renderer/culling/Frustum;)Lnet/minecraft/client/renderer/blockentity/state/BlockEntityRenderState;") + ) + private void beforeExtractRenderStateExpected(CallbackInfo ci) { + } } diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityItemMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityItemMixin.java new file mode 100644 index 00000000..7b34d432 --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityItemMixin.java @@ -0,0 +1,23 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntity.class) +public class LivingEntityItemMixin { + @Unique + private final LivingEntity entity = (LivingEntity) (Object) this; + + @ModifyExpressionValue(method = "updatingUsingItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;isSameItem(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)Z")) + private boolean cancelItemUsing(boolean original) { + return original && !this.entity.isShiftKeyDown(); + } + + @ModifyExpressionValue(method = "updatingUsingItem()V", at = @At(value = "INVOKE", target = "Lnet/neoforged/neoforge/common/CommonHooks;canContinueUsing(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)Z")) + private boolean cancelItemUsingExpected(boolean original) { + return original && !this.entity.isShiftKeyDown(); + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityMixin.java index 0344d3af..0afee97e 100644 --- a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityMixin.java +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/LivingEntityMixin.java @@ -8,6 +8,8 @@ import net.minecraft.world.level.block.state.BlockState; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; @Mixin(LivingEntity.class) public class LivingEntityMixin { @@ -35,4 +37,14 @@ private void playSoundCorrectlyForBlocks(LivingEntity instance, SoundEvent sound private void playSoundCorrectlyForBlocksExpected(LivingEntity instance, SoundEvent sound, float volume, float pitch, Operation original, @Local BlockState state) { original.call(instance, sound, volume, pitch); } + + @ModifyConstant(method = "goDownInWater", constant = @Constant(doubleValue = -0.03999999910593033D)) + private double sinkSpeed(double speed) { + return speed; + } + + @ModifyConstant(method = "sinkInFluid(Lnet/neoforged/neoforge/fluids/FluidType;)V", constant = @Constant(doubleValue = -0.03999999910593033D)) + private double sinkSpeedExpected(double speed) { + return speed; + } } diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/MinecraftMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/MinecraftMixin.java new file mode 100644 index 00000000..01df6b4d --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/MinecraftMixin.java @@ -0,0 +1,29 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(Minecraft.class) +public class MinecraftMixin { + // NeoForge adds a local for the input event and the block position moves from index 6 to 7 + @ModifyVariable( + method = "startAttack", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/BlockHitResult;getBlockPos()Lnet/minecraft/core/BlockPos;", shift = At.Shift.BY, by = 2), + index = 6 + ) + private BlockPos modifyAttackedPos(BlockPos pos) { + return pos; + } + + @ModifyExpressionValue( + method = "startAttack()Z", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/phys/BlockHitResult;getBlockPos()Lnet/minecraft/core/BlockPos;") + ) + private BlockPos modifyAttackedPosExpected(BlockPos pos) { + return pos; + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/MultiPartModelMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/MultiPartModelMixin.java new file mode 100644 index 00000000..b3a905d9 --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/MultiPartModelMixin.java @@ -0,0 +1,44 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.client.renderer.block.dispatch.multipart.MultiPartModel; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockState; +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.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(MultiPartModel.class) +public class MultiPartModelMixin { + @Inject(method = "collectParts", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/RandomSource;setSeed(J)V")) + private void onSetSeed(RandomSource random, List output, CallbackInfo ci) { + random.nextInt(); + } + + @Inject( + method = "collectParts(Lnet/minecraft/client/renderer/block/BlockAndTintGetter;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/util/RandomSource;Ljava/util/List;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/util/RandomSource;setSeed(J)V") + ) + private void onSetSeedExpected(BlockAndTintGetter adapter_injected_0, BlockPos adapter_injected_1, BlockState adapter_injected_2, RandomSource random, List output, CallbackInfo ci) { + random.nextInt(); + } + + @ModifyVariable(method = "collectParts", at = @At("HEAD"), argsOnly = true) + private RandomSource modifyRandom(RandomSource value, RandomSource random) { + return random; + } + + @ModifyVariable( + method = "collectParts(Lnet/minecraft/client/renderer/block/BlockAndTintGetter;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/util/RandomSource;Ljava/util/List;)V", + at = @At("HEAD"), + argsOnly = true + ) + private RandomSource modifyRandomExpected(RandomSource value, BlockAndTintGetter adapter_injected_1, BlockPos adapter_injected_2, BlockState adapter_injected_3, RandomSource random) { + return random; + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/PowderSnowBlockMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/PowderSnowBlockMixin.java new file mode 100644 index 00000000..f8bd9b0d --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/PowderSnowBlockMixin.java @@ -0,0 +1,24 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.PowderSnowBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(PowderSnowBlock.class) +public class PowderSnowBlockMixin { + @WrapOperation(method = "canEntityWalkOnPowderSnow", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;is(Ljava/lang/Object;)Z")) + private static boolean walkOnSnow(ItemStack stack, Object item, Operation original) { + return original.call(stack, item); + } + + @WrapOperation( + method = "canWalkOnPowderedSnow(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/entity/LivingEntity;)Z", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;is(Ljava/lang/Object;)Z") + ) + private static boolean walkOnSnowExpected(ItemStack stack, Object item, Operation original) { + return original.call(stack, item); + } +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalAccessor.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalAccessor.java new file mode 100644 index 00000000..9feb6242 --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalAccessor.java @@ -0,0 +1,12 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal; +import net.minecraft.world.entity.monster.Monster; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(RangedCrossbowAttackGoal.class) +public interface RangedCrossbowAttackGoalAccessor { + @Accessor + Monster getMob(); +} diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalMixin.java new file mode 100644 index 00000000..5e53942e --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/RangedCrossbowAttackGoalMixin.java @@ -0,0 +1,22 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal; +import net.minecraft.world.entity.monster.Monster; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(RangedCrossbowAttackGoal.class) +public class RangedCrossbowAttackGoalMixin { + @Shadow + @Final + private Monster mob; + + @Inject(method = "tick", at = @At("HEAD")) + private void readMob(CallbackInfo ci) { + this.mob.getTarget(); + } +} diff --git a/test/src/main/java/org/sinytra/adapter/patch/test_main/mixin/MinecraftMixinPatchTest.java b/test/src/main/java/org/sinytra/adapter/patch/test_main/mixin/MinecraftMixinPatchTest.java index 24256c8e..04437290 100644 --- a/test/src/main/java/org/sinytra/adapter/patch/test_main/mixin/MinecraftMixinPatchTest.java +++ b/test/src/main/java/org/sinytra/adapter/patch/test_main/mixin/MinecraftMixinPatchTest.java @@ -4,22 +4,25 @@ import org.apache.commons.lang3.tuple.Pair; import org.assertj.core.api.Assertions; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; -import org.sinytra.adapter.analysis.selector.AnnotationHandle; -import org.sinytra.adapter.analysis.selector.AnnotationValueHandle; -import org.sinytra.adapter.env.ann.AtData; -import org.sinytra.adapter.env.ctx.MixinClassGenerator; -import org.sinytra.adapter.env.ctx.PatchEnvironment; -import org.sinytra.adapter.env.util.MixinAnnotations; +import org.sinytra.adapter.api.ClassLookup; +import org.sinytra.adapter.api.MixinClassGenerator; +import org.sinytra.adapter.api.PatchEnvironment; +import org.sinytra.adapter.api.ZipClassLookup; +import org.sinytra.adapter.model.ann.AnnotationHandle; +import org.sinytra.adapter.model.ann.AnnotationValueHandle; +import org.sinytra.adapter.model.ann.AtData; +import org.sinytra.adapter.model.ann.ExpressionData; +import org.sinytra.adapter.model.constant.MixinAnnotations; import org.sinytra.adapter.util.AdapterUtil; -import org.sinytra.adapter.util.provider.ClassLookup; -import org.sinytra.adapter.util.provider.ZipClassLookup; import org.slf4j.Logger; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -28,8 +31,8 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import java.util.stream.StreamSupport; +import java.util.stream.Stream; import java.util.zip.ZipFile; import static org.junit.jupiter.api.Assertions.*; @@ -120,6 +123,15 @@ protected final void assertSameCodeExtracted( Stream.of(assertions).forEach(c -> c.accept(patched, expected, result.env())); } + protected final void assertPatched(String className, String testName, AssertCallback... assertions) throws Exception { + LoadResult result = load(className, List.of(testName)); + MethodNode patched = result.patched.methods.stream() + .filter(m -> m.name.equals(testName)) + .findFirst() + .orElseThrow(); + Stream.of(assertions).forEach(c -> c.accept(patched, null, result.env())); + } + protected final void assertSameField( String className, String testName @@ -137,6 +149,17 @@ protected final void assertSameField( assertEquals(patched.desc, expected.desc, "Field types differ"); } + protected final void assertMixinTarget(String className, String testName, String expectedTarget) throws Exception { + ClassNode patched = load(className, List.of(testName)).patched(); + AnnotationNode mixin = patched.invisibleAnnotations.stream().filter(a -> a.desc.equals(MixinAnnotations.MIXIN)).findFirst().orElseThrow(); + Object targets = new AnnotationHandle(mixin).getValue("targets").or(() -> new AnnotationHandle(mixin).getValue("value")) + .orElseThrow().get(); + List names = ((List) targets).stream() + .map(t -> t instanceof Type type ? type.getInternalName() : ((String) t).replace('.', '/')) + .toList(); + assertEquals(List.of(expectedTarget), names, "Mixin targets"); + } + public static class InsnComparator implements Comparator { @Override public int compare(AbstractInsnNode o1, AbstractInsnNode o2) { @@ -210,6 +233,55 @@ protected AssertCallback assertHasGeneratedMethod(String targetClass) { return (patched, expected, env) -> { MixinClassGenerator.GeneratedClass generatedClass = env.classGenerator().getGeneratedMixinClasses().get(targetClass); assertNotNull(generatedClass, "Missing generated class for " + targetClass); + boolean callsPatched = generatedClass.node().methods.stream() + .filter(m -> m.visibleAnnotations != null && !m.visibleAnnotations.isEmpty()) + .anyMatch(m -> StreamSupport.stream(m.instructions.spliterator(), false) + .anyMatch(insn -> insn instanceof MethodInsnNode call && call.name.equals(patched.name) && call.desc.equals(patched.desc))); + Assertions.assertThat(callsPatched).as("Generated injector calls " + patched.name).isTrue(); + }; + } + + /// The generated class, if there is one, has neither the patched handler and its lambdas nor an injector that calls the handler + protected AssertCallback assertNotRelocated(String targetClass) { + return (patched, expected, env) -> { + MixinClassGenerator.GeneratedClass generatedClass = env.classGenerator().getGeneratedMixinClasses().get(targetClass); + if (generatedClass == null) { + return; + } + List leftovers = generatedClass.node().methods.stream() + .filter(m -> m.name.equals(patched.name) || m.name.startsWith("lambda$" + patched.name + "$") + || StreamSupport.stream(m.instructions.spliterator(), false) + .anyMatch(insn -> insn instanceof MethodInsnNode call && call.name.equals(patched.name))) + .map(m -> m.name) + .toList(); + Assertions.assertThat(leftovers).as("Generated methods left by " + patched.name).isEmpty(); + }; + } + + protected AssertCallback assertLocalSelectors() { + return (patched, expected, env) -> { + Function> selectors = method -> { + List values = new ArrayList<>(); + List[] annotations = Objects.requireNonNullElseGet(method.invisibleParameterAnnotations, () -> new List[0]); + for (List list : annotations) { + if (list != null) { + list.stream().filter(a -> a.desc.equals(MixinAnnotations.LOCAL)).forEach(a -> values.add(String.valueOf(a.values))); + } + } + return values; + }; + Assertions.assertThat(selectors.apply(patched)).as("@Local selectors").isEqualTo(selectors.apply(expected)); + }; + } + + /// The audit log of the patched method has an entry containing the text + protected AssertCallback assertAudit(String text) { + return (patched, expected, env) -> { + boolean found = env.auditTrail().getAuditTrail().values().stream() + .filter(log -> log.originalMethod() != null && log.originalMethod().startsWith(patched.name + "(")) + .flatMap(log -> log.entries().stream()) + .anyMatch(entry -> entry.second().toString().contains(text)); + Assertions.assertThat(found).as("Audit log of " + patched.name + " contains \"" + text + "\"").isTrue(); }; } @@ -257,6 +329,34 @@ protected AssertCallback assertInjectionPoint() { }; } + protected AssertCallback assertExpression() { + Function> expressionExtractor = method -> method.invisibleAnnotations == null ? List.of() + : method.invisibleAnnotations.stream() + .filter(ExpressionData::isExpressionAnnotation) + .map(MinecraftMixinPatchTest::describeAnnotation) + .toList(); + + return (patched, expected, env) -> Assertions.assertThat(expressionExtractor.apply(patched)) + .as("Expression Annotations") + .isEqualTo(expressionExtractor.apply(expected)); + } + + private static String describeAnnotation(Object value) { + if (value instanceof AnnotationNode node) { + List values = new ArrayList<>(); + if (node.values != null) { + for (int i = 0; i + 1 < node.values.size(); i += 2) { + values.add(node.values.get(i) + "=" + describeAnnotation(node.values.get(i + 1))); + } + } + return node.desc + values; + } + if (value instanceof List list) { + return list.stream().map(MinecraftMixinPatchTest::describeAnnotation).toList().toString(); + } + return String.valueOf(value); + } + protected AssertCallback assertSliceRange() { BiFunction> sliceExtractor = (name, node) -> new AnnotationHandle(node).getNested("slice") .flatMap(ann -> ann.getNested(name)) diff --git a/test/src/test/java/org/sinytra/adapter/patch/test/ParamDiffResolverTest.java b/test/src/test/java/org/sinytra/adapter/patch/test/ParamDiffResolverTest.java index f97b058b..d08f21cc 100644 --- a/test/src/test/java/org/sinytra/adapter/patch/test/ParamDiffResolverTest.java +++ b/test/src/test/java/org/sinytra/adapter/patch/test/ParamDiffResolverTest.java @@ -2,11 +2,11 @@ import org.junit.jupiter.api.Test; import org.objectweb.asm.Type; -import org.sinytra.adapter.env.param.ParamDiffResolver; -import org.sinytra.adapter.env.param.ParamDiffResolver.ParamEvalResult; -import org.sinytra.adapter.env.param.ParamDiffResolver.ParamState; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; +import org.sinytra.adapter.analysis.params.ParamsEditDistance; import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; +import org.sinytra.adapter.analysis.params.ParamDiffResolver; +import org.sinytra.adapter.analysis.params.ParamDiffResolver.ParamEvalResult; +import org.sinytra.adapter.analysis.params.ParamDiffResolver.ParamState; import java.io.InputStream; import java.io.OutputStream; @@ -65,7 +65,7 @@ private static void testParams(List clean, List dirty, int oldPos, i private static void testParams(List clean, List dirty, int oldPos, int newPos, Type newType) { Type original = clean.get(oldPos); - LayeredParamsDiffSnapshot snapshot = EnhancedParamsDiff.createLayered(clean, dirty); + LayeredParamsDiffSnapshot snapshot = ParamsEditDistance.diffTypes(clean, dirty); ParamEvalResult result = ParamDiffResolver.resolve(clean, snapshot); ParamState updated = result.getUpdated(original); diff --git a/test/src/test/java/org/sinytra/adapter/patch/test/ParameterComparisonTest.java b/test/src/test/java/org/sinytra/adapter/patch/test/ParameterComparisonTest.java index b769cda3..4d64c51c 100644 --- a/test/src/test/java/org/sinytra/adapter/patch/test/ParameterComparisonTest.java +++ b/test/src/test/java/org/sinytra/adapter/patch/test/ParameterComparisonTest.java @@ -4,9 +4,9 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.analysis.params.ParametersDiff; +import org.sinytra.adapter.analysis.params.ParamsEditDistance; +import org.sinytra.adapter.analysis.params.TypeRelation; import org.sinytra.adapter.patch.test_main.mixin.MinecraftMixinPatchTest; import java.io.IOException; @@ -18,77 +18,42 @@ public class ParameterComparisonTest { @Test public void testCompareInsertedParameters() { - Type[] original = new Type[]{Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)}; - Type[] modified = new Type[]{Type.getType(String.class), Type.DOUBLE_TYPE, Type.FLOAT_TYPE, Type.INT_TYPE, Type.getType(Object.class)}; - - ParametersDiff diff = ParametersDiff.compareTypeParameters(original, modified); - System.out.println("Original originalCount: " + original.length); - System.out.println("Actual originalCount: " + diff.originalCount()); - assertEquals(original.length, diff.originalCount()); - - System.out.println("Insertions:"); - diff.insertions().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); + LayeredParamsDiffSnapshot diff = diff( + List.of(Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)), + List.of(Type.getType(String.class), Type.DOUBLE_TYPE, Type.FLOAT_TYPE, Type.INT_TYPE, Type.getType(Object.class)) + ); assertEquals(2, diff.insertions().size()); - - System.out.println("Replacements:"); - diff.replacements().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); assertTrue(diff.replacements().isEmpty()); } @Test public void testCompareReplacedParameters() { - Type[] original = new Type[]{Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)}; - Type[] modified = new Type[]{Type.getType(String.class), Type.INT_TYPE, Type.getType(List.class)}; - - ParametersDiff diff = ParametersDiff.compareTypeParameters(original, modified); - System.out.println("Original originalCount: " + original.length); - System.out.println("Actual originalCount: " + diff.originalCount()); - assertEquals(original.length, diff.originalCount()); - - System.out.println("Insertions:"); - diff.insertions().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); + LayeredParamsDiffSnapshot diff = diff( + List.of(Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)), + List.of(Type.getType(String.class), Type.INT_TYPE, Type.getType(List.class)) + ); assertTrue(diff.insertions().isEmpty()); - - System.out.println("Replacements:"); - diff.replacements().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); assertEquals(1, diff.replacements().size()); } @Test public void testCompareCombinedParameters() { - Type[] original = new Type[]{Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)}; - Type[] modified = new Type[]{Type.getType(String.class), Type.FLOAT_TYPE, Type.INT_TYPE, Type.getType(List.class), Type.DOUBLE_TYPE}; - - ParametersDiff diff = ParametersDiff.compareTypeParameters(original, modified); - System.out.println("Original originalCount: " + original.length); - System.out.println("Actual originalCount: " + diff.originalCount()); - assertEquals(original.length, diff.originalCount()); - - System.out.println("Insertions:"); - diff.insertions().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); + LayeredParamsDiffSnapshot diff = diff( + List.of(Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)), + List.of(Type.getType(String.class), Type.FLOAT_TYPE, Type.INT_TYPE, Type.getType(List.class), Type.DOUBLE_TYPE) + ); assertEquals(2, diff.insertions().size()); - - System.out.println("Replacements:"); - diff.replacements().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); assertEquals(1, diff.replacements().size()); } @Test public void testCompareAppendedParameters() { - Type[] original = new Type[]{Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)}; - Type[] modified = new Type[]{Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class), Type.FLOAT_TYPE, Type.DOUBLE_TYPE}; - - ParametersDiff diff = ParametersDiff.compareTypeParameters(original, modified); - System.out.println("Original originalCount: " + original.length); - System.out.println("Actual originalCount: " + diff.originalCount()); - assertEquals(original.length, diff.originalCount()); - - System.out.println("Insertions:"); - diff.insertions().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); + LayeredParamsDiffSnapshot diff = diff( + List.of(Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class)), + List.of(Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class), Type.FLOAT_TYPE, Type.DOUBLE_TYPE) + ); assertEquals(2, diff.insertions().size()); - - System.out.println("Replacements:"); - diff.replacements().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); + assertEquals(3, diff.insertions().getFirst().index()); assertTrue(diff.replacements().isEmpty()); } @@ -100,43 +65,26 @@ public void testMethodParameterNameComparison() throws IOException { MethodNode cleanMethod = cleanNode.methods.stream().filter(m -> m.name.equals("namedInsertionTest")).findFirst().orElseThrow(); MethodNode dirtyMethod = dirtyNode.methods.stream().filter(m -> m.name.equals("namedInsertionTest")).findFirst().orElseThrow(); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.compareMethodParameters(cleanMethod, dirtyMethod); - - System.out.println("Insertions:"); - diff.insertions().forEach(param -> System.out.println("AT " + param.index() + " TYPE " + param.type())); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffParameters(cleanMethod, dirtyMethod, TypeRelation.STATIC); assertEquals(1, diff.insertions().size()); assertEquals(7, diff.insertions().getFirst().index()); assertEquals(Type.FLOAT_TYPE, diff.insertions().getFirst().type()); - - System.out.println("Replacements:"); - diff.replacements().forEach(param -> System.out.println("AT " + param.index() + " TYPE " + param.type())); assertTrue(diff.replacements().isEmpty()); } @Test public void testCompareRemovedParameters() { - Type[] original = new Type[]{Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class), Type.FLOAT_TYPE}; - Type[] modified = new Type[]{Type.getType(String.class), Type.getType(Object.class), Type.FLOAT_TYPE, Type.DOUBLE_TYPE}; - - ParametersDiff diff = ParametersDiff.compareTypeParameters(original, modified); - System.out.println("Original originalCount: " + original.length); - System.out.println("Actual originalCount: " + diff.originalCount()); - assertEquals(original.length, diff.originalCount()); - - System.out.println("Insertions:"); - diff.insertions().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); + LayeredParamsDiffSnapshot diff = diff( + List.of(Type.getType(String.class), Type.INT_TYPE, Type.getType(Object.class), Type.FLOAT_TYPE), + List.of(Type.getType(String.class), Type.getType(Object.class), Type.FLOAT_TYPE, Type.DOUBLE_TYPE) + ); assertEquals(1, diff.insertions().size()); - - System.out.println("Replacements:"); - diff.replacements().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); assertTrue(diff.replacements().isEmpty()); - - System.out.println("Swaps:"); - diff.swaps().forEach(param -> System.out.println("AT " + param.getFirst() + " TYPE " + param.getSecond())); assertTrue(diff.swaps().isEmpty()); - - System.out.println("Removals:"); - diff.removals().forEach(param -> System.out.println("AT " + param)); assertEquals(1, diff.removals().size()); } + + private static LayeredParamsDiffSnapshot diff(List clean, List dirty) { + return ParamsEditDistance.diffTypes(clean, dirty); + } } diff --git a/test/src/test/java/org/sinytra/adapter/patch/test/EnhancedParamsDiffTest.java b/test/src/test/java/org/sinytra/adapter/patch/test/ParamsDiffRegressionTest.java similarity index 89% rename from test/src/test/java/org/sinytra/adapter/patch/test/EnhancedParamsDiffTest.java rename to test/src/test/java/org/sinytra/adapter/patch/test/ParamsDiffRegressionTest.java index d4287ee6..20251604 100644 --- a/test/src/test/java/org/sinytra/adapter/patch/test/EnhancedParamsDiffTest.java +++ b/test/src/test/java/org/sinytra/adapter/patch/test/ParamsDiffRegressionTest.java @@ -2,18 +2,18 @@ import org.junit.jupiter.api.Test; import org.objectweb.asm.Type; -import org.sinytra.adapter.analysis.params.EnhancedParamsDiff; +import org.sinytra.adapter.analysis.params.ParamsEditDistance; import org.sinytra.adapter.analysis.params.LayeredParamsDiffSnapshot; -import org.sinytra.adapter.transform.param.InjectParameterTransform; -import org.sinytra.adapter.transform.param.ReplaceParametersTransformer; -import org.sinytra.adapter.transform.param.RemoveParameterTransformer; +import org.sinytra.adapter.edit.param.InjectParameterTransform; +import org.sinytra.adapter.edit.param.RemoveParameterTransformer; +import org.sinytra.adapter.edit.param.ReplaceParametersTransformer; import java.util.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class EnhancedParamsDiffTest { +public class ParamsDiffRegressionTest { @Test void testMovedParameter() { @@ -37,7 +37,7 @@ void testMovedParameter() { Type.BOOLEAN_TYPE ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(clean, dirty); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(clean, dirty); assertEquals(1, diff.insertions().size()); assertTrue(diff.replacements().isEmpty()); assertTrue(diff.removals().isEmpty()); @@ -60,7 +60,7 @@ void testReplacedParameters() { Type.getObjectType("com/llamalad7/mixinextras/sugar/ref/LocalRef") ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(clean, dirty); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(clean, dirty); assertTrue(diff.insertions().isEmpty()); assertEquals(2, diff.replacements().size()); assertTrue(diff.removals().isEmpty()); @@ -81,7 +81,7 @@ void testRemovedParameter() { Type.getType("Lnet/minecraft/world/item/Item;") ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(clean, dirty); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(clean, dirty); assertTrue(diff.insertions().isEmpty()); assertTrue(diff.replacements().isEmpty()); assertEquals(1, diff.removals().size()); @@ -105,7 +105,7 @@ void testCompareSwappedParameters() { Type.getObjectType("net/minecraft/world/item/ItemStack") ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertTrue(diff.insertions().isEmpty()); assertTrue(diff.replacements().isEmpty()); assertTrue(diff.removals().isEmpty()); @@ -128,7 +128,7 @@ void testCompareInsertedParameters() { Type.getType(Object.class) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(2, diff.insertions().size()); assertTrue(diff.replacements().isEmpty()); assertTrue(diff.removals().isEmpty()); @@ -152,7 +152,7 @@ void testCompareInsertedParameters2() { Type.getType(Object.class) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(1, diff.insertions().size()); assertEquals(2, diff.insertions().getFirst().index()); assertTrue(diff.replacements().isEmpty()); @@ -186,7 +186,7 @@ void testCompareInsertedParameters3() { new InjectParameterTransform(7, Type.INT_TYPE) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(6, diff.insertions().size()); assertEquals(expectedInsertions, diff.insertions()); assertTrue(diff.replacements().isEmpty()); @@ -230,7 +230,7 @@ void testCompareInsertedParametersOrder() { new InjectParameterTransform(12, Type.getType(String.class)) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(9, diff.insertions().size()); assertEquals(expectedInsertions, diff.insertions()); assertTrue(diff.replacements().isEmpty()); @@ -271,7 +271,7 @@ void testCompareInsertedParametersIndexOffset() { Type.INT_TYPE ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(5, diff.insertions().size()); assertEquals(1, diff.insertions().get(0).index()); assertEquals(9, diff.insertions().get(1).index()); @@ -297,7 +297,7 @@ void testCompareReplacedParameters() { Type.getType(List.class) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertTrue(diff.insertions().isEmpty()); assertEquals(1, diff.replacements().size()); assertTrue(diff.removals().isEmpty()); @@ -318,7 +318,7 @@ void testCompareReplacedParameters2() { Type.getType(List.class) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertTrue(diff.insertions().isEmpty()); assertEquals(1, diff.replacements().size()); assertTrue(diff.removals().isEmpty()); @@ -339,7 +339,7 @@ void testCompareReplacedParameters3() { Type.getObjectType("com/llamalad7/mixinextras/injector/wrapoperation/Operation") ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertTrue(diff.insertions().isEmpty()); assertEquals(1, diff.replacements().size()); assertTrue(diff.removals().isEmpty()); @@ -362,7 +362,7 @@ void testCompareCombinedParameters() { Type.DOUBLE_TYPE ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(2, diff.insertions().size()); assertEquals(1, diff.replacements().size()); assertTrue(diff.removals().isEmpty()); @@ -386,7 +386,7 @@ void testCompareInsertedParametersAtDifferentPlaces() { Type.DOUBLE_TYPE ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(3, diff.insertions().size()); assertEquals(new InjectParameterTransform(1, Type.FLOAT_TYPE), diff.insertions().get(0)); assertEquals(new InjectParameterTransform(4, Type.FLOAT_TYPE), diff.insertions().get(1)); @@ -412,7 +412,7 @@ void testCompareRemovedParameters() { Type.DOUBLE_TYPE ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(1, diff.insertions().size()); assertEquals(new InjectParameterTransform(3, Type.DOUBLE_TYPE), diff.insertions().getFirst()); assertTrue(diff.replacements().isEmpty()); @@ -441,7 +441,7 @@ void testCompareReorderedParameters() { Type.getType(Set.class) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(1, diff.insertions().size()); assertEquals(new InjectParameterTransform(2, Type.getType(Deque.class)), diff.insertions().getFirst()); assertTrue(diff.replacements().isEmpty()); @@ -459,7 +459,7 @@ void testCompareComplexChanges() { Type.getType(Object.class) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); assertEquals(1, diff.replacements().size()); assertEquals(new ReplaceParametersTransformer(0, Type.getType(Object.class)), diff.replacements().getFirst()); assertEquals(1, diff.removals().size()); @@ -480,7 +480,7 @@ void testRemovedParamsOrder() { Type.getType(String.class) ); - LayeredParamsDiffSnapshot diff = EnhancedParamsDiff.createLayered(original, modified); - assertEquals(List.of(1, 4, 3, 2, 1), diff.removals().stream().map(RemoveParameterTransformer::index).toList()); + LayeredParamsDiffSnapshot diff = ParamsEditDistance.diffTypes(original, modified); + assertEquals(List.of(5, 4, 3, 2, 1), diff.removals().stream().map(RemoveParameterTransformer::index).toList()); } }