From 1046304d933dd4927cd8ed59c88de8e7a19404f5 Mon Sep 17 00:00:00 2001 From: NebelNidas <48808497+NebelNidas@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:24:36 +0100 Subject: [PATCH] Add outer class name inheriting visitor (#89) --- CHANGELOG.md | 1 + .../OuterClassNameInheritingVisitor.java | 203 +++++++++++++ .../OuterClassNameInheritingVisitorTest.java | 267 ++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 src/main/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitor.java create mode 100644 src/test/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitorTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b65a66d..091d1141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added JAM reader and writer - Added JOBF reader and writer - Added Recaf Simple reader and writer +- Added `OuterClassNameInheritingVisitor` - Added `MappingFormat#hasWriter` boolean ## [0.5.1] - 2023-11-30 diff --git a/src/main/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitor.java b/src/main/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitor.java new file mode 100644 index 00000000..1812cb24 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitor.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.mappingio.adapter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingFlag; +import net.fabricmc.mappingio.MappingUtil; +import net.fabricmc.mappingio.MappingVisitor; + +/** + * Searches for inner classes with no mapped name, whose enclosing classes do have mapped names, + * and applies those to the outer part of the inner classes' fully qualified name. + * + *

For example, it takes a class {@code class_1$class_2} that doesn't have a mapping, + * tries to find {@code class_1}, which let's say has the mapping {@code SomeClass}, + * and changes the former's destination name to {@code SomeClass$class_2}. + */ +public class OuterClassNameInheritingVisitor extends ForwardingMappingVisitor { + protected OuterClassNameInheritingVisitor(MappingVisitor next) { + super(next); + } + + @Override + public Set getFlags() { + Set ret = EnumSet.noneOf(MappingFlag.class); + ret.addAll(next.getFlags()); + ret.add(MappingFlag.NEEDS_MULTIPLE_PASSES); + + return ret; + } + + @Override + public boolean visitHeader() throws IOException { + if (pass < firstEmitPass) return true; + + return super.visitHeader(); + } + + @Override + @SuppressWarnings("unchecked") + public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException { + dstNsCount = dstNamespaces.size(); + + if (pass == collectClassesPass) { + visitedDstName = new boolean[dstNsCount]; + dstNameBySrcNameByNamespace = new HashMap[dstNsCount]; + } else if (pass >= firstEmitPass) { + super.visitNamespaces(srcNamespace, dstNamespaces); + } + } + + @Override + public void visitMetadata(String key, @Nullable String value) throws IOException { + if (pass < firstEmitPass) return; + + super.visitMetadata(key, value); + } + + @Override + public boolean visitContent() throws IOException { + if (pass < firstEmitPass) return true; + + return super.visitContent(); + } + + @Override + public boolean visitClass(String srcName) throws IOException { + this.srcName = srcName; + + if (pass == collectClassesPass) { + dstNamesBySrcName.putIfAbsent(srcName, new String[dstNsCount]); + } else if (pass >= firstEmitPass) { + super.visitClass(srcName); + } + + return true; + } + + @Override + public void visitDstName(MappedElementKind targetKind, int namespace, String name) throws IOException { + if (pass == collectClassesPass) { + if (targetKind != MappedElementKind.CLASS) return; + + dstNamesBySrcName.get(srcName)[namespace] = name; + } else if (pass >= firstEmitPass) { + if (targetKind == MappedElementKind.CLASS) { + visitedDstName[namespace] = true; + name = dstNamesBySrcName.get(srcName)[namespace]; + } + + super.visitDstName(targetKind, namespace, name); + } + } + + @Override + public void visitDstDesc(MappedElementKind targetKind, int namespace, String desc) throws IOException { + if (pass < firstEmitPass) return; + + if (modifiedClasses.contains(srcName)) { + Map nsDstNameBySrcName = dstNameBySrcNameByNamespace[namespace]; + + if (nsDstNameBySrcName == null) { + dstNameBySrcNameByNamespace[namespace] = nsDstNameBySrcName = dstNamesBySrcName.entrySet() + .stream() + .filter(entry -> entry.getValue()[namespace] != null) + .collect(HashMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()[namespace]), HashMap::putAll); + } + + desc = MappingUtil.mapDesc(desc, nsDstNameBySrcName); + } + + super.visitDstDesc(targetKind, namespace, desc); + } + + @Override + public boolean visitElementContent(MappedElementKind targetKind) throws IOException { + if (targetKind == MappedElementKind.CLASS && pass > collectClassesPass) { + String[] dstNames = dstNamesBySrcName.get(srcName); + + for (int ns = 0; ns < dstNames.length; ns++) { + String dstName = dstNames[ns]; + + if (pass == fixOuterClassesPass) { + if (dstName != null) continue; // skip if already mapped + + String[] parts = srcName.split(Pattern.quote("$")); + + for (int pos = parts.length - 2; pos >= 0; pos--) { + String outerSrcName = String.join("$", Arrays.copyOfRange(parts, 0, pos + 1)); + String outerDstName = dstNamesBySrcName.get(outerSrcName)[ns]; + + if (outerDstName != null) { + dstName = outerDstName + "$" + String.join("$", Arrays.copyOfRange(parts, pos + 1, parts.length)); + + dstNames[ns] = dstName; + modifiedClasses.add(srcName); + break; + } + } + } else if (!visitedDstName[ns]) { + if (dstName == null) continue; // skip if not mapped + + // Class didn't have a mapping before we added one, + // so we have to call visitDstName manually. + super.visitDstName(targetKind, ns, dstName); + } + } + } + + if (pass < firstEmitPass) { + return false; // prevent other element visits, we only care about classes here + } + + Arrays.fill(visitedDstName, false); + return super.visitElementContent(targetKind); + } + + @Override + public boolean visitEnd() throws IOException { + if (pass++ < firstEmitPass) { + return false; + } + + return super.visitEnd(); + } + + private static final int collectClassesPass = 1; + private static final int fixOuterClassesPass = 2; + private static final int firstEmitPass = 3; + private final Map dstNamesBySrcName = new HashMap<>(); + private final Set modifiedClasses = new HashSet<>(); + private int pass = 1; + private int dstNsCount = -1; + private String srcName; + private boolean[] visitedDstName; + private Map[] dstNameBySrcNameByNamespace; +} diff --git a/src/test/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitorTest.java b/src/test/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitorTest.java new file mode 100644 index 00000000..7d351a19 --- /dev/null +++ b/src/test/java/net/fabricmc/mappingio/adapter/OuterClassNameInheritingVisitorTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2023 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.mappingio.adapter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingFlag; +import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.NopMappingVisitor; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import net.fabricmc.mappingio.tree.VisitableMappingTree; + +public class OuterClassNameInheritingVisitorTest { + private static void accept(MappingVisitor visitor) throws IOException { + do { + if (visitor.visitHeader()) { + visitor.visitNamespaces("source", Arrays.asList("dstNs0", "dstNs1", "dstNs2", "dstNs3", "dstNs4", "dstNs5", "dstNs6")); + } + + if (visitor.visitContent()) { + if (visitor.visitClass("class_1")) { + visitor.visitDstName(MappedElementKind.CLASS, 0, "class1Ns0Rename"); + visitor.visitDstName(MappedElementKind.CLASS, 1, "class1Ns1Rename"); + visitor.visitDstName(MappedElementKind.CLASS, 2, "class1Ns2Rename"); + visitor.visitDstName(MappedElementKind.CLASS, 4, "class1Ns4Rename"); + + if (visitor.visitElementContent(MappedElementKind.CLASS)) { + if (visitor.visitField("field_1", "Lclass_1;")) { + for (int i = 0; i <= 6; i++) { + visitor.visitDstDesc(MappedElementKind.FIELD, i, "Lclass_1;"); + visitor.visitElementContent(MappedElementKind.FIELD); + } + } + } + } + + if (visitor.visitClass("class_1$class_2")) { + visitor.visitDstName(MappedElementKind.CLASS, 2, "class1Ns2Rename$class2Ns2Rename"); + visitor.visitDstName(MappedElementKind.CLASS, 3, "class_1$class2Ns3Rename"); + visitor.visitDstName(MappedElementKind.CLASS, 4, "class_1$class_2"); + visitor.visitDstName(MappedElementKind.CLASS, 5, "class_1$class2Ns5Rename"); + + if (visitor.visitElementContent(MappedElementKind.CLASS)) { + if (visitor.visitField("field_2", "Lclass_1$class_2;")) { + for (int i = 0; i <= 6; i++) { + visitor.visitDstDesc(MappedElementKind.FIELD, i, "Lclass_1$class_2;"); + visitor.visitElementContent(MappedElementKind.FIELD); + } + } + } + } + + if (visitor.visitClass("class_1$class_2$class_3")) { + visitor.visitDstName(MappedElementKind.CLASS, 5, "class_1$class2Ns5Rename$class3Ns5Rename"); + visitor.visitDstName(MappedElementKind.CLASS, 6, "class_1$class_2$class3Ns6Rename"); + + if (visitor.visitElementContent(MappedElementKind.CLASS)) { + if (visitor.visitField("field_2", "Lclass_1$class_2$class_3;")) { + for (int i = 0; i <= 6; i++) { + visitor.visitDstDesc(MappedElementKind.FIELD, i, "Lclass_1$class_2$class_3;"); + visitor.visitElementContent(MappedElementKind.FIELD); + } + } + } + } + } + } while (!visitor.visitEnd()); + } + + @Test + public void directVisit() throws IOException { + accept(new OuterClassNameInheritingVisitor(new CheckingVisitor(false))); + } + + @Test + public void tree() throws IOException { + VisitableMappingTree tree = new MemoryMappingTree(); + accept(new OuterClassNameInheritingVisitor(tree)); + tree.accept(new CheckingVisitor(true)); + } + + private static class CheckingVisitor extends NopMappingVisitor { + CheckingVisitor(boolean tree) { + super(true); + this.tree = tree; + } + + @Override + public Set getFlags() { + return EnumSet.of(MappingFlag.NEEDS_DST_FIELD_DESC, MappingFlag.NEEDS_DST_METHOD_DESC); + } + + @Override + public boolean visitClass(String srcName) throws IOException { + clsSrcName = srcName; + return true; + } + + @Override + public void visitDstDesc(MappedElementKind targetKind, int namespace, String desc) throws IOException { + if (tree) return; // trees handle destination descriptor remapping themselves + + switch (clsSrcName) { + case "class_1": + assertEquals("Lclass_1;", desc); + break; + case "class_1$class_2": + switch (namespace) { + case 0: + assertEquals("Lclass1Ns0Rename$class_2;", desc); + break; + case 1: + assertEquals("Lclass1Ns1Rename$class_2;", desc); + break; + case 2: + assertEquals("Lclass1Ns2Rename$class2Ns2Rename;", desc); + break; + case 3: + assertEquals("Lclass_1$class2Ns3Rename;", desc); + break; + case 4: + assertEquals("Lclass_1$class_2;", desc); + break; + case 5: + assertEquals("Lclass_1$class2Ns5Rename;", desc); + break; + case 6: + assertEquals("Lclass_1$class_2;", desc); + break; + default: + throw new IllegalStateException(); + } + + break; + case "class_1$class_2$class_3": + switch (namespace) { + case 0: + assertEquals("Lclass1Ns0Rename$class_2$class_3;", desc); + break; + case 1: + assertEquals("Lclass1Ns1Rename$class_2$class_3;", desc); + break; + case 2: + assertEquals("Lclass1Ns2Rename$class2Ns2Rename$class_3;", desc); + break; + case 3: + assertEquals("Lclass_1$class2Ns3Rename$class_3;", desc); + break; + case 4: + assertEquals("Lclass_1$class_2$class_3;", desc); + break; + case 5: + assertEquals("Lclass_1$class2Ns5Rename$class3Ns5Rename;", desc); + break; + case 6: + assertEquals("Lclass_1$class_2$class3Ns6Rename;", desc); + break; + default: + throw new IllegalStateException(); + } + + break; + default: + throw new IllegalStateException(); + } + } + + @Override + public void visitDstName(MappedElementKind targetKind, int namespace, String name) throws IOException { + if (targetKind != MappedElementKind.CLASS) return; + + switch (clsSrcName) { + case "class_1": + break; + case "class_1$class_2": + switch (namespace) { + case 0: + assertEquals("class1Ns0Rename$class_2", name); + break; + case 1: + assertEquals("class1Ns1Rename$class_2", name); + break; + case 2: + assertEquals("class1Ns2Rename$class2Ns2Rename", name); + break; + case 3: + assertEquals("class_1$class2Ns3Rename", name); + break; + case 4: + assertEquals("class_1$class_2", name); + break; + case 5: + assertEquals("class_1$class2Ns5Rename", name); + break; + case 6: + assertEquals("class_1$class_2", name); + break; + default: + throw new IllegalStateException(); + } + + break; + case "class_1$class_2$class_3": + switch (namespace) { + case 0: + assertEquals("class1Ns0Rename$class_2$class_3", name); + break; + case 1: + assertEquals("class1Ns1Rename$class_2$class_3", name); + break; + case 2: + assertEquals("class1Ns2Rename$class2Ns2Rename$class_3", name); + break; + case 3: + assertEquals("class_1$class2Ns3Rename$class_3", name); + break; + case 4: + assertEquals("class_1$class_2$class_3", name); + break; + case 5: + assertEquals("class_1$class2Ns5Rename$class3Ns5Rename", name); + break; + case 6: + assertEquals("class_1$class_2$class3Ns6Rename", name); + break; + default: + throw new IllegalStateException(); + } + + break; + default: + throw new IllegalStateException(); + } + } + + @Override + public boolean visitEnd() throws IOException { + return ++passesDone == 2; + } + + private final boolean tree; + private byte passesDone = 0; + private String clsSrcName; + } +}