diff --git a/build.gradle b/build.gradle index 13063306d7..ed72249f37 100644 --- a/build.gradle +++ b/build.gradle @@ -106,6 +106,7 @@ import net.fabricmc.filament.task.mappingio.FormatMappingsTask import net.fabricmc.filament.task.mappingio.MergeMappingsTask import net.fabricmc.filament.task.mappingio.CheckMappingsTask import net.fabricmc.filament.task.mappingio.CheckMergedMappingsTask +import net.fabricmc.filament.task.mappingio.PropagateHierarchyTask import net.fabricmc.filament.nameproposal.MappingNameCompleter import net.fabricmc.mappingio.format.MappingFormat import org.gradle.work.DisableCachingByDefault @@ -185,14 +186,24 @@ tasks.register('mapSpecializedMethods', MapSpecializedMethodsTask) { outputMappingsFormat = "tinyv2:intermediary:named" } +tasks.register('propagateNames', PropagateHierarchyTask) { + intermediaryJarFile = mapIntermediaryJar.output + mappingsDirectory = mappingsDir + srcNamespace = "intermediary" + dstNamespace = "named" + classpath.from minecraftLibraries + output = new File(tempDir, "yarn-propagated-v2.tiny") + outputFormat = MappingFormat.TINY_2_FILE +} + tasks.register('completeMappings', CompleteMappingsTask) { - input = mapSpecializedMethods.output + input = propagateNames.output output = new File(tempDir, "yarn-mappings-v2.tiny") outputFormat = MappingFormat.TINY_2_FILE } tasks.register('convertToV1', ConvertMappingsTask) { - input = mapSpecializedMethods.output + input = propagateNames.output output = new File(tempDir, "yarn-mappings.tiny") outputFormat = MappingFormat.TINY_FILE } diff --git a/filament/build.gradle b/filament/build.gradle index c0f584dee8..2bd1d1a354 100644 --- a/filament/build.gradle +++ b/filament/build.gradle @@ -28,19 +28,26 @@ repositories { } dependencies { - implementation "org.ow2.asm:asm:${properties.asm_version}" - implementation "org.ow2.asm:asm-tree:${properties.asm_version}" + implementation "org.ow2.asm:asm:$properties.asm_version" + implementation "org.ow2.asm:asm-tree:$properties.asm_version" implementation "cuchaz:enigma:$properties.enigma_version" implementation "cuchaz:enigma-cli:$properties.enigma_version" implementation "net.fabricmc.unpick:unpick:$properties.unpick_version" implementation "net.fabricmc.unpick:unpick-format-utils:$properties.unpick_version" implementation "net.fabricmc.unpick:unpick-cli:$properties.unpick_version" implementation "net.fabricmc:tiny-remapper:$properties.tiny_remapper_version" - implementation 'net.fabricmc:mapping-io:0.6.1' - implementation 'net.fabricmc:javapoet:0.1.1' + implementation "net.fabricmc:mapping-io:$properties.mappingio_version" + implementation "net.fabricmc:javapoet:$properties.javapoet_version" + + implementation platform("dev.denwav.hypo:hypo-platform:$properties.hypo_version") + implementation 'dev.denwav.hypo:hypo-model' + implementation 'dev.denwav.hypo:hypo-core' + implementation 'dev.denwav.hypo:hypo-hydrate' + implementation 'dev.denwav.hypo:hypo-asm' + implementation 'dev.denwav.hypo:hypo-asm-hydrate' // Contains a number of useful utilities we can re-use. - implementation ("net.fabricmc:fabric-loom:1.7.3") { + implementation ("net.fabricmc:fabric-loom:$properties.loom_version") { transitive = false } diff --git a/filament/src/main/java/net/fabricmc/filament/task/enigma/MapSpecializedMethodsTask.java b/filament/src/main/java/net/fabricmc/filament/task/enigma/MapSpecializedMethodsTask.java index d6483a6a69..5b08ddb11f 100644 --- a/filament/src/main/java/net/fabricmc/filament/task/enigma/MapSpecializedMethodsTask.java +++ b/filament/src/main/java/net/fabricmc/filament/task/enigma/MapSpecializedMethodsTask.java @@ -15,6 +15,7 @@ import net.fabricmc.filament.task.base.WithFileOutput; +@Deprecated(forRemoval = true) public abstract class MapSpecializedMethodsTask extends EnigmaCommandTask implements WithFileOutput { @InputFile public abstract RegularFileProperty getIntermediaryJarFile(); diff --git a/filament/src/main/java/net/fabricmc/filament/task/mappingio/CheckMergedMappingsTask.java b/filament/src/main/java/net/fabricmc/filament/task/mappingio/CheckMergedMappingsTask.java index e60b3a5d15..8469745493 100644 --- a/filament/src/main/java/net/fabricmc/filament/task/mappingio/CheckMergedMappingsTask.java +++ b/filament/src/main/java/net/fabricmc/filament/task/mappingio/CheckMergedMappingsTask.java @@ -3,9 +3,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import org.gradle.api.tasks.TaskAction; import org.jetbrains.annotations.Nullable; @@ -27,10 +25,7 @@ public final void run() throws IOException { List errors = new ArrayList<>(); MappingReader.read(path, new MappingVisitor() { - private final Set mthDstNames = new HashSet<>(); private String clsSrcName; - private String mthSrcName; - private String mthSrcDesc; @Override public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException { @@ -57,9 +52,10 @@ public boolean visitField(String srcName, @Nullable String srcDesc) throws IOExc @Override public boolean visitMethod(String srcName, @Nullable String srcDesc) throws IOException { - mthSrcName = srcName; - mthSrcDesc = srcDesc; - mthDstNames.clear(); + if (srcName.startsWith("method_")) { + errors.add("Encountered mapping for non-existent method " + clsSrcName + "#" + srcName + srcDesc); + } + return true; } @@ -75,25 +71,6 @@ public boolean visitMethodVar(int lvtRowIndex, int lvIndex, int startOpIdx, int @Override public void visitDstName(MappedElementKind targetKind, int namespace, String name) throws IOException { - if (targetKind == MappedElementKind.METHOD) { - mthDstNames.add(name); - } - } - - @Override - public boolean visitElementContent(MappedElementKind targetKind) throws IOException { - if (targetKind == MappedElementKind.METHOD) { - // Checking if the srcName is an intermediary name (like for classes and fields) would be the more correct option, - // but Enigma's bridge method mapper behaves weirdly and injects copies of the mapping into the whole hierarchy. - // We haven't been able to fix this yet, so in the meantime, we're using the following workaround, - // which ignores any Enigma-bridge-mapper generated mappings, but should still catch all other - // mappings without official names. - if (mthDstNames.size() == 1 && mthDstNames.contains(mthSrcName) && mthSrcName.startsWith("method_")) { - errors.add("Encountered mapping for non-existent method " + clsSrcName + "#" + mthSrcName + mthSrcDesc); - } - } - - return true; } @Override diff --git a/filament/src/main/java/net/fabricmc/filament/task/mappingio/HypoHierarchyProvider.java b/filament/src/main/java/net/fabricmc/filament/task/mappingio/HypoHierarchyProvider.java new file mode 100644 index 0000000000..a5de055a85 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/task/mappingio/HypoHierarchyProvider.java @@ -0,0 +1,418 @@ +package net.fabricmc.filament.task.mappingio; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import dev.denwav.hypo.core.HypoContext; +import dev.denwav.hypo.hydrate.generic.HypoHydration; +import dev.denwav.hypo.model.ClassDataProvider; +import dev.denwav.hypo.model.data.ClassData; +import dev.denwav.hypo.model.data.ClassKind; +import dev.denwav.hypo.model.data.FieldData; +import dev.denwav.hypo.model.data.MethodData; +import dev.denwav.hypo.model.data.MethodDescriptor; +import dev.denwav.hypo.model.data.Visibility; +import dev.denwav.hypo.model.data.types.ArrayType; +import dev.denwav.hypo.model.data.types.JvmType; +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.filament.task.mappingio.HypoHierarchyProvider.HierarchyData; +import net.fabricmc.mappingio.tree.HierarchyInfoProvider; +import net.fabricmc.mappingio.tree.MappingTreeView; + +// TODO: Move this class to Mapping-IO +class HypoHierarchyProvider implements HierarchyInfoProvider { + private final HypoContext hypo; + private final String namespace; + + HypoHierarchyProvider(HypoContext hypoContext, String namespace) { + this.hypo = hypoContext; + this.namespace = namespace; + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + @Nullable + public String resolveField(String owner, String name, @Nullable String desc) { + ClassData cls = getClassData(owner); + if (cls == null) return null; + + FieldData field; + + try { + field = resolveField(cls, name, desc); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return field != null ? field.parentClass().name() : null; + } + + /* + * Based on Tiny Remapper's ClassInstance#resolveField (commit b22c17e). + */ + @Nullable + private FieldData resolveField(ClassData cls, String name, @Nullable String desc) throws IOException { + FieldData field = getFieldData(cls, name, desc); + + if (field != null) return field; + + Deque queue = new ArrayDeque<>(); + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + visited.add(cls); + ClassData context = cls; + + for (;;) { // overall-recursion for fields + // step 1 + // search in all direct super interfaces recursively + + ClassData currentCls = context; + + do { + for (ClassData parent : currentCls.interfaces()) { + if (visited.add(parent)) { + FieldData ret = getFieldData(parent, name, desc); + if (ret != null) return ret; + + queue.addLast(parent); + } + } + } while ((currentCls = queue.pollLast()) != null); + + // step 2 + // search in all super classes recursively (self-lookup and queue only, outer loop will recurse) + + currentCls = context; + context = currentCls.superClass(); + if (context == null) break; + + FieldData parentField = getFieldData(context, name, desc); + if (parentField != null) return parentField; + } + + return null; + } + + @Override + @Nullable + public String resolveMethod(String owner, String name, @Nullable String desc) { + ClassData cls = getClassData(owner); + if (cls == null) return null; + + MethodData method; + + try { + method = resolveMethod(cls, name, desc); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return method != null ? method.parentClass().name() : null; + } + + /* + * Based on Tiny Remapper's ClassInstance#resolveMethod (commit b22c17e). + */ + @Nullable + private MethodData resolveMethod(final ClassData cls, String name, @Nullable String desc) throws IOException { + MethodData method = getMethodData(cls, name, desc); + + if (method != null) return method; + + // step 1 + // search in all super classes recursively + + ClassData currentCls = cls; + + while ((currentCls = currentCls.superClass()) != null) { + MethodData ret = getMethodData(currentCls, name, desc); + if (ret != null) return ret; + } + + // step 2 + // search for non-static, non-private, non-abstract in all super interfaces recursively + // (breadth first search to obtain the potentially maximally-specific superinterface directly) + // step 3 + // bridgeMethod: search for non-static, non-private in all super interfaces recursively + + // step 3 is a super set of step 2 with any option being able to be "arbitrarily chosen" as per the jvm + // spec, so step 2 ignoring the "exactly one" match requirement doesn't matter and >potentially< + // maximally-specific superinterface is good enough + + Deque queue = new ArrayDeque<>(); + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + visited.add(cls); + List matchedMethods = new ArrayList<>(); + boolean hasNonAbstract = false; + currentCls = cls; + + do { + List parents = currentCls.allSuperClasses().toList(); + + for (ClassData parent : parents) { + if (!visited.add(parent)) continue; + + if (parent.is(ClassKind.INTERFACE)) { + MethodData parentMethod = getMethodData(parent, name, desc); + + if (parentMethod != null && !parentMethod.isStatic()) { // potential match + if (!parentMethod.isAbstract()) hasNonAbstract = true; + matchedMethods.add(parentMethod); + continue; // skip queuing, subclasses aren't relevant for maximally-specific selection + } + } + + queue.addLast(parent); + } + } while ((currentCls = queue.pollFirst()) != null); + + if (hasNonAbstract && matchedMethods.size() > 1) { + // try to select first maximally-specific superinterface bridgeMethod (doesn't matter if it's the only one, jvm spec allows arbitrary choice otherwise) + matchLoop: for (MethodData match : matchedMethods) { + if (match.isAbstract()) continue; + + for (MethodData m : matchedMethods) { + if (m != match && m.parentClass().doesExtendOrImplement(match.parentClass())) continue matchLoop; + } + + return match; + } + } + + if (!matchedMethods.isEmpty()) return matchedMethods.get(0); + + return null; + } + + @Override + @Nullable + public HierarchyData getMethodHierarchy(String owner, String name, @Nullable String desc) { + try { + return getMethodHierarchy0(owner, name, desc); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /* + * Based on Mapping-IO's TinyRemapperHierarchyProvider#getMethodHierarchy (commit 135f1b5). + */ + @Nullable + private HierarchyData getMethodHierarchy0(String owner, String name, @Nullable String desc) throws IOException { + ClassData cls = getClassData(owner); + if (cls == null) return null; + + MethodData method = getMethodData(cls, name, desc); + if (method == null) return null; + + if (method.isStatic()) { + return new HierarchyData(Collections.singleton(method)); + } + + List methods = new ArrayList<>(); + Queue toCheckUp = new ArrayDeque<>(); + Queue toCheckDown = new ArrayDeque<>(); + Set queuedUp = Collections.newSetFromMap(new IdentityHashMap<>()); + Set queuedDown = Collections.newSetFromMap(new IdentityHashMap<>()); + methods.add(method); + toCheckUp.add(method); + toCheckDown.add(method); + queuedUp.add(method); + queuedDown.add(method); + + do { + while ((method = toCheckUp.poll()) != null) { + MethodData superMethod = method.superMethod(); + Set syntheticSources = method.get(HypoHydration.SYNTHETIC_SOURCES); + Set upMethods = new HashSet<>(); + + if (superMethod != null) { + upMethods.add(superMethod); + } + + if (syntheticSources != null) { + for (MethodData syntheticSource : syntheticSources) { + if (isPotentialBridge(syntheticSource, method)) { + upMethods.add(syntheticSource); + } + } + } + + for (MethodData upMethod : upMethods) { + if (queuedDown.add(upMethod)) { + methods.add(upMethod); + toCheckDown.add(upMethod); + } + + if (queuedUp.add(upMethod)) { + toCheckUp.add(upMethod); + } + } + } + + while ((method = toCheckDown.poll()) != null) { + Set childMethods = method.childMethods(); + MethodData syntheticTarget = method.get(HypoHydration.SYNTHETIC_TARGET); + Set downMethods = new HashSet<>(); + + if (childMethods != null) { + downMethods.addAll(childMethods); + } + + if (syntheticTarget != null && isPotentialBridge(method, syntheticTarget)) { + downMethods.add(syntheticTarget); + } + + for (MethodData downMethod : downMethods) { + if (queuedUp.add(downMethod)) { + methods.add(downMethod); + toCheckUp.add(downMethod); + } + + if (queuedDown.add(downMethod)) { + toCheckDown.add(downMethod); + } + } + } + } while (!toCheckUp.isEmpty() || !toCheckDown.isEmpty()); + + assert methods.size() == new HashSet<>(methods).size(); + + return new HierarchyData(methods); + } + + @Override + public int getHierarchySize(HierarchyData hierarchy) { + return hierarchy != null ? hierarchy.methods.size() : 0; + } + + @Override + public Collection getHierarchyMethods(HierarchyData hierarchy, MappingTreeView tree) { + if (hierarchy == null) return Collections.emptyList(); + + List ret = new ArrayList<>(hierarchy.methods.size()); + int ns = tree.getNamespaceId(namespace); + assert ns != MappingTreeView.NULL_NAMESPACE_ID; + + for (MethodData method : hierarchy.methods) { + MappingTreeView.MethodMappingView m = tree.getMethod(method.parentClass().name(), method.name(), method.descriptorText(), ns); + if (m != null) ret.add(m); + } + + return ret; + } + + @Nullable + private ClassData getClassData(String name) { + return findClass(name, true); + } + + @Nullable + private FieldData getFieldData(ClassData owner, String name, String desc) { + for (FieldData field : owner.fields()) { + if (field.name().equals(name) && (desc == null || field.fieldType().asInternalName().equals(desc))) { + return field; + } + } + + return null; + } + + @Nullable + private MethodData getMethodData(ClassData owner, String name, String desc) { + for (MethodData method : owner.methods()) { + if (method.name().equals(name) && (desc == null || method.descriptorText().equals(desc))) { + return method; + } + } + + return null; + } + + private boolean isPotentialBridge(MethodData bridgeMethod, MethodData bridgedMethod) { + if (!bridgeMethod.isSynthetic()) return false; + if (bridgeMethod.isBridge()) return true; + + if (bridgeMethod.visibility() == Visibility.PRIVATE || bridgeMethod.isFinal() || bridgeMethod.isStatic()) { + return false; + } + + MethodDescriptor bridgeDesc = bridgeMethod.descriptor(); + MethodDescriptor bridgedDesc = bridgedMethod.descriptor(); + List bridgeParams = bridgeDesc.getParams(); + List bridgedParams = bridgedDesc.getParams(); + + if (bridgeParams.size() != bridgedParams.size()) { + return false; + } + + for (int i = 0; i < bridgeParams.size(); i++) { + if (!areTypesBridgeCompatible(bridgeParams.get(i), bridgedParams.get(i))) { + return false; + } + } + + return areTypesBridgeCompatible(bridgeDesc.getReturnType(), bridgedDesc.getReturnType()); + } + + private boolean areTypesBridgeCompatible(JvmType bridgeType, JvmType bridgedType) { + if (bridgeType.equals(bridgedType)) { + return true; + } + + ClassData bridgeClass = findClass(bridgeType.asInternalName(), true); + ClassData bridgedClass = findClass(bridgedType.asInternalName(), true); + + if (bridgeClass == null || bridgedClass == null) { + assert bridgeType instanceof ArrayType || bridgedType instanceof ArrayType; + return false; + } + + boolean bridgedExtendsBridge = bridgedClass.doesExtendOrImplement(bridgeClass); + + // If not equal, types in bridge method descriptors should always be less specific than in the bridged method + assert bridgedExtendsBridge || !bridgeClass.doesExtendOrImplement(bridgedClass); + + return bridgedExtendsBridge; + } + + private ClassData findClass(String name, boolean allowContext) { + ClassData cls = findClass(name, hypo.getProvider()); + + if (cls != null || !allowContext) { + return cls; + } + + return findClass(name, hypo.getContextProvider()); + } + + private ClassData findClass(String name, ClassDataProvider provider) { + try { + return provider.findClass(name); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static final class HierarchyData { + HierarchyData(Collection methods) { + this.methods = methods; + } + + final Collection methods; + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/task/mappingio/PropagateHierarchyTask.java b/filament/src/main/java/net/fabricmc/filament/task/mappingio/PropagateHierarchyTask.java new file mode 100644 index 0000000000..bc4b1588c3 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/task/mappingio/PropagateHierarchyTask.java @@ -0,0 +1,186 @@ +package net.fabricmc.filament.task.mappingio; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +import dev.denwav.hypo.asm.AsmClassDataProvider; +import dev.denwav.hypo.asm.hydrate.BridgeMethodHydrator; +import dev.denwav.hypo.core.HypoContext; +import dev.denwav.hypo.hydrate.HydrationManager; +import dev.denwav.hypo.model.ClassProviderRoot; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.InputFile; + +import net.fabricmc.loom.util.Pair; +import net.fabricmc.mappingio.MappingWriter; +import net.fabricmc.mappingio.format.enigma.EnigmaDirReader; +import net.fabricmc.mappingio.tree.HierarchyInfoProvider; +import net.fabricmc.mappingio.tree.MappingTree.ClassMapping; +import net.fabricmc.mappingio.tree.MappingTree.MethodMapping; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import net.fabricmc.mappingio.tree.VisitOrder; +import net.fabricmc.mappingio.tree.VisitableMappingTree; + +public abstract class PropagateHierarchyTask extends MappingOutputTask { + @InputFile + public abstract RegularFileProperty getIntermediaryJarFile(); + + @Classpath + public abstract ConfigurableFileCollection getClasspath(); + + @InputDirectory + public abstract DirectoryProperty getMappingsDirectory(); + + @Input + public abstract Property getSrcNamespace(); + + @Input + public abstract Property getDstNamespace(); + + @Override + void run(MappingWriter writer) throws IOException { + Path intermediaryJar = getIntermediaryJarFile().get().getAsFile().toPath(); + Path mappingsDir = getMappingsDirectory().get().getAsFile().toPath(); + String srcNs = getSrcNamespace().get(); + String dstNs = getDstNamespace().get(); + + HypoContext hypo = HypoContext.builder() + .withProvider(AsmClassDataProvider.of(ClassProviderRoot.fromJar(intermediaryJar))) + .withContextProvider(AsmClassDataProvider.of(ClassProviderRoot.ofJdk())) + .withContextProvider(AsmClassDataProvider.of(ClassProviderRoot.fromJars(getClasspath().getFiles().stream().map(File::toPath).toArray(Path[]::new)))) + .build(); + HydrationManager.createDefault() + .register(BridgeMethodHydrator.create()) + .hydrate(hypo); + var hierarchyProvider = new HypoHierarchyProvider(hypo, srcNs); + + VisitableMappingTree tree = new MemoryMappingTree(); + EnigmaDirReader.read(mappingsDir, srcNs, dstNs, tree); + + int nsId = tree.getNamespaceId(srcNs); + assert nsId != MappingTreeView.NULL_NAMESPACE_ID; + + propagateNames(tree, nsId, hierarchyProvider); + + tree.accept(writer, VisitOrder.createByName()); + hypo.close(); + } + + /** + * Copy of {@link MemoryMappingTree#propagateNames} (commit c123d0d) that reports duplicates and conflicts. + */ + private void propagateNames(VisitableMappingTree tree, int nsId, HierarchyInfoProvider hierarchyProvider) { + Set processed = Collections.newSetFromMap(new IdentityHashMap<>()); + List warnings = new ArrayList<>(); + List errors = new ArrayList<>(); + + for (ClassMapping cls : tree.getClasses()) { + for (MethodMapping method : cls.getMethods()) { + String name = method.getName(nsId); + if (name == null || name.startsWith("<")) continue; // missing name, or + if (!processed.add(method)) continue; + + T hierarchy = hierarchyProvider.getMethodHierarchy(method); + if (hierarchyProvider.getHierarchySize(hierarchy) <= 1) continue; + + Collection hierarchyMethods = hierarchyProvider.getHierarchyMethods(hierarchy, tree); + if (hierarchyMethods.size() <= 1) continue; + + String[] dstNames = new String[tree.getDstNamespaces().size()]; + List, Integer>> duplicatesByNs = new ArrayList<>(); + List, Integer>> conflictsByNs = new ArrayList<>(); + + for (int ns = 0; ns < dstNames.length; ns++) { + for (MethodMapping m : hierarchyMethods) { + String existingName = dstNames[ns]; + String currentName = m.getDstName(ns); + + if (currentName != null) { + if (existingName != null) { + if (existingName.equals(currentName)) { + duplicatesByNs.add(new Pair<>(hierarchyMethods, ns)); + continue; + } else { + conflictsByNs.add(new Pair<>(hierarchyMethods, ns)); + break; + } + } + + dstNames[ns] = currentName; + } + } + } + + if (!duplicatesByNs.isEmpty()) { + addBadMappingsToErrorList(duplicatesByNs, tree, warnings, false); + } + + if (!conflictsByNs.isEmpty()) { + addBadMappingsToErrorList(conflictsByNs, tree, errors, true); + } + + for (MethodMapping m : hierarchyMethods) { + processed.add(m); + + for (int ns = 0; ns < dstNames.length; ns++) { + String currentName = dstNames[ns]; + + if (currentName != null) { + m.setDstName(currentName.equals(m.getSrcName()) ? null : currentName, ns); + } + } + } + } + } + + if (!warnings.isEmpty()) { + getLogger().warn("Warnings while propagating:\n{}", String.join("\n", warnings)); + } + + if (!errors.isEmpty()) { + throw new RuntimeException("Failed to propagate:\n" + String.join("\n", errors)); + } + } + + private static void addBadMappingsToErrorList(List, Integer>> mappingsByNs, MappingTreeView tree, List targetList, boolean conflicting) { + for (Pair, Integer> pair : mappingsByNs) { + Collection hierarchyMethods = pair.left(); + int ns = pair.right(); + StringBuilder sb = new StringBuilder("- ") + .append(conflicting ? "Conflicting" : "Duplicate") + .append(" names in hierarchy for namespace '") + .append(tree.getNamespaceName(ns)) + .append("':"); + + for (MethodMapping m : hierarchyMethods) { + if (m.getDstName(ns) == null) { + continue; + } + + sb.append("\n - ") + .append(m.getOwner().getSrcName()) + .append(".") + .append(m.getSrcName()) + .append(m.getSrcDesc() != null ? m.getSrcDesc() : "") + .append(" -> ") + .append(m.getDstName(ns)); + } + + targetList.add(sb.toString()); + } + } +} diff --git a/gradle.properties b/gradle.properties index 50542fc88f..32cb28b807 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,5 +15,9 @@ jetbrains_annotations_version=24.1.0 # Build logic tiny_remapper_version=0.10.4 +mappingio_version=0.6.1 +javapoet_version=0.1.1 +hypo_version = 2.3.0 +loom_version=1.7.3 junit_version=5.11.0 -assertj_version=3.26.3 +assertj_version=3.26.3 \ No newline at end of file