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;
+ }
+}