diff --git a/CHANGELOG.md b/CHANGELOG.md index 7865a0d7..7dde2bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Added IntelliJ IDEA migration map reader and writer +- Added `MappingFormat#features()` to allow for more fine-grained programmatic querying of format capabilities - Overhauled the internal `ColumnFileReader` to behave more consistently - Made handling of the `NEEDS_MULTIPLE_PASSES` flag more consistent, reducing memory usage in a few cases - Made some internal methods in Enigma and TSRG readers actually private diff --git a/src/main/java/net/fabricmc/mappingio/MappingReader.java b/src/main/java/net/fabricmc/mappingio/MappingReader.java index c69eff93..3201bf61 100644 --- a/src/main/java/net/fabricmc/mappingio/MappingReader.java +++ b/src/main/java/net/fabricmc/mappingio/MappingReader.java @@ -31,6 +31,7 @@ import net.fabricmc.mappingio.format.MappingFormat; import net.fabricmc.mappingio.format.enigma.EnigmaDirReader; import net.fabricmc.mappingio.format.enigma.EnigmaFileReader; +import net.fabricmc.mappingio.format.intellij.MigrationMapFileReader; import net.fabricmc.mappingio.format.jobf.JobfFileReader; import net.fabricmc.mappingio.format.proguard.ProGuardFileReader; import net.fabricmc.mappingio.format.simple.RecafSimpleFileReader; @@ -105,7 +106,9 @@ private static MappingFormat detectFormat(Reader reader, @Nullable String fileEx String headerStr = String.valueOf(buffer, 0, pos); - if ((headerStr.startsWith("p ") + if (headerStr.contains("")) { + return MappingFormat.INTELLIJ_MIGRATION_MAP_FILE; + } else if ((headerStr.startsWith("p ") || headerStr.startsWith("c ") || headerStr.startsWith("f ") || headerStr.startsWith("m ")) @@ -161,7 +164,7 @@ public static List getNamespaces(Path file, MappingFormat format) throws if (format == null) throw new IOException("invalid/unsupported mapping format"); } - if (format.hasNamespaces) { + if (format.features().hasNamespaces()) { try (Reader reader = Files.newBufferedReader(file)) { return getNamespaces(reader, format); } @@ -183,7 +186,7 @@ public static List getNamespaces(Reader reader, MappingFormat format) th if (format == null) throw new IOException("invalid/unsupported mapping format"); } - if (format.hasNamespaces) { + if (format.features().hasNamespaces()) { checkReaderCompatible(format); switch (format) { @@ -296,6 +299,9 @@ public static void read(Reader reader, MappingFormat format, MappingVisitor visi case PROGUARD_FILE: ProGuardFileReader.read(reader, visitor); break; + case INTELLIJ_MIGRATION_MAP_FILE: + MigrationMapFileReader.read(reader, visitor); + break; case RECAF_SIMPLE_FILE: RecafSimpleFileReader.read(reader, visitor); break; diff --git a/src/main/java/net/fabricmc/mappingio/MappingWriter.java b/src/main/java/net/fabricmc/mappingio/MappingWriter.java index f7a11cf9..2063c1ad 100644 --- a/src/main/java/net/fabricmc/mappingio/MappingWriter.java +++ b/src/main/java/net/fabricmc/mappingio/MappingWriter.java @@ -27,6 +27,7 @@ import net.fabricmc.mappingio.format.MappingFormat; import net.fabricmc.mappingio.format.enigma.EnigmaDirWriter; import net.fabricmc.mappingio.format.enigma.EnigmaFileWriter; +import net.fabricmc.mappingio.format.intellij.MigrationMapFileWriter; import net.fabricmc.mappingio.format.jobf.JobfFileWriter; import net.fabricmc.mappingio.format.proguard.ProGuardFileWriter; import net.fabricmc.mappingio.format.simple.RecafSimpleFileWriter; @@ -65,6 +66,7 @@ static MappingWriter create(Writer writer, MappingFormat format) throws IOExcept case TSRG_FILE: return new TsrgFileWriter(writer, false); case TSRG_2_FILE: return new TsrgFileWriter(writer, true); case PROGUARD_FILE: return new ProGuardFileWriter(writer); + case INTELLIJ_MIGRATION_MAP_FILE: return new MigrationMapFileWriter(writer); case RECAF_SIMPLE_FILE: return new RecafSimpleFileWriter(writer); case JOBF_FILE: return new JobfFileWriter(writer); default: return null; diff --git a/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java b/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java index 4d26aa0b..f6e009aa 100644 --- a/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/ColumnFileReader.java @@ -32,6 +32,11 @@ @ApiStatus.Internal public final class ColumnFileReader implements Closeable { public ColumnFileReader(Reader reader, char indentationChar, char columnSeparator) { + assert indentationChar != '\r'; + assert indentationChar != '\n'; + assert columnSeparator != '\r'; + assert columnSeparator != '\n'; + this.reader = reader; this.indentationChar = indentationChar; this.columnSeparator = columnSeparator; @@ -51,7 +56,7 @@ public void close() throws IOException { * @return {@code true} if the column was read and had the expected content, {@code false} otherwise. */ public boolean nextCol(String expected) throws IOException { - return read(false, false, true, expected) != noMatch; + return read(false, false, true, expected) != NO_MATCH; } /** @@ -89,20 +94,20 @@ public String peekCol(boolean unescape) throws IOException { * @param unescape Whether to unescape the read string. * @param consume Whether to advance the bufferPos. * @param stopAtNextCol Whether to only read one column. - * @param expected If not {@code null}, the read string must match this exactly, otherwise we early-exit with {@link #noMatch}. Always consumes if matched. + * @param expected If not {@code null}, the read string must match this exactly, otherwise we early-exit with {@link #NO_MATCH}. Always consumes if matched. * * @return {@code null} if nothing has been read (first char was EOL), otherwise the read string (may be empty). - * If {@code expected} is not {@code null}, it will be returned if matched, otherwise {@link #noMatch}. + * If {@code expected} is not {@code null}, it will be returned if matched, otherwise {@link #NO_MATCH}. */ @Nullable private String read(boolean unescape, boolean consume, boolean stopAtNextCol, @Nullable String expected) throws IOException { - if (eol) return expected == null ? null : noMatch; + if (eol) return expected == null ? null : NO_MATCH; int expectedLength = expected != null ? expected.length() : -1; // Check if the buffer needs to be filled and if we hit EOF while doing so if (expectedLength > 0 && bufferPos + expectedLength >= bufferLimit) { - if (!fillBuffer(expectedLength, !consume, false)) return noMatch; + if (!fillBuffer(expectedLength, !consume, false)) return NO_MATCH; } int start; @@ -110,31 +115,24 @@ private String read(boolean unescape, boolean consume, boolean stopAtNextCol, @N int firstEscaped = -1; int contentCharsRead = 0; int modifiedBufferPos = -1; - int startOffset = 0; boolean readAnything = false; boolean filled = true; + boolean isColumnSeparator = false; readLoop: for (;;) { while (end < bufferLimit) { char c = buffer[end]; - boolean isColumnSeparator = (c == columnSeparator); - - // skip leading column separator - if (isColumnSeparator && !readAnything) { - startOffset = 1; - contentCharsRead = -1; - } - + isColumnSeparator = (c == columnSeparator); readAnything = true; - if (expected != null && contentCharsRead > -1) { + if (expected != null) { if ((contentCharsRead < expectedLength && c != expected.charAt(contentCharsRead)) || contentCharsRead > expectedLength) { - return noMatch; + return NO_MATCH; } } - if (c == '\n' || c == '\r' || (isColumnSeparator && stopAtNextCol && contentCharsRead > -1)) { // stop reading + if (c == '\n' || c == '\r' || (isColumnSeparator && stopAtNextCol)) { // stop reading start = bufferPos; modifiedBufferPos = end; @@ -166,28 +164,36 @@ private String read(boolean unescape, boolean consume, boolean stopAtNextCol, @N } } - start += startOffset; String ret; if (expected != null) { consume = true; ret = expected; } else { - int len = end - start; + int contentLength = end - start; - if (len == 0) { + if (contentLength == 0) { ret = readAnything ? "" : null; } else if (firstEscaped >= 0) { - ret = Tiny2Util.unescape(String.valueOf(buffer, start, len)); + ret = Tiny2Util.unescape(String.valueOf(buffer, start, contentLength)); } else { - ret = String.valueOf(buffer, start, len); + ret = String.valueOf(buffer, start, contentLength); } } if (consume) { if (readAnything) bof = false; + + if (modifiedBufferPos != -1) { + bufferPos = modifiedBufferPos; + + // consume trailing column separator if present + if (isColumnSeparator && filled && fillBuffer(1, false, false)) { + bufferPos++; + } + } + if (!filled) eof = eol = true; - if (modifiedBufferPos != -1) bufferPos = modifiedBufferPos; if (eol && !eof) { // manually check for EOF int charsToRead = buffer[bufferPos] == '\r' ? 2 : 1; // 2 for \r\n, 1 for just \n @@ -237,6 +243,15 @@ public int nextIntCol() throws IOException { } } + /** + * Read and consume until the the start of the next line is reached, and return whether the + * following {@code indent} characters match {@link #indentationChar}. + * + *

Empty lines are skipped if {@code indent} is 0. + * + * @param indent The number of characters to check for indentation. + * @return {@code true} if the next line has the specified indentation or higher, {@code false} otherwise. + */ public boolean nextLine(int indent) throws IOException { fillLoop: do { while (bufferPos < bufferLimit) { @@ -246,9 +261,9 @@ public boolean nextLine(int indent) throws IOException { if (indent == 0) { // skip empty lines if indent is 0 if (!fillBuffer(2, false, true)) break fillLoop; - c = buffer[bufferPos + 1]; + char next = buffer[bufferPos + 1]; - if (c == '\n' || c == '\r') { // 2+ consecutive new lines, consume first nl and retry + if (next == '\n' || next == '\r') { // 2+ consecutive new lines, consume first nl and retry bufferPos++; lineNumber++; bof = false; @@ -428,7 +443,7 @@ private boolean fillBuffer(int count, boolean preventCompaction, boolean markEof return true; } - private static final String noMatch = new String(); + private static final String NO_MATCH = new String(); private final Reader reader; private final char indentationChar; private final char columnSeparator; diff --git a/src/main/java/net/fabricmc/mappingio/format/FeatureSet.java b/src/main/java/net/fabricmc/mappingio/format/FeatureSet.java new file mode 100644 index 00000000..7d59433e --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/FeatureSet.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 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.format; + +public interface FeatureSet { + boolean hasNamespaces(); + MetadataSupport fileMetadata(); + MetadataSupport elementMetadata(); + NameSupport packages(); + NameSupport classes(); + MemberSupport fields(); + MemberSupport methods(); + LocalSupport args(); + LocalSupport vars(); + ElementCommentSupport elementComments(); + boolean hasFileComments(); + + default boolean supportsPackages() { + return packages().srcNames() != FeaturePresence.ABSENT + || packages().dstNames() != FeaturePresence.ABSENT; + } + + default boolean supportsClasses() { + return classes().srcNames() != FeaturePresence.ABSENT + || classes().dstNames() != FeaturePresence.ABSENT; + } + + default boolean supportsFields() { + return FeatureSetHelper.isSupported(fields()); + } + + default boolean supportsMethods() { + return FeatureSetHelper.isSupported(methods()); + } + + default boolean supportsArgs() { + return FeatureSetHelper.isSupported(args()); + } + + default boolean supportsVars() { + return FeatureSetHelper.isSupported(vars()); + } + + enum MetadataSupport { + /** No metadata at all. */ + NONE, + + /** Only some select properties. */ + FIXED, + + /** Arbitrary metadata may be attached. */ + ARBITRARY + } + + enum FeaturePresence { + REQUIRED, + OPTIONAL, + ABSENT + } + + interface NameSupport { + FeaturePresence srcNames(); + FeaturePresence dstNames(); + } + + interface DescSupport { + FeaturePresence srcDescs(); + FeaturePresence dstDescs(); + } + + interface MemberSupport extends NameSupport, DescSupport { + } + + interface LocalSupport extends NameSupport, DescSupport { + FeaturePresence positions(); + FeaturePresence lvIndices(); + FeaturePresence lvtRowIndices(); + FeaturePresence startOpIndices(); + FeaturePresence endOpIndices(); + } + + enum ElementCommentSupport { + NAMESPACED, + SHARED, + NONE + } +} diff --git a/src/main/java/net/fabricmc/mappingio/format/FeatureSetBuilder.java b/src/main/java/net/fabricmc/mappingio/format/FeatureSetBuilder.java new file mode 100644 index 00000000..c8975b46 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/FeatureSetBuilder.java @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2024 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.format; + +import java.util.function.Consumer; + +import org.jetbrains.annotations.ApiStatus; + +import net.fabricmc.mappingio.format.FeatureSet.DescSupport; +import net.fabricmc.mappingio.format.FeatureSet.ElementCommentSupport; +import net.fabricmc.mappingio.format.FeatureSet.FeaturePresence; +import net.fabricmc.mappingio.format.FeatureSet.LocalSupport; +import net.fabricmc.mappingio.format.FeatureSet.MemberSupport; +import net.fabricmc.mappingio.format.FeatureSet.MetadataSupport; +import net.fabricmc.mappingio.format.FeatureSet.NameSupport; +import net.fabricmc.mappingio.format.FeatureSetImpl.DescSupportImpl; +import net.fabricmc.mappingio.format.FeatureSetImpl.LocalSupportImpl; +import net.fabricmc.mappingio.format.FeatureSetImpl.MemberSupportImpl; +import net.fabricmc.mappingio.format.FeatureSetImpl.NameSupportImpl; + +@ApiStatus.Experimental +public class FeatureSetBuilder { + public static FeatureSetBuilder create() { + return new FeatureSetBuilder(false); + } + + public static FeatureSetBuilder createFrom(FeatureSet featureSet) { + return new FeatureSetBuilder( + featureSet.hasNamespaces(), + featureSet.fileMetadata(), + featureSet.elementMetadata(), + new NameFeatureBuilder(featureSet.packages()), + new NameFeatureBuilder(featureSet.classes()), + new MemberSupportBuilder(featureSet.fields()), + new MemberSupportBuilder(featureSet.methods()), + new LocalSupportBuilder(featureSet.args()), + new LocalSupportBuilder(featureSet.vars()), + featureSet.elementComments(), + featureSet.hasFileComments()); + } + + FeatureSetBuilder(boolean initWithFullSupport) { + this(initWithFullSupport, + initWithFullSupport ? MetadataSupport.ARBITRARY : MetadataSupport.NONE, + initWithFullSupport ? MetadataSupport.ARBITRARY : MetadataSupport.NONE, + new NameFeatureBuilder(initWithFullSupport), + new NameFeatureBuilder(initWithFullSupport), + new MemberSupportBuilder(initWithFullSupport), + new MemberSupportBuilder(initWithFullSupport), + new LocalSupportBuilder(initWithFullSupport), + new LocalSupportBuilder(initWithFullSupport), + initWithFullSupport ? ElementCommentSupport.NAMESPACED : ElementCommentSupport.NONE, + initWithFullSupport); + } + + FeatureSetBuilder(boolean hasNamespaces, MetadataSupport fileMetadata, MetadataSupport elementMetadata, NameFeatureBuilder packages, NameFeatureBuilder classes, MemberSupportBuilder fields, MemberSupportBuilder methods, LocalSupportBuilder args, LocalSupportBuilder vars, ElementCommentSupport elementComments, boolean hasFileComments) { + this.hasNamespaces = hasNamespaces; + this.fileMetadata = fileMetadata; + this.elementMetadata = elementMetadata; + this.packages = packages; + this.classes = classes; + this.fields = fields; + this.methods = methods; + this.args = args; + this.vars = vars; + this.elementComments = elementComments; + this.hasFileComments = hasFileComments; + } + + public FeatureSetBuilder withNamespaces(boolean value) { + this.hasNamespaces = value; + return this; + } + + public FeatureSetBuilder withFileMetadata(MetadataSupport featurePresence) { + this.fileMetadata = featurePresence; + return this; + } + + public FeatureSetBuilder withElementMetadata(MetadataSupport featurePresence) { + this.elementMetadata = featurePresence; + return this; + } + + public FeatureSetBuilder withPackages(Consumer featureApplier) { + featureApplier.accept(packages); + return this; + } + + public FeatureSetBuilder withClasses(Consumer featureApplier) { + featureApplier.accept(classes); + return this; + } + + public FeatureSetBuilder withFields(Consumer featureApplier) { + featureApplier.accept(fields); + return this; + } + + public FeatureSetBuilder withMethods(Consumer featureApplier) { + featureApplier.accept(methods); + return this; + } + + public FeatureSetBuilder withArgs(Consumer featureApplier) { + featureApplier.accept(args); + return this; + } + + public FeatureSetBuilder withVars(Consumer featureApplier) { + featureApplier.accept(vars); + return this; + } + + public FeatureSetBuilder withElementComments(ElementCommentSupport featurePresence) { + this.elementComments = featurePresence; + return this; + } + + public FeatureSetBuilder withFileComments(boolean value) { + this.hasFileComments = value; + return this; + } + + public FeatureSet build() { + return new FeatureSetImpl( + hasNamespaces, + fileMetadata, + elementMetadata, + packages.build(), + classes.build(), + fields.build(), + methods.build(), + args.build(), + vars.build(), + elementComments, + hasFileComments); + } + + private boolean hasNamespaces; + private MetadataSupport fileMetadata; + private MetadataSupport elementMetadata; + private NameFeatureBuilder packages; + private NameFeatureBuilder classes; + private MemberSupportBuilder fields; + private MemberSupportBuilder methods; + private LocalSupportBuilder args; + private LocalSupportBuilder vars; + private ElementCommentSupport elementComments; + private boolean hasFileComments; + + public static class MemberSupportBuilder { + MemberSupportBuilder() { + this(false); + } + + MemberSupportBuilder(boolean initWithFullSupport) { + this(new NameFeatureBuilder(initWithFullSupport), new DescFeatureBuilder(initWithFullSupport)); + } + + MemberSupportBuilder(MemberSupport memberSupport) { + this(new NameFeatureBuilder(memberSupport), new DescFeatureBuilder(memberSupport)); + } + + private MemberSupportBuilder(NameFeatureBuilder names, DescFeatureBuilder descriptors) { + this.names = names; + this.descriptors = descriptors; + } + + public MemberSupportBuilder withSrcNames(FeaturePresence featurePresence) { + names.withSrcNames(featurePresence); + return this; + } + + public MemberSupportBuilder withDstNames(FeaturePresence featurePresence) { + names.withDstNames(featurePresence); + return this; + } + + public MemberSupportBuilder withSrcDescs(FeaturePresence featurePresence) { + descriptors.withSrcDescs(featurePresence); + return this; + } + + public MemberSupportBuilder withDstDescs(FeaturePresence featurePresence) { + descriptors.withDstDescs(featurePresence); + return this; + } + + public MemberSupport build() { + return new MemberSupportImpl(names.build(), descriptors.build()); + } + + private NameFeatureBuilder names; + private DescFeatureBuilder descriptors; + } + + public static class LocalSupportBuilder { + LocalSupportBuilder() { + this(false); + } + + LocalSupportBuilder(boolean initWithFullSupport) { + this(initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT, + initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT, + initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT, + initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT, + initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT, + new NameFeatureBuilder(), + new DescFeatureBuilder()); + } + + LocalSupportBuilder(LocalSupport localSupport) { + this(localSupport.positions(), + localSupport.lvIndices(), + localSupport.lvtRowIndices(), + localSupport.startOpIndices(), + localSupport.endOpIndices(), + new NameFeatureBuilder(localSupport), + new DescFeatureBuilder(localSupport)); + } + + private LocalSupportBuilder(FeaturePresence positions, FeaturePresence lvIndices, FeaturePresence lvtRowIndices, FeaturePresence startOpIndices, FeaturePresence endOpIndices, NameFeatureBuilder names, DescFeatureBuilder descriptors) { + this.positions = positions; + this.lvIndices = lvIndices; + this.lvtRowIndices = lvtRowIndices; + this.startOpIndices = startOpIndices; + this.endOpIndices = endOpIndices; + this.names = names; + this.descriptors = descriptors; + } + + public LocalSupportBuilder withPositions(FeaturePresence featurePresence) { + this.positions = featurePresence; + return this; + } + + public LocalSupportBuilder withLvIndices(FeaturePresence featurePresence) { + this.lvIndices = featurePresence; + return this; + } + + public LocalSupportBuilder withLvtRowIndices(FeaturePresence featurePresence) { + this.lvtRowIndices = featurePresence; + return this; + } + + public LocalSupportBuilder withStartOpIndices(FeaturePresence featurePresence) { + this.startOpIndices = featurePresence; + return this; + } + + public LocalSupportBuilder withEndOpIndices(FeaturePresence featurePresence) { + this.endOpIndices = featurePresence; + return this; + } + + public LocalSupportBuilder withSrcNames(FeaturePresence featurePresence) { + names.withSrcNames(featurePresence); + return this; + } + + public LocalSupportBuilder withDstNames(FeaturePresence featurePresence) { + names.withDstNames(featurePresence); + return this; + } + + public LocalSupportBuilder withSrcDescs(FeaturePresence featurePresence) { + descriptors.withSrcDescs(featurePresence); + return this; + } + + public LocalSupportBuilder withDstDescs(FeaturePresence featurePresence) { + descriptors.withDstDescs(featurePresence); + return this; + } + + public LocalSupport build() { + return new LocalSupportImpl( + positions, + lvIndices, + lvtRowIndices, + startOpIndices, + endOpIndices, + names.build(), + descriptors.build()); + } + + private FeaturePresence positions; + private FeaturePresence lvIndices; + private FeaturePresence lvtRowIndices; + private FeaturePresence startOpIndices; + private FeaturePresence endOpIndices; + private NameFeatureBuilder names; + private DescFeatureBuilder descriptors; + } + + public static class NameFeatureBuilder { + NameFeatureBuilder() { + this(false); + } + + NameFeatureBuilder(boolean initWithFullSupport) { + this(initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT, + initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT); + } + + private NameFeatureBuilder(NameSupport nameFeature) { + this(nameFeature.srcNames(), nameFeature.dstNames()); + } + + private NameFeatureBuilder(FeaturePresence srcNames, FeaturePresence dstNames) { + this.srcNames = srcNames; + this.dstNames = dstNames; + } + + public NameFeatureBuilder withSrcNames(FeaturePresence featurePresence) { + this.srcNames = featurePresence; + return this; + } + + public NameFeatureBuilder withDstNames(FeaturePresence featurePresence) { + this.dstNames = featurePresence; + return this; + } + + public NameSupport build() { + return new NameSupportImpl(srcNames, dstNames); + } + + private FeaturePresence srcNames; + private FeaturePresence dstNames; + } + + public static class DescFeatureBuilder { + DescFeatureBuilder() { + this(false); + } + + DescFeatureBuilder(boolean initWithFullSupport) { + this(initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT, + initWithFullSupport ? FeaturePresence.OPTIONAL : FeaturePresence.ABSENT); + } + + private DescFeatureBuilder(DescSupport descFeature) { + this(descFeature.srcDescs(), descFeature.dstDescs()); + } + + private DescFeatureBuilder(FeaturePresence srcDescriptors, FeaturePresence dstDescriptors) { + this.srcDescriptors = srcDescriptors; + this.dstDescriptors = dstDescriptors; + } + + public DescFeatureBuilder withSrcDescs(FeaturePresence featurePresence) { + this.srcDescriptors = featurePresence; + return this; + } + + public DescFeatureBuilder withDstDescs(FeaturePresence featurePresence) { + this.dstDescriptors = featurePresence; + return this; + } + + public DescSupport build() { + return new DescSupportImpl(srcDescriptors, dstDescriptors); + } + + private FeaturePresence srcDescriptors; + private FeaturePresence dstDescriptors; + } +} diff --git a/src/main/java/net/fabricmc/mappingio/format/FeatureSetHelper.java b/src/main/java/net/fabricmc/mappingio/format/FeatureSetHelper.java new file mode 100644 index 00000000..27855f23 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/FeatureSetHelper.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 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.format; + +import net.fabricmc.mappingio.format.FeatureSet.FeaturePresence; +import net.fabricmc.mappingio.format.FeatureSet.LocalSupport; +import net.fabricmc.mappingio.format.FeatureSet.MemberSupport; + +// Only exists since Java 8 doesn't support private interface methods +final class FeatureSetHelper { + private FeatureSetHelper() { + } + + static boolean isSupported(MemberSupport members) { + return members.srcNames() != FeaturePresence.ABSENT + || members.dstNames() != FeaturePresence.ABSENT + || members.srcDescs() != FeaturePresence.ABSENT + || members.dstDescs() != FeaturePresence.ABSENT; + } + + static boolean isSupported(LocalSupport locals) { + return locals.positions() != FeaturePresence.ABSENT + || locals.lvIndices() != FeaturePresence.ABSENT + || locals.lvtRowIndices() != FeaturePresence.ABSENT + || locals.startOpIndices() != FeaturePresence.ABSENT + || locals.endOpIndices() != FeaturePresence.ABSENT + || locals.srcNames() != FeaturePresence.ABSENT + || locals.dstNames() != FeaturePresence.ABSENT + || locals.srcDescs() != FeaturePresence.ABSENT + || locals.dstDescs() != FeaturePresence.ABSENT; + } +} diff --git a/src/main/java/net/fabricmc/mappingio/format/FeatureSetImpl.java b/src/main/java/net/fabricmc/mappingio/format/FeatureSetImpl.java new file mode 100644 index 00000000..778d66f1 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/FeatureSetImpl.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2024 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.format; + +class FeatureSetImpl implements FeatureSet { + FeatureSetImpl(boolean hasNamespaces, MetadataSupport fileMetadata, MetadataSupport elementMetadata, NameSupport packages, NameSupport classes, MemberSupport fields, MemberSupport methods, LocalSupport args, LocalSupport vars, ElementCommentSupport elementComments, boolean hasFileComments) { + this.hasNamespaces = hasNamespaces; + this.fileMetadata = fileMetadata; + this.elementMetadata = elementMetadata; + this.packages = packages; + this.classes = classes; + this.fields = fields; + this.methods = methods; + this.args = args; + this.vars = vars; + this.elementComments = elementComments; + this.hasFileComments = hasFileComments; + } + + @Override + public boolean hasNamespaces() { + return hasNamespaces; + } + + @Override + public MetadataSupport fileMetadata() { + return fileMetadata; + } + + @Override + public MetadataSupport elementMetadata() { + return elementMetadata; + } + + @Override + public NameSupport packages() { + return packages; + } + + @Override + public NameSupport classes() { + return classes; + } + + @Override + public MemberSupport fields() { + return fields; + } + + @Override + public MemberSupport methods() { + return methods; + } + + @Override + public LocalSupport args() { + return args; + } + + @Override + public LocalSupport vars() { + return vars; + } + + @Override + public ElementCommentSupport elementComments() { + return elementComments; + } + + @Override + public boolean hasFileComments() { + return hasFileComments; + } + + private final boolean hasNamespaces; + private final MetadataSupport fileMetadata; + private final MetadataSupport elementMetadata; + private final NameSupport packages; + private final NameSupport classes; + private final MemberSupport fields; + private final MemberSupport methods; + private final LocalSupport args; + private final LocalSupport vars; + private final ElementCommentSupport elementComments; + private final boolean hasFileComments; + + static class MemberSupportImpl implements MemberSupport { + MemberSupportImpl(NameSupport names, DescSupport descriptors) { + this.names = names; + this.descriptors = descriptors; + } + + @Override + public FeaturePresence srcNames() { + return names.srcNames(); + } + + @Override + public FeaturePresence dstNames() { + return names.dstNames(); + } + + @Override + public FeaturePresence srcDescs() { + return descriptors.srcDescs(); + } + + @Override + public FeaturePresence dstDescs() { + return descriptors.dstDescs(); + } + + private final NameSupport names; + private final DescSupport descriptors; + } + + static class LocalSupportImpl implements LocalSupport { + LocalSupportImpl(FeaturePresence positions, FeaturePresence lvIndices, FeaturePresence lvtRowIndices, FeaturePresence startOpIndices, FeaturePresence endOpIndices, NameSupport names, DescSupport descriptors) { + this.positions = positions; + this.lvIndices = lvIndices; + this.lvtRowIndices = lvtRowIndices; + this.startOpIndices = startOpIndices; + this.endOpIndices = endOpIndices; + this.names = names; + this.descriptors = descriptors; + } + + @Override + public FeaturePresence positions() { + return positions; + } + + @Override + public FeaturePresence lvIndices() { + return lvIndices; + } + + @Override + public FeaturePresence lvtRowIndices() { + return lvtRowIndices; + } + + @Override + public FeaturePresence startOpIndices() { + return startOpIndices; + } + + @Override + public FeaturePresence endOpIndices() { + return endOpIndices; + } + + @Override + public FeaturePresence srcNames() { + return names.srcNames(); + } + + @Override + public FeaturePresence dstNames() { + return names.dstNames(); + } + + @Override + public FeaturePresence srcDescs() { + return descriptors.srcDescs(); + } + + @Override + public FeaturePresence dstDescs() { + return descriptors.dstDescs(); + } + + private final FeaturePresence positions; + private final FeaturePresence lvIndices; + private final FeaturePresence lvtRowIndices; + private final FeaturePresence startOpIndices; + private final FeaturePresence endOpIndices; + private final NameSupport names; + private final DescSupport descriptors; + } + + static class NameSupportImpl implements NameSupport { + NameSupportImpl(FeaturePresence srcNames, FeaturePresence dstNames) { + this.srcNames = srcNames; + this.dstNames = dstNames; + } + + @Override + public FeaturePresence srcNames() { + return srcNames; + } + + @Override + public FeaturePresence dstNames() { + return dstNames; + } + + private final FeaturePresence srcNames; + private final FeaturePresence dstNames; + } + + static class DescSupportImpl implements DescSupport { + DescSupportImpl(FeaturePresence srcDescriptors, FeaturePresence dstDescriptors) { + this.srcDescriptors = srcDescriptors; + this.dstDescriptors = dstDescriptors; + } + + @Override + public FeaturePresence srcDescs() { + return srcDescriptors; + } + + @Override + public FeaturePresence dstDescs() { + return dstDescriptors; + } + + private final FeaturePresence srcDescriptors; + private final FeaturePresence dstDescriptors; + } +} diff --git a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java index 24c2ab51..02e76411 100644 --- a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java +++ b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java @@ -18,6 +18,10 @@ import org.jetbrains.annotations.Nullable; +import net.fabricmc.mappingio.format.FeatureSet.ElementCommentSupport; +import net.fabricmc.mappingio.format.FeatureSet.FeaturePresence; +import net.fabricmc.mappingio.format.FeatureSet.MetadataSupport; + /** * Represents a supported mapping format. Every format can be assumed to have an associated reader available. * @@ -31,12 +35,51 @@ public enum MappingFormat { *

Implementation notes

* File metadata only has limited support as of now, and is hardcoded to intermediary counters. */ - TINY_FILE("Tiny file", "tiny", true, true, false, false, false, true), + TINY_FILE("Tiny file", "tiny", true, FeatureSetBuilder.create() + .withNamespaces(true) + .withFileMetadata(MetadataSupport.FIXED) // TODO: change this to ARBITRARY once https://github.com/FabricMC/mapping-io/pull/29 is merged + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL)) + .withFields(f -> f + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withFileComments(true)), /** * The {@code Tiny v2} mapping format, as specified here. */ - TINY_2_FILE("Tiny v2 file", "tiny", true, true, true, true, true, true), + TINY_2_FILE("Tiny v2 file", "tiny", true, FeatureSetBuilder.create() + .withNamespaces(true) + .withFileMetadata(MetadataSupport.ARBITRARY) + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL)) + .withFields(f -> f + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withArgs(a -> a + .withLvIndices(FeaturePresence.REQUIRED) + .withSrcNames(FeaturePresence.OPTIONAL) + .withDstNames(FeaturePresence.OPTIONAL)) + .withVars(v -> v + .withLvIndices(FeaturePresence.REQUIRED) + .withLvtRowIndices(FeaturePresence.OPTIONAL) + .withStartOpIndices(FeaturePresence.REQUIRED) + .withSrcNames(FeaturePresence.OPTIONAL) + .withDstNames(FeaturePresence.OPTIONAL)) + .withElementComments(ElementCommentSupport.SHARED) + .withFileComments(true)), // only in reserved places /** * Enigma's mapping format, as specified here. @@ -44,7 +87,24 @@ public enum MappingFormat { *

Implementation notes

* Access modifiers are currently not supported. */ - ENIGMA_FILE("Enigma file", "mapping", false, true, true, true, false, true), + ENIGMA_FILE("Enigma file", "mapping", true, FeatureSetBuilder.create() + .withElementMetadata(MetadataSupport.FIXED) // access modifiers + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL)) + .withFields(f -> f + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withArgs(a -> a + .withLvIndices(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.OPTIONAL)) + .withElementComments(ElementCommentSupport.SHARED) + .withFileComments(true)), /** * Enigma's mapping format (in directory form), as specified here. @@ -52,7 +112,7 @@ public enum MappingFormat { *

Implementation notes

* Access modifiers are currently not supported. */ - ENIGMA_DIR("Enigma directory", null, false, true, true, true, false, true), + ENIGMA_DIR("Enigma directory", null, true, FeatureSetBuilder.createFrom(ENIGMA_FILE.features)), /** * The {@code SRG} ("Searge RetroGuard") mapping format, as specified here. @@ -60,21 +120,51 @@ public enum MappingFormat { *

Implementation notes

* Package mappings are currently not supported. */ - SRG_FILE("SRG file", "srg", false, false, false, false, false, true), + SRG_FILE("SRG file", "srg", true, FeatureSetBuilder.create() + .withPackages(p -> p + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withFields(f -> f + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.REQUIRED) + .withDstDescs(FeaturePresence.REQUIRED)) + .withFileComments(true)), /** * The {@code XSRG} ("Extended SRG") mapping format, as specified here. - * Same as SRG, but with field descriptors. + * + *

Same as SRG, but with field descriptors. * *

Implementation notes

* Package mappings are currently not supported. */ - XSRG_FILE("XSRG file", "xsrg", false, true, false, false, false, true), + XSRG_FILE("XSRG file", "xsrg", true, FeatureSetBuilder.createFrom(SRG_FILE.features) + .withFields(f -> f + .withSrcDescs(FeaturePresence.REQUIRED) + .withDstDescs(FeaturePresence.REQUIRED))), /** * The {@code JAM} ("Java Associated Mapping"; formerly {@code SRGX}) mapping format, as specified here. */ - JAM_FILE("JAM file", "jam", false, true, false, true, false, true), + JAM_FILE("JAM file", "jam", true, FeatureSetBuilder.createFrom(SRG_FILE.features) + .withPackages(p -> p + .withSrcNames(FeaturePresence.ABSENT) + .withDstNames(FeaturePresence.ABSENT)) + .withFields(f -> f + .withSrcDescs(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withDstDescs(FeaturePresence.ABSENT)) + .withArgs(a -> a + .withPositions(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.OPTIONAL) + .withDstNames(FeaturePresence.REQUIRED))), /** * The {@code CSRG} ("Compact SRG", since it saves disk space over SRG) mapping format, as specified here. @@ -82,16 +172,19 @@ public enum MappingFormat { *

Implementation notes

* Package mappings are currently not supported. */ - CSRG_FILE("CSRG file", "csrg", false, false, false, false, false, true), + CSRG_FILE("CSRG file", "csrg", true, FeatureSetBuilder.createFrom(SRG_FILE.features) + .withMethods(m -> m + .withDstDescs(FeaturePresence.ABSENT))), /** * The {@code TSRG} ("Tiny SRG", since it saves disk space over SRG) mapping format, as specified here. - * Same as CSRG, but hierarchical instead of flat. + * + *

Same as CSRG, but hierarchical instead of flat. * *

Implementation notes

* Package mappings are currently not supported. */ - TSRG_FILE("TSRG file", "tsrg", false, false, false, false, false, true), + TSRG_FILE("TSRG file", "tsrg", true, FeatureSetBuilder.createFrom(CSRG_FILE.features)), /** * The {@code TSRG v2} mapping format, as specified here. @@ -99,7 +192,15 @@ public enum MappingFormat { *

Implementation notes

* Package mappings and static markers for methods are currently not supported. */ - TSRG_2_FILE("TSRG v2 file", "tsrg", true, true, false, true, false, true), + TSRG_2_FILE("TSRG v2 file", "tsrg", true, FeatureSetBuilder.createFrom(TSRG_FILE.features) + .withNamespaces(true) + .withElementMetadata(MetadataSupport.FIXED) // static info for methods + .withFields(f -> f + .withSrcDescs(FeaturePresence.OPTIONAL)) + .withArgs(a -> a + .withLvIndices(FeaturePresence.REQUIRED) + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED))), /** * ProGuard's mapping format, as specified here. @@ -107,12 +208,50 @@ public enum MappingFormat { *

Implementation notes

* Line numbers are currently not supported. */ - PROGUARD_FILE("ProGuard file", "txt", false, true, false, false, false, true), + PROGUARD_FILE("ProGuard file", "txt", true, FeatureSetBuilder.create() + .withElementMetadata(MetadataSupport.FIXED) // line numbers + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withFields(f -> f + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withFileComments(true)), + + /** + * The IntelliJ IDEA migration map format, as implemented here. + */ + INTELLIJ_MIGRATION_MAP_FILE("IntelliJ migration map file", "xml", true, FeatureSetBuilder.create() + .withFileMetadata(MetadataSupport.FIXED) // migration map name and description + .withPackages(p -> p + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withFileComments(true)), /** * Recaf's {@code Simple} mapping format, as specified here. */ - RECAF_SIMPLE_FILE("Recaf Simple file", "txt", false, true, false, false, false, true), + RECAF_SIMPLE_FILE("Recaf Simple file", "txt", true, FeatureSetBuilder.create() + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withFields(f -> f + .withSrcNames(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.OPTIONAL) + .withDstNames(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withFileComments(true)), /** * The {@code JOBF} mapping format, as specified here. @@ -120,20 +259,37 @@ public enum MappingFormat { *

Implementation notes

* Package mappings are currently not supported. */ - JOBF_FILE("JOBF file", "jobf", false, true, false, false, false, true); + JOBF_FILE("JOBF file", "jobf", true, FeatureSetBuilder.create() + .withPackages(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withClasses(c -> c + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED)) + .withFields(f -> f + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withMethods(m -> m + .withSrcNames(FeaturePresence.REQUIRED) + .withDstNames(FeaturePresence.REQUIRED) + .withSrcDescs(FeaturePresence.REQUIRED)) + .withFileComments(true)); - MappingFormat(String name, @Nullable String fileExt, - boolean hasNamespaces, boolean hasFieldDescriptors, - boolean supportsComments, boolean supportsArgs, boolean supportsLocals, - boolean hasWriter) { + MappingFormat(String name, @Nullable String fileExt, boolean hasWriter, FeatureSetBuilder featureBuilder) { this.name = name; this.fileExt = fileExt; - this.hasNamespaces = hasNamespaces; - this.hasFieldDescriptors = hasFieldDescriptors; - this.supportsComments = supportsComments; - this.supportsArgs = supportsArgs; - this.supportsLocals = supportsLocals; this.hasWriter = hasWriter; + this.features = featureBuilder.build(); + this.hasNamespaces = features.hasNamespaces(); + this.hasFieldDescriptors = features.fields().srcDescs() != FeaturePresence.ABSENT || features.fields().dstDescs() != FeaturePresence.ABSENT; + this.supportsComments = features.elementComments() != ElementCommentSupport.NONE; + this.supportsArgs = features.supportsArgs(); + this.supportsLocals = features.supportsVars(); + } + + public FeatureSet features() { + return features; } public boolean hasSingleFile() { @@ -146,13 +302,39 @@ public String getGlobPattern() { return "*."+fileExt; } + private final FeatureSet features; public final String name; + public final boolean hasWriter; @Nullable public final String fileExt; + + /** + * @deprecated Use {@link #features()} instead. + */ + @Deprecated public final boolean hasNamespaces; + + /** + * @deprecated Use {@link #features()} instead. + */ + @Deprecated public final boolean hasFieldDescriptors; + + /** + * @deprecated Use {@link #features()} instead. + */ + @Deprecated public final boolean supportsComments; + + /** + * @deprecated Use {@link #features()} instead. + */ + @Deprecated public final boolean supportsArgs; + + /** + * @deprecated Use {@link #features()} instead. + */ + @Deprecated public final boolean supportsLocals; - public final boolean hasWriter; } diff --git a/src/main/java/net/fabricmc/mappingio/format/intellij/MigrationMapFileReader.java b/src/main/java/net/fabricmc/mappingio/format/intellij/MigrationMapFileReader.java new file mode 100644 index 00000000..4968e945 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/intellij/MigrationMapFileReader.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2024 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.format.intellij; + +import java.io.BufferedReader; +import java.io.CharArrayReader; +import java.io.IOException; +import java.io.Reader; +import java.util.Arrays; +import java.util.Collections; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingFlag; +import net.fabricmc.mappingio.MappingUtil; +import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.format.MappingFormat; + +/** + * {@linkplain MappingFormat#INTELLIJ_MIGRATION_MAP_FILE IntelliJ migration map} reader. + * + *

Crashes if a second visit pass is requested without + * {@link MappingFlag#NEEDS_MULTIPLE_PASSES} having been passed beforehand. + */ +public final class MigrationMapFileReader { + private MigrationMapFileReader() { + } + + public static void read(Reader reader, MappingVisitor visitor) throws IOException { + read(reader, MappingUtil.NS_SOURCE_FALLBACK, MappingUtil.NS_TARGET_FALLBACK, visitor); + } + + public static void read(Reader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { + BufferedReader br = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader); + + read(br, sourceNs, targetNs, visitor); + } + + private static void read(BufferedReader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException { + try { + read0(reader, sourceNs, targetNs, visitor); + } catch (XMLStreamException e) { + throw new IOException(e); + } + } + + private static void read0(BufferedReader reader, String sourceNs, String targetNs, MappingVisitor visitor) throws IOException, XMLStreamException { + CharArrayReader parentReader = null; + + if (visitor.getFlags().contains(MappingFlag.NEEDS_MULTIPLE_PASSES)) { + char[] buffer = new char[100_000]; + int pos = 0; + int len; + + while ((len = reader.read(buffer, pos, buffer.length - pos)) >= 0) { + pos += len; + + if (pos == buffer.length) buffer = Arrays.copyOf(buffer, buffer.length * 2); + } + + parentReader = new CharArrayReader(buffer, 0, pos); + reader = new CustomBufferedReader(parentReader); + } + + XMLInputFactory factory = XMLInputFactory.newInstance(); + + for (;;) { + XMLStreamReader xmlReader = factory.createXMLStreamReader(reader); + boolean visitHeader; + + if (visitHeader = visitor.visitHeader()) { + visitor.visitNamespaces(sourceNs, Collections.singletonList(targetNs)); + } + + if (visitor.visitContent()) { + int depth = 0; + + while (xmlReader.hasNext()) { + int event = xmlReader.next(); + + switch (event) { + case XMLStreamConstants.START_ELEMENT: + String name = xmlReader.getLocalName(); + + if (depth != (name.equals("migrationMap") ? 0 : 1)) { + throw new IOException("unexpected depth at line "+xmlReader.getLocation().getLineNumber()); + } + + depth++; + + switch (name) { + case "name": + case "description": + if (visitHeader) { + // TODO: visit as metadata once https://github.com/FabricMC/mapping-io/pull/29 is merged + } + + break; + case "entry": + String type = xmlReader.getAttributeValue(null, "type"); + + if (type == null || type.isEmpty()) throw new IOException("missing/empty type attribute at line "+xmlReader.getLocation().getLineNumber()); + if (type.equals("package")) continue; // TODO: support packages + if (!type.equals("class")) throw new IOException("unexpected entry type "+type+" at line "+xmlReader.getLocation().getLineNumber()); + + String srcName = xmlReader.getAttributeValue(null, "oldName"); + String dstName = xmlReader.getAttributeValue(null, "newName"); + // String recursive = xmlReader.getAttributeValue(null, "recursive"); // only used for packages + + if (srcName == null || srcName.isEmpty()) throw new IOException("missing/empty oldName attribute at line "+xmlReader.getLocation().getLineNumber()); + if (dstName == null || dstName.isEmpty()) throw new IOException("missing/empty newName attribute at line "+xmlReader.getLocation().getLineNumber()); + + srcName = srcName.replace('.', '/'); + dstName = dstName.replace('.', '/'); + + if (visitor.visitClass(srcName)) { + visitor.visitDstName(MappedElementKind.CLASS, 0, dstName); + visitor.visitElementContent(MappedElementKind.CLASS); + } + + break; + } + + break; + case XMLStreamConstants.END_ELEMENT: + depth--; + break; + } + } + } + + if (visitor.visitEnd()) { + if (parentReader != null) { + ((CustomBufferedReader) reader).forceClose(); + } + + break; + } + + if (parentReader == null) { + throw new IllegalStateException("repeated visitation requested without NEEDS_MULTIPLE_PASSES"); + } else { + parentReader.reset(); + reader = new CustomBufferedReader(parentReader); + } + } + } + + private static class CustomBufferedReader extends BufferedReader { + private CustomBufferedReader(Reader in) { + super(in); + } + + public void forceClose() throws IOException { + super.close(); + } + + @Override + public void close() throws IOException { + } + } +} diff --git a/src/main/java/net/fabricmc/mappingio/format/intellij/MigrationMapFileWriter.java b/src/main/java/net/fabricmc/mappingio/format/intellij/MigrationMapFileWriter.java new file mode 100644 index 00000000..98d91c9d --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/format/intellij/MigrationMapFileWriter.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 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.format.intellij; + +import java.io.IOException; +import java.io.Writer; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingFlag; +import net.fabricmc.mappingio.MappingWriter; +import net.fabricmc.mappingio.format.MappingFormat; + +/** + * {@linkplain MappingFormat#INTELLIJ_MIGRATION_MAP_FILE IntelliJ migration map} writer. + */ +public final class MigrationMapFileWriter implements MappingWriter { + public MigrationMapFileWriter(Writer writer) { + this.writer = writer; + } + + @Override + public void close() throws IOException { + try { + if (xmlWriter != null) { + xmlWriter.writeEndDocument(); + xmlWriter.close(); + } + } catch (XMLStreamException e) { + throw new IOException(e); + } finally { + writer.close(); + } + } + + @Override + public Set getFlags() { + return flags; + } + + @Override + public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException { + } + + @Override + public void visitMetadata(String key, @Nullable String value) throws IOException { + // TODO: Support once https://github.com/FabricMC/mapping-io/pull/29 is merged + } + + @Override + public boolean visitClass(String srcName) throws IOException { + this.srcName = srcName; + this.dstName = null; + + return true; + } + + @Override + public boolean visitField(String srcName, @Nullable String srcDesc) throws IOException { + return false; + } + + @Override + public boolean visitMethod(String srcName, @Nullable String srcDesc) throws IOException { + return false; + } + + @Override + public boolean visitMethodArg(int argPosition, int lvIndex, @Nullable String srcName) throws IOException { + return false; + } + + @Override + public boolean visitMethodVar(int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, @Nullable String srcName) throws IOException { + return false; + } + + @Override + public void visitDstName(MappedElementKind targetKind, int namespace, String name) { + if (namespace != 0) return; + + dstName = name; + } + + @Override + public boolean visitElementContent(MappedElementKind targetKind) throws IOException { + if (dstName == null) return false; + + try { + if (xmlWriter == null) { + xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(writer); + + xmlWriter.writeStartDocument("UTF-8", "1.0"); + xmlWriter.writeStartElement("migrationMap"); + } + + xmlWriter.writeStartElement("entry"); + xmlWriter.writeAttribute("oldName", srcName.replace('/', '.')); + xmlWriter.writeAttribute("newName", dstName.replace('/', '.')); + xmlWriter.writeAttribute("type", "class"); + xmlWriter.writeEndElement(); + + return false; + } catch (XMLStreamException | FactoryConfigurationError e) { + throw new IOException(e); + } + } + + @Override + public void visitComment(MappedElementKind targetKind, String comment) throws IOException { + // not supported, skip + } + + private static final Set flags = EnumSet.of(MappingFlag.NEEDS_ELEMENT_UNIQUENESS); + + private final Writer writer; + private XMLStreamWriter xmlWriter; + private String srcName; + private String dstName; +} diff --git a/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java b/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java index 3534b584..cb02e054 100644 --- a/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/srg/TsrgFileReader.java @@ -77,16 +77,23 @@ private static void read(ColumnFileReader reader, String sourceNs, String target if (format == MappingFormat.TSRG_2_FILE) { srcNamespace = reader.nextCol(); + if (srcNamespace == null || srcNamespace.isEmpty()) throw new IOException("no source namespace in TSRG v2 header"); + dstNamespaces = new ArrayList<>(); String dstNamespace; while ((dstNamespace = reader.nextCol()) != null) { + if (dstNamespace.isEmpty()) throw new IOException("empty destination namespace in TSRG v2 header"); dstNamespaces.add(dstNamespace); } + if (dstNamespaces.isEmpty()) throw new IOException("no destination namespaces in TSRG v2 header"); reader.nextLine(0); } else { + if (sourceNs == null || sourceNs.isEmpty()) throw new IllegalArgumentException("provided source namespace must not be null or empty"); srcNamespace = sourceNs; + + if (targetNs == null || targetNs.isEmpty()) throw new IllegalArgumentException("provided target namespace must not be null or empty"); dstNamespaces = Collections.singletonList(targetNs); } diff --git a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java index e824a186..1c740dc8 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny1FileReader.java @@ -69,14 +69,19 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws } String srcNamespace = reader.nextCol(); + if (srcNamespace == null || srcNamespace.isEmpty()) throw new IOException("no source namespace in Tiny v1 header"); + List dstNamespaces = new ArrayList<>(); String dstNamespace; while ((dstNamespace = reader.nextCol()) != null) { + if (dstNamespace.isEmpty()) throw new IOException("empty destination namespace in Tiny v1 header"); dstNamespaces.add(dstNamespace); } int dstNsCount = dstNamespaces.size(); + if (dstNsCount == 0) throw new IOException("no destination namespaces in Tiny v1 header"); + Set flags = visitor.getFlags(); MappingVisitor parentVisitor = null; boolean readerMarked = false; diff --git a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java index f805d063..d6643a32 100644 --- a/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java +++ b/src/main/java/net/fabricmc/mappingio/format/tiny/Tiny2FileReader.java @@ -70,14 +70,18 @@ private static void read(ColumnFileReader reader, MappingVisitor visitor) throws } String srcNamespace = reader.nextCol(); + if (srcNamespace == null || srcNamespace.isEmpty()) throw new IOException("no source namespace in Tiny v2 header"); + List dstNamespaces = new ArrayList<>(); String dstNamespace; while ((dstNamespace = reader.nextCol()) != null) { + if (dstNamespace.isEmpty()) throw new IOException("empty destination namespace in Tiny v2 header"); dstNamespaces.add(dstNamespace); } int dstNsCount = dstNamespaces.size(); + if (dstNsCount == 0) throw new IOException("no destination namespaces in Tiny v2 header"); boolean readerMarked = false; if (visitor.getFlags().contains(MappingFlag.NEEDS_MULTIPLE_PASSES)) { diff --git a/src/test/java/net/fabricmc/mappingio/SubsetAssertingVisitor.java b/src/test/java/net/fabricmc/mappingio/SubsetAssertingVisitor.java index 074760e7..878a03e6 100644 --- a/src/test/java/net/fabricmc/mappingio/SubsetAssertingVisitor.java +++ b/src/test/java/net/fabricmc/mappingio/SubsetAssertingVisitor.java @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,6 +27,10 @@ import org.jetbrains.annotations.Nullable; +import net.fabricmc.mappingio.format.FeatureSet; +import net.fabricmc.mappingio.format.FeatureSet.ElementCommentSupport; +import net.fabricmc.mappingio.format.FeatureSet.FeaturePresence; +import net.fabricmc.mappingio.format.FeatureSetInstantiator; import net.fabricmc.mappingio.format.MappingFormat; import net.fabricmc.mappingio.tree.MappingTreeView; import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; @@ -37,25 +40,27 @@ import net.fabricmc.mappingio.tree.MappingTreeView.MethodVarMappingView; public class SubsetAssertingVisitor implements FlatMappingVisitor { + /** + * @param supTree The superset tree. + * @param supFormat The superset format, or null if supTree has all the original data. + * @param subFormat The subset format, or null if lossless (i.e. if the visits are coming from a tree). + */ public SubsetAssertingVisitor(MappingTreeView supTree, @Nullable MappingFormat supFormat, @Nullable MappingFormat subFormat) { this.supTree = supTree; this.supDstNsCount = supTree.getMaxNamespaceId(); - this.subHasNamespaces = subFormat == null ? true : subFormat.hasNamespaces; - this.supHasNamespaces = supFormat == null ? true : supFormat.hasNamespaces; - this.supHasFieldDesc = supFormat == null ? true : supFormat.hasFieldDescriptors; - this.supHasArgs = supFormat == null ? true : supFormat.supportsArgs; - this.supHasVars = supFormat == null ? true : supFormat.supportsLocals; - this.supHasComments = supFormat == null ? true : supFormat.supportsComments; + this.supFeatures = supFormat == null ? FeatureSetInstantiator.withFullSupport() : supFormat.features(); + this.subFeatures = subFormat == null ? FeatureSetInstantiator.withFullSupport() : subFormat.features(); } @Override public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException { - assertTrue(srcNamespace.equals(subHasNamespaces ? supTree.getSrcNamespace() : MappingUtil.NS_SOURCE_FALLBACK)); - this.dstNamespaces = dstNamespaces; + String expectedSrcNs = subFeatures.hasNamespaces() ? supTree.getSrcNamespace() : MappingUtil.NS_SOURCE_FALLBACK; + assertEquals(expectedSrcNs, srcNamespace, "Incoming mappings have different source namespace than supTree"); + this.subDstNamespaces = dstNamespaces; - if (!subHasNamespaces) { - assertTrue(dstNamespaces.size() == 1); - assertTrue(dstNamespaces.get(0).equals(MappingUtil.NS_TARGET_FALLBACK)); + if (!subFeatures.hasNamespaces()) { + assertEquals(1, dstNamespaces.size(), "Incoming mappings have multiple namespaces despite declaring not to support them"); + assertEquals(MappingUtil.NS_TARGET_FALLBACK, dstNamespaces.get(0), "Incoming mappings don't have default namespace name of non-namespaced formats"); return; } @@ -63,197 +68,350 @@ public void visitNamespaces(String srcNamespace, List dstNamespaces) thr String dstNs = dstNamespaces.get(i); boolean contained = supTree.getDstNamespaces().contains(dstNs); - if (!supHasNamespaces) { - if (contained) return; - } else { - assertTrue(contained); + if (!supFeatures.hasNamespaces()) { + // One of the sub namespaces must equal the sup namespace + if (contained) { + subNsIfSupNotNamespaced = i; + break; + } + + if (i < dstNamespaces.size() - 1) continue; } - } - if (!supHasNamespaces) throw new RuntimeException("SubTree namespace not contained in SupTree"); + assertTrue(contained, "Incoming namespace not contained in supTree: " + dstNs); + } } @Override - public boolean visitClass(String srcName, String[] dstNames) throws IOException { + public boolean visitClass(String srcName, @Nullable String[] dstNames) throws IOException { + if (!supFeatures.supportsClasses()) return true; // sub-elements might still be supported + ClassMappingView supCls = supTree.getClass(srcName); - Map supDstNamesByNsName = new HashMap<>(); + boolean supHasDstNames = supFeatures.classes().dstNames() != FeaturePresence.ABSENT; + boolean subHasDstNames = subFeatures.classes().dstNames() != FeaturePresence.ABSENT; - if (supCls == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return true; - throw new RuntimeException("SubTree class not contained in SupTree: " + srcName); + if (supCls == null) { // SupTree doesn't have this class, ensure the incoming mappings don't have any data for it + if (supHasDstNames && subHasDstNames) { + String[] subDstNames = supFeatures.hasNamespaces() || dstNames == null ? dstNames : new String[]{dstNames[subNsIfSupNotNamespaced]}; + assertTrue(isEmpty(subDstNames), "Incoming class not contained in SupTree: " + srcName); + } + + return true; } + Map supDstNamesByNsName = new HashMap<>(); + for (int supNs = 0; supNs < supDstNsCount; supNs++) { supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supCls.getDstName(supNs)); } - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String supDstName = supDstNamesByNsName.get(dstNamespaces.get(subNs)); - if (!supHasNamespaces && supDstName == null) continue; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + if (supHasDstNames && subHasDstNames && dstNames != null) { + for (int subNs = 0; subNs < subDstNamespaces.size(); subNs++) { + String supDstName = supDstNamesByNsName.get(subDstNamespaces.get(subNs)); + if (supDstName == null && !supFeatures.hasNamespaces()) continue; + + String subDstName = dstNames[subNs]; + if (subDstName == null && (supDstName == null || Objects.equals(supDstName, srcName))) continue; // uncompleted dst name + + assertEquals(supDstName != null ? supDstName : srcName, subDstName, "Incoming class destination name differs from supTree"); + } } return true; } @Override - public void visitClassComment(String srcName, String[] dstNames, String comment) throws IOException { - if (!supHasComments) return; - assertEquals(supTree.getClass(srcName).getComment(), comment); + public void visitClassComment(String srcName, @Nullable String[] dstNames, String comment) throws IOException { + if (!supFeatures.supportsClasses() || supFeatures.elementComments() == ElementCommentSupport.NONE) return; + + assertEquals(supTree.getClass(srcName).getComment(), comment, "Incoming class comment not contained in supTree: " + srcName); } @Override - public boolean visitField(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs) throws IOException { - FieldMappingView supFld = supTree.getClass(srcClsName).getField(srcName, srcDesc); - Map supDstDataByNsName = new HashMap<>(); + public boolean visitField(String srcClsName, String srcName, @Nullable String srcDesc, + @Nullable String[] dstClsNames, @Nullable String[] dstNames, @Nullable String[] dstDescs) throws IOException { + if (!supFeatures.supportsFields()) return true; - if (supFld == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return true; - throw new RuntimeException("SubTree field not contained in SupTree: " + srcName); + FieldMappingView supFld = supTree.getClass(srcClsName).getField(srcName, srcDesc); + boolean supHasSrcDescs = supFeatures.fields().srcDescs() != FeaturePresence.ABSENT; + boolean subHasSrcDescs = subFeatures.fields().srcDescs() != FeaturePresence.ABSENT; + boolean supHasDstNames = supFeatures.fields().dstNames() != FeaturePresence.ABSENT; + boolean subHasDstNames = subFeatures.fields().dstNames() != FeaturePresence.ABSENT; + boolean supHasDstDescs = supFeatures.fields().dstDescs() != FeaturePresence.ABSENT; + boolean subHasDstDescs = subFeatures.fields().dstDescs() != FeaturePresence.ABSENT; + + if (supFld == null) { // SupTree doesn't have this field, ensure the incoming mappings don't have any data for it + String[] subDstNames = null; + String[] subDstDescs = null; + + if (supHasDstNames && subHasDstNames) subDstNames = supFeatures.hasNamespaces() || dstNames == null ? dstNames : new String[]{dstNames[subNsIfSupNotNamespaced]}; + if (supHasDstDescs && subHasDstDescs) subDstDescs = supFeatures.hasNamespaces() || dstDescs == null ? dstDescs : new String[]{dstDescs[subNsIfSupNotNamespaced]}; + + assertTrue(isEmpty(subDstNames) && isEmpty(subDstDescs), "Incoming field not contained in SupTree: " + srcName + ", descriptor: " + srcDesc); + return true; } + Map supDstDataByNsName = new HashMap<>(); + for (int supNs = 0; supNs < supDstNsCount; supNs++) { supDstDataByNsName.put(supTree.getNamespaceName(supNs), new String[]{supFld.getDstName(supNs), supFld.getDstDesc(supNs)}); } - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String[] supDstData = supDstDataByNsName.get(dstNamespaces.get(subNs)); - if (!supHasNamespaces && supDstData == null) continue; + for (int subNs = 0; subNs < subDstNamespaces.size(); subNs++) { + if (supHasSrcDescs && subHasSrcDescs && srcDesc != null) { + assertEquals(supFld.getSrcDesc(), srcDesc, "Incoming field source descriptor differs from supTree"); + } - String supDstName = supDstData[0]; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + String[] supDstData = supDstDataByNsName.get(subDstNamespaces.get(subNs)); + if (supDstData == null && !supFeatures.hasNamespaces()) continue; - if (!supHasFieldDesc) continue; - String supDstDesc = supDstData[1]; - assertTrue(dstDescs == null || dstDescs[subNs] == null || dstDescs[subNs].equals(supDstDesc)); + if (supHasDstNames && subHasDstNames && dstNames != null) { + String supDstName = supDstData[0]; + String subDstName = dstNames[subNs]; + boolean uncompletedDst = subDstName == null && (supDstName == null || Objects.equals(supDstName, srcName)); + + if (!uncompletedDst) { + assertEquals(supDstName != null ? supDstName : srcName, subDstName, "Incoming field destination name differs from supTree"); + } + } + + if (supHasDstDescs && subHasDstDescs && dstDescs != null) { + String supDstDesc = supDstData[1]; + String subDstDesc = dstDescs[subNs]; + boolean uncompletedDst = subDstDesc == null && (supDstDesc == null || Objects.equals(supDstDesc, srcDesc)); + + if (!uncompletedDst) { + assertEquals(supDstDesc != null ? supDstDesc : srcDesc, subDstDesc, "Incoming field destination descriptor differs from supTree"); + } + } } return true; } @Override - public void visitFieldComment(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs, String comment) throws IOException { - if (!supHasComments) return; + public void visitFieldComment(String srcClsName, String srcName, @Nullable String srcDesc, + @Nullable String[] dstClsNames, @Nullable String[] dstNames, @Nullable String[] dstDescs, String comment) throws IOException { + if (!supFeatures.supportsFields() || supFeatures.elementComments() == ElementCommentSupport.NONE) return; + assertEquals(supTree.getClass(srcClsName).getField(srcName, srcDesc).getComment(), comment); } @Override - public boolean visitMethod(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs) throws IOException { - MethodMappingView supMth = supTree.getClass(srcClsName).getMethod(srcName, srcDesc); - Map supDstDataByNsName = new HashMap<>(); + public boolean visitMethod(String srcClsName, String srcName, @Nullable String srcDesc, + @Nullable String[] dstClsNames, @Nullable String[] dstNames, @Nullable String[] dstDescs) throws IOException { + if (!supFeatures.supportsMethods()) return true; - if (supMth == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return true; - throw new RuntimeException("SubTree method not contained in SupTree: " + srcName); + MethodMappingView supMth = supTree.getClass(srcClsName).getMethod(srcName, srcDesc); + boolean supHasSrcDescs = supFeatures.methods().srcDescs() != FeaturePresence.ABSENT; + boolean subHasSrcDescs = subFeatures.methods().srcDescs() != FeaturePresence.ABSENT; + boolean supHasDstNames = supFeatures.methods().dstNames() != FeaturePresence.ABSENT; + boolean subHasDstNames = subFeatures.methods().dstNames() != FeaturePresence.ABSENT; + boolean supHasDstDescs = supFeatures.methods().dstDescs() != FeaturePresence.ABSENT; + boolean subHasDstDescs = subFeatures.methods().dstDescs() != FeaturePresence.ABSENT; + + if (supMth == null) { // SupTree doesn't have this method, ensure the incoming mappings don't have any data for it + String[] subDstNames = null; + String[] subDstDescs = null; + + if (supHasDstNames && subHasDstNames) subDstNames = supFeatures.hasNamespaces() || dstNames == null ? dstNames : new String[]{dstNames[subNsIfSupNotNamespaced]}; + if (supHasDstDescs && subHasDstDescs) subDstDescs = supFeatures.hasNamespaces() || dstDescs == null ? dstDescs : new String[]{dstDescs[subNsIfSupNotNamespaced]}; + + assertTrue(isEmpty(subDstNames) && isEmpty(subDstDescs), "Incoming method not contained in SupTree: " + srcName + ", descriptor: " + srcDesc); + return true; } + Map supDstDataByNsName = new HashMap<>(); + for (int supNs = 0; supNs < supDstNsCount; supNs++) { supDstDataByNsName.put(supTree.getNamespaceName(supNs), new String[]{supMth.getDstName(supNs), supMth.getDstDesc(supNs)}); } - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String[] supDstData = supDstDataByNsName.get(dstNamespaces.get(subNs)); - if (!supHasNamespaces && supDstData == null) continue; + for (int subNs = 0; subNs < subDstNamespaces.size(); subNs++) { + if (supHasSrcDescs && subHasSrcDescs && srcDesc != null) { + assertEquals(supMth.getSrcDesc(), srcDesc, "Incoming method source descriptor differs from supTree"); + } - String supDstName = supDstData[0]; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + String[] supDstData = supDstDataByNsName.get(subDstNamespaces.get(subNs)); + if (supDstData == null && !supFeatures.hasNamespaces()) continue; - String supDstDesc = supDstData[1]; - assertTrue(dstDescs == null || dstDescs[subNs] == null || dstDescs[subNs].equals(supDstDesc)); + if (supHasDstNames && subHasDstNames && dstNames != null) { + String supDstName = supDstData[0]; + String subDstName = dstNames[subNs]; + boolean uncompletedDst = subDstName == null && (supDstName == null || Objects.equals(supDstName, srcName)); + + if (!uncompletedDst) { + assertEquals(supDstName != null ? supDstName : srcName, subDstName, "Incoming method destination name differs from supTree"); + } + } + + if (supHasDstDescs && subHasDstDescs && dstDescs != null) { + String supDstDesc = supDstData[1]; + String subDstDesc = dstDescs[subNs]; + boolean uncompletedDst = subDstDesc == null && (supDstDesc == null || Objects.equals(supDstDesc, srcDesc)); + + if (!uncompletedDst) { + assertEquals(supDstDesc != null ? supDstDesc : srcDesc, subDstDesc, "Incoming method destination descriptor differs from supTree"); + } + } } return true; } @Override - public void visitMethodComment(String srcClsName, String srcName, String srcDesc, - String[] dstClsNames, String[] dstNames, String[] dstDescs, String comment) throws IOException { - if (!supHasComments) return; + public void visitMethodComment(String srcClsName, String srcName, @Nullable String srcDesc, + @Nullable String[] dstClsNames, @Nullable String[] dstNames, @Nullable String[] dstDescs, String comment) throws IOException { + if (!supFeatures.supportsMethods() || supFeatures.elementComments() == ElementCommentSupport.NONE) return; + assertEquals(supTree.getClass(srcClsName).getMethod(srcName, srcDesc).getComment(), comment); } @Override - public boolean visitMethodArg(String srcClsName, String srcMethodName, String srcMethodDesc, int argPosition, int lvIndex, String srcName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames) throws IOException { - if (!supHasArgs) return false; + public boolean visitMethodArg(String srcClsName, String srcMethodName, @Nullable String srcMethodDesc, int argPosition, int lvIndex, @Nullable String srcName, + @Nullable String[] dstClsNames, @Nullable String[] dstMethodNames, @Nullable String[] dstMethodDescs, @Nullable String[] dstNames) throws IOException { + if (!supFeatures.supportsArgs()) return true; + MethodArgMappingView supArg = supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getArg(argPosition, lvIndex, srcName); - Map supDstNamesByNsName = new HashMap<>(); + boolean supHasPositions = supFeatures.args().positions() != FeaturePresence.ABSENT; + boolean subHasPositions = subFeatures.args().positions() != FeaturePresence.ABSENT; + boolean supHasLvIndices = supFeatures.args().lvIndices() != FeaturePresence.ABSENT; + boolean subHasLvIndices = subFeatures.args().lvIndices() != FeaturePresence.ABSENT; + boolean supHasDstNames = supFeatures.args().dstNames() != FeaturePresence.ABSENT; + boolean subHasDstNames = subFeatures.args().dstNames() != FeaturePresence.ABSENT; + + if (supArg == null) { // SupTree doesn't have this arg, ensure the incoming mappings don't have any data for it + if (supHasDstNames && subHasDstNames) { + String[] subDstNames = supFeatures.hasNamespaces() || dstNames == null ? dstNames : new String[]{dstNames[subNsIfSupNotNamespaced]}; + assertTrue(isEmpty(subDstNames), "Incoming arg not contained in SupTree: " + "position: " + argPosition + ", lvIndex: " + lvIndex + ", name: " + srcName); + } - if (supArg == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return true; - throw new RuntimeException("SubTree arg not contained in SupTree: " + srcName); + return true; } + Map supDstNamesByNsName = new HashMap<>(); + for (int supNs = 0; supNs < supDstNsCount; supNs++) { supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supArg.getDstName(supNs)); } - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String supDstName = supDstNamesByNsName.get(dstNamespaces.get(subNs)); - if (!supHasNamespaces && supDstName == null) continue; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + for (int subNs = 0; subNs < subDstNamespaces.size(); subNs++) { + if (supHasPositions && subHasPositions) { + assertEquals(supArg.getArgPosition(), argPosition, "Incoming arg position differs from supTree"); + } + + if (supHasLvIndices && subHasLvIndices) { + assertEquals(supArg.getLvIndex(), lvIndex, "Incoming arg local variable index differs from supTree"); + } + + if (supHasDstNames && subHasDstNames && dstNames != null) { + String supDstName = supDstNamesByNsName.get(subDstNamespaces.get(subNs)); + if (supDstName == null && !supFeatures.hasNamespaces()) continue; + + String subDstName = dstNames[subNs]; + if (subDstName == null && (supDstName == null || Objects.equals(supDstName, srcName))) continue; // uncompleted dst name + + assertEquals(supDstName != null ? supDstName : srcName, subDstName, "Incoming arg destination name differs from supTree"); + } } return true; } @Override - public void visitMethodArgComment(String srcClsName, String srcMethodName, String srcMethodDesc, int argPosition, int lvIndex, String srcArgName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames, String comment) throws IOException { - if (!supHasComments) return; + public void visitMethodArgComment(String srcClsName, String srcMethodName, @Nullable String srcMethodDesc, int argPosition, int lvIndex, @Nullable String srcArgName, + @Nullable String[] dstClsNames, @Nullable String[] dstMethodNames, @Nullable String[] dstMethodDescs, @Nullable String[] dstNames, String comment) throws IOException { + if (!supFeatures.supportsArgs() || supFeatures.elementComments() == ElementCommentSupport.NONE) return; + assertEquals(supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getArg(argPosition, lvIndex, srcArgName).getComment(), comment); } @Override - public boolean visitMethodVar(String srcClsName, String srcMethodName, String srcMethodDesc, - int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, String srcName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames) throws IOException { - if (!supHasVars) return false; + public boolean visitMethodVar(String srcClsName, String srcMethodName, @Nullable String srcMethodDesc, int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, @Nullable String srcName, + @Nullable String[] dstClsNames, @Nullable String[] dstMethodNames, @Nullable String[] dstMethodDescs, @Nullable String[] dstNames) throws IOException { + if (!supFeatures.supportsVars()) return true; + MethodVarMappingView supVar = supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getVar(lvtRowIndex, lvIndex, startOpIdx, endOpIdx, srcName); - Map supDstNamesByNsName = new HashMap<>(); + boolean supHasLvIndices = supFeatures.vars().lvIndices() != FeaturePresence.ABSENT; + boolean subHasLvIndices = subFeatures.vars().lvIndices() != FeaturePresence.ABSENT; + boolean supHasLvtIndices = supFeatures.vars().lvtRowIndices() != FeaturePresence.ABSENT; + boolean subHasLvtIndices = subFeatures.vars().lvtRowIndices() != FeaturePresence.ABSENT; + boolean supHasStartOpIndices = supFeatures.vars().startOpIndices() != FeaturePresence.ABSENT; + boolean subHasStartOpIndices = subFeatures.vars().startOpIndices() != FeaturePresence.ABSENT; + boolean supHasEndOpIndices = supFeatures.vars().endOpIndices() != FeaturePresence.ABSENT; + boolean subHasEndOpIndices = subFeatures.vars().endOpIndices() != FeaturePresence.ABSENT; + boolean supHasDstNames = supFeatures.vars().dstNames() != FeaturePresence.ABSENT; + boolean subHasDstNames = subFeatures.vars().dstNames() != FeaturePresence.ABSENT; + + if (supVar == null) { // SupTree doesn't have this var, ensure the incoming mappings don't have any data for it + if (supHasDstNames && subHasDstNames) { + String[] subDstNames = supFeatures.hasNamespaces() || dstNames == null ? dstNames : new String[]{dstNames[subNsIfSupNotNamespaced]}; + assertTrue(isEmpty(subDstNames), "Incoming var not contained in SupTree: " + "lvtRowIndex: " + lvtRowIndex + ", lvIndex: " + lvIndex + ", startOpIdx: " + startOpIdx + ", endOpIdx: " + endOpIdx + ", name: " + srcName); + } - if (supVar == null) { - String[] tmpDst = supHasNamespaces ? dstNames : new String[]{dstNames[0]}; - if (!Arrays.stream(tmpDst).anyMatch(Objects::nonNull)) return true; - throw new RuntimeException("SubTree var not contained in SupTree: " + srcName); + return true; } + Map supDstNamesByNsName = new HashMap<>(); + for (int supNs = 0; supNs < supDstNsCount; supNs++) { supDstNamesByNsName.put(supTree.getNamespaceName(supNs), supVar.getDstName(supNs)); } - for (int subNs = 0; subNs < dstNames.length; subNs++) { - String supDstName = supDstNamesByNsName.get(dstNamespaces.get(subNs)); - if (!supHasNamespaces && supDstName == null) continue; - assertTrue(dstNames[subNs] == null || dstNames[subNs].equals(supDstName) || (supDstName == null && dstNames[subNs].equals(srcName))); + for (int subNs = 0; subNs < subDstNamespaces.size(); subNs++) { + if (supHasLvIndices && subHasLvIndices) { + assertEquals(supVar.getLvIndex(), lvIndex, "Incoming var lvIndex differs from supTree"); + } + + if (supHasLvtIndices && subHasLvtIndices) { + assertEquals(supVar.getLvtRowIndex(), lvtRowIndex, "Incoming var lvtIndex differs from supTree"); + } + + if (supHasStartOpIndices && subHasStartOpIndices) { + assertEquals(supVar.getStartOpIdx(), startOpIdx, "Incoming var startOpIndex differs from supTree"); + } + + if (supHasEndOpIndices && subHasEndOpIndices) { + assertEquals(supVar.getEndOpIdx(), endOpIdx, "Incoming var endOpIndex differs from supTree"); + } + + String supDstName = supDstNamesByNsName.get(subDstNamespaces.get(subNs)); + if (supDstName == null && !supFeatures.hasNamespaces()) continue; + + if (supHasDstNames && subHasDstNames && dstNames != null) { + String subDstName = dstNames[subNs]; + if (subDstName == null && (supDstName == null || Objects.equals(supDstName, srcName))) continue; // uncompleted dst name + + assertEquals(supDstName != null ? supDstName : srcName, subDstName, "Incoming var destination name differs from supTree"); + } } return true; } @Override - public void visitMethodVarComment(String srcClsName, String srcMethodName, String srcMethodDesc, - int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, String srcVarName, - String[] dstClsNames, String[] dstMethodNames, String[] dstMethodDescs, String[] dstNames, String comment) throws IOException { - if (!supHasComments) return; + public void visitMethodVarComment(String srcClsName, String srcMethodName, @Nullable String srcMethodDesc, int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, @Nullable String srcVarName, + @Nullable String[] dstClsNames, @Nullable String[] dstMethodNames, @Nullable String[] dstMethodDescs, @Nullable String[] dstNames, String comment) throws IOException { + if (!supFeatures.supportsVars() || supFeatures.elementComments() == ElementCommentSupport.NONE) return; + assertEquals(supTree.getClass(srcClsName).getMethod(srcMethodName, srcMethodDesc).getVar(lvtRowIndex, lvIndex, startOpIdx, endOpIdx, srcVarName).getComment(), comment); } + private boolean isEmpty(String[] arr) { + if (arr == null) return true; + + for (String s : arr) { + if (s != null) return false; + } + + return true; + } + private final MappingTreeView supTree; private final int supDstNsCount; - private final boolean subHasNamespaces; - private final boolean supHasNamespaces; - private final boolean supHasFieldDesc; - private final boolean supHasArgs; - private final boolean supHasVars; - private final boolean supHasComments; - private List dstNamespaces; + private final FeatureSet supFeatures; + private final FeatureSet subFeatures; + private int subNsIfSupNotNamespaced; + private List subDstNamespaces; } diff --git a/src/test/java/net/fabricmc/mappingio/TestHelper.java b/src/test/java/net/fabricmc/mappingio/TestHelper.java index 268effe3..bae2c536 100644 --- a/src/test/java/net/fabricmc/mappingio/TestHelper.java +++ b/src/test/java/net/fabricmc/mappingio/TestHelper.java @@ -63,6 +63,8 @@ public static String getFileName(MappingFormat format) { return "tsrgV2.tsrg"; case PROGUARD_FILE: return "proguard.txt"; + case INTELLIJ_MIGRATION_MAP_FILE: + return "migration-map.xml"; case RECAF_SIMPLE_FILE: return "recaf-simple.txt"; case JOBF_FILE: @@ -91,6 +93,7 @@ public static MemoryMappingTree createTestTree() { visitMethodArg(tree, dstNs); visitMethodVar(tree, dstNs); visitInnerClass(tree, 1, dstNs); + visitComment(tree); visitField(tree, dstNs); visitClass(tree, dstNs); diff --git a/src/test/java/net/fabricmc/mappingio/format/FeatureSetInstantiator.java b/src/test/java/net/fabricmc/mappingio/format/FeatureSetInstantiator.java new file mode 100644 index 00000000..388deab9 --- /dev/null +++ b/src/test/java/net/fabricmc/mappingio/format/FeatureSetInstantiator.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 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.format; + +public class FeatureSetInstantiator { + public static FeatureSet withFullSupport() { + return new FeatureSetBuilder(true).build(); + } +} diff --git a/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java b/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java index 94890650..477bfebe 100644 --- a/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java +++ b/src/test/java/net/fabricmc/mappingio/read/DetectionTest.java @@ -102,6 +102,12 @@ public void proguardFile() throws Exception { check(format); } + @Test + public void migrationMapFile() throws Exception { + MappingFormat format = MappingFormat.INTELLIJ_MIGRATION_MAP_FILE; + check(format); + } + @Test public void recafSimpleFile() throws Exception { MappingFormat format = MappingFormat.RECAF_SIMPLE_FILE; diff --git a/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java b/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java index e7916350..30244c7b 100644 --- a/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java +++ b/src/test/java/net/fabricmc/mappingio/read/EmptyContentReadTest.java @@ -27,6 +27,7 @@ import net.fabricmc.mappingio.MappingVisitor; import net.fabricmc.mappingio.VisitOrderVerifyingVisitor; import net.fabricmc.mappingio.format.enigma.EnigmaFileReader; +import net.fabricmc.mappingio.format.intellij.MigrationMapFileReader; import net.fabricmc.mappingio.format.jobf.JobfFileReader; import net.fabricmc.mappingio.format.proguard.ProGuardFileReader; import net.fabricmc.mappingio.format.simple.RecafSimpleFileReader; @@ -52,14 +53,36 @@ public void emptyEnigmaFile() throws Exception { @Test public void emptyTinyFile() throws Exception { - assertThrows(IOException.class, () -> Tiny1FileReader.read(new StringReader(""), target)); - Tiny1FileReader.read(new StringReader("v1\t"), target); + String header0 = ""; + String header1 = "v1"; + String header2 = header1 + "\t"; + String header3 = header2 + "srcNs"; + String header4 = header3 + "\t"; + String header5 = header4 + "dstNs"; + + assertThrows(IOException.class, () -> Tiny1FileReader.read(new StringReader(header0), target)); + assertThrows(IOException.class, () -> Tiny1FileReader.read(new StringReader(header1), target)); + assertThrows(IOException.class, () -> Tiny1FileReader.read(new StringReader(header2), target)); + assertThrows(IOException.class, () -> Tiny1FileReader.read(new StringReader(header3), target)); + assertThrows(IOException.class, () -> Tiny1FileReader.read(new StringReader(header4), target)); + Tiny1FileReader.read(new StringReader(header5), target); } @Test public void emptyTinyV2File() throws Exception { - assertThrows(IOException.class, () -> Tiny2FileReader.read(new StringReader(""), target)); - Tiny2FileReader.read(new StringReader("tiny\t2\t0"), target); + String header0 = ""; + String header1 = "tiny\t2\t0"; + String header2 = header1 + "\t"; + String header3 = header2 + "srcNs"; + String header4 = header3 + "\t"; + String header5 = header4 + "dstNs"; + + assertThrows(IOException.class, () -> Tiny2FileReader.read(new StringReader(header0), target)); + assertThrows(IOException.class, () -> Tiny2FileReader.read(new StringReader(header1), target)); + assertThrows(IOException.class, () -> Tiny2FileReader.read(new StringReader(header2), target)); + assertThrows(IOException.class, () -> Tiny2FileReader.read(new StringReader(header3), target)); + assertThrows(IOException.class, () -> Tiny2FileReader.read(new StringReader(header4), target)); + Tiny2FileReader.read(new StringReader(header5), target); } @Test @@ -79,7 +102,35 @@ public void emptyJamFile() throws Exception { @Test public void emptyTsrgFile() throws Exception { - TsrgFileReader.read(new StringReader(""), target); + String header0 = ""; + String header1 = "tsrg2"; + String header2 = header1 + " "; + String header3 = header2 + "srcNs"; + String header4 = header3 + " "; + String header5 = header4 + "dstNs"; + + TsrgFileReader.read(new StringReader(header0), target); // interpreted as TSRG v1 + assertThrows(IOException.class, () -> TsrgFileReader.read(new StringReader(header1), target)); + assertThrows(IOException.class, () -> TsrgFileReader.read(new StringReader(header2), target)); + assertThrows(IOException.class, () -> TsrgFileReader.read(new StringReader(header3), target)); + assertThrows(IOException.class, () -> TsrgFileReader.read(new StringReader(header4), target)); + instantiateTree(); + TsrgFileReader.read(new StringReader(header5), target); + } + + @Test + public void emptyMigrationMapFile() throws Exception { + assertThrows(IOException.class, () -> MigrationMapFileReader.read(new StringReader(""), target)); + + instantiateTree(); + assertThrows(IOException.class, () -> MigrationMapFileReader.read(new StringReader(""), target)); + + instantiateTree(); + MigrationMapFileReader.read( + new StringReader("\n" + + "\n" + + ""), + target); } @Test diff --git a/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java b/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java index 083c00ee..2b990818 100644 --- a/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java +++ b/src/test/java/net/fabricmc/mappingio/read/ValidContentReadTest.java @@ -16,6 +16,8 @@ package net.fabricmc.mappingio.read; +import java.nio.file.Path; + import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,8 +27,10 @@ import net.fabricmc.mappingio.TestHelper; import net.fabricmc.mappingio.VisitOrderVerifyingVisitor; import net.fabricmc.mappingio.adapter.FlatAsRegularMappingVisitor; +import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.format.MappingFormat; import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MappingTreeView; import net.fabricmc.mappingio.tree.MemoryMappingTree; import net.fabricmc.mappingio.tree.VisitableMappingTree; @@ -129,6 +133,14 @@ public void proguardFile() throws Exception { checkRepeated(format, true); } + @Test + public void migrationMapFile() throws Exception { + MappingFormat format = MappingFormat.INTELLIJ_MIGRATION_MAP_FILE; + checkDefault(format); + checkHoles(format); + checkRepeated(format, true); + } + @Test public void recafSimpleFile() throws Exception { MappingFormat format = MappingFormat.RECAF_SIMPLE_FILE; @@ -145,39 +157,66 @@ public void jobfFile() throws Exception { checkRepeated(format, true); } - private VisitableMappingTree checkDefault(MappingFormat format) throws Exception { + private void checkDefault(MappingFormat format) throws Exception { + Path path = TestHelper.MappingDirs.VALID.resolve(TestHelper.getFileName(format)); + VisitableMappingTree tree = new MemoryMappingTree(); boolean allowConsecutiveDuplicateElementVisits = false; - MappingReader.read(TestHelper.MappingDirs.VALID.resolve(TestHelper.getFileName(format)), format, new VisitOrderVerifyingVisitor(allowConsecutiveDuplicateElementVisits, tree)); - assertSubset(tree, format, testTree, null, allowConsecutiveDuplicateElementVisits); - assertSubset(testTree, null, tree, format, allowConsecutiveDuplicateElementVisits); + MappingReader.read(path, format, new VisitOrderVerifyingVisitor(allowConsecutiveDuplicateElementVisits, tree)); + assertEqual(tree, format, testTree, allowConsecutiveDuplicateElementVisits); - return tree; + tree = new MemoryMappingTree(); + MappingReader.read(path, format, + new MappingSourceNsSwitch( + new VisitOrderVerifyingVisitor( + allowConsecutiveDuplicateElementVisits, + new MappingSourceNsSwitch( + new VisitOrderVerifyingVisitor( + allowConsecutiveDuplicateElementVisits, + tree), + testTree.getSrcNamespace())), + testTree.getDstNamespaces().get(0))); + assertEqual(tree, format, testTree, allowConsecutiveDuplicateElementVisits); } - private VisitableMappingTree checkHoles(MappingFormat format) throws Exception { + private void checkHoles(MappingFormat format) throws Exception { + Path path = TestHelper.MappingDirs.VALID_WITH_HOLES.resolve(TestHelper.getFileName(format)); + VisitableMappingTree tree = new MemoryMappingTree(); boolean allowConsecutiveDuplicateElementVisits = false; - MappingReader.read(TestHelper.MappingDirs.VALID_WITH_HOLES.resolve(TestHelper.getFileName(format)), format, new VisitOrderVerifyingVisitor(allowConsecutiveDuplicateElementVisits, tree)); - assertSubset(tree, format, testTreeWithHoles, null, allowConsecutiveDuplicateElementVisits); - assertSubset(testTreeWithHoles, null, tree, format, allowConsecutiveDuplicateElementVisits); - - return tree; + MappingReader.read(path, format, new VisitOrderVerifyingVisitor(allowConsecutiveDuplicateElementVisits, tree)); + assertEqual(tree, format, testTreeWithHoles, allowConsecutiveDuplicateElementVisits); } - private VisitableMappingTree checkRepeated(MappingFormat format, boolean allowConsecutiveDuplicateElementVisits) throws Exception { + private void checkRepeated(MappingFormat format, boolean allowConsecutiveDuplicateElementVisits) throws Exception { + Path path = TestHelper.MappingDirs.REPEATED_ELEMENTS.resolve(TestHelper.getFileName(format)); + VisitableMappingTree tree = new MemoryMappingTree(); - MappingReader.read(TestHelper.MappingDirs.REPEATED_ELEMENTS.resolve(TestHelper.getFileName(format)), format, new VisitOrderVerifyingVisitor(allowConsecutiveDuplicateElementVisits, tree)); + MappingReader.read(path, format, new VisitOrderVerifyingVisitor(allowConsecutiveDuplicateElementVisits, tree)); + assertEqual(tree, format, testTreeWithRepeatedElements, allowConsecutiveDuplicateElementVisits); - assertSubset(tree, format, testTreeWithRepeatedElements, null, allowConsecutiveDuplicateElementVisits); - assertSubset(testTreeWithRepeatedElements, null, tree, format, allowConsecutiveDuplicateElementVisits); + tree = new MemoryMappingTree(); + MappingReader.read(path, format, + new MappingSourceNsSwitch( + new VisitOrderVerifyingVisitor( + allowConsecutiveDuplicateElementVisits, + new MappingSourceNsSwitch( + new VisitOrderVerifyingVisitor( + allowConsecutiveDuplicateElementVisits, + tree), + testTreeWithRepeatedElements.getSrcNamespace())), + testTreeWithRepeatedElements.getDstNamespaces().get(0))); + assertEqual(tree, format, testTreeWithRepeatedElements, allowConsecutiveDuplicateElementVisits); + } - return tree; + private void assertEqual(MappingTreeView tree, MappingFormat format, MappingTreeView referenceTree, boolean allowConsecutiveDuplicateElementVisits) throws Exception { + assertSubset(tree, format, referenceTree, null, allowConsecutiveDuplicateElementVisits); + assertSubset(referenceTree, null, tree, format, allowConsecutiveDuplicateElementVisits); } - private void assertSubset(MappingTree subTree, @Nullable MappingFormat subFormat, MappingTree supTree, @Nullable MappingFormat supFormat, boolean allowConsecutiveDuplicateElementVisits) throws Exception { + private void assertSubset(MappingTreeView subTree, @Nullable MappingFormat subFormat, MappingTreeView supTree, @Nullable MappingFormat supFormat, boolean allowConsecutiveDuplicateElementVisits) throws Exception { subTree.accept( new VisitOrderVerifyingVisitor( allowConsecutiveDuplicateElementVisits, diff --git a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java index d36a9454..d57a2bb0 100644 --- a/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java +++ b/src/test/java/net/fabricmc/mappingio/visiting/VisitEndTest.java @@ -107,6 +107,12 @@ public void proguardFile() throws Exception { check(format); } + @Test + public void migrationMapFile() throws Exception { + MappingFormat format = MappingFormat.INTELLIJ_MIGRATION_MAP_FILE; + check(format); + } + @Test public void recafSimpleFile() throws Exception { MappingFormat format = MappingFormat.RECAF_SIMPLE_FILE; diff --git a/src/test/java/net/fabricmc/mappingio/write/WriteTest.java b/src/test/java/net/fabricmc/mappingio/write/WriteTest.java index 9f2621f3..3a3cb5ed 100644 --- a/src/test/java/net/fabricmc/mappingio/write/WriteTest.java +++ b/src/test/java/net/fabricmc/mappingio/write/WriteTest.java @@ -98,6 +98,11 @@ public void proguardFile() throws Exception { check(MappingFormat.PROGUARD_FILE); } + @Test + public void migrationMapFile() throws Exception { + check(MappingFormat.INTELLIJ_MIGRATION_MAP_FILE); + } + @Test public void recafSimpleFile() throws Exception { check(MappingFormat.RECAF_SIMPLE_FILE); diff --git a/src/test/resources/detection/migration-map.xml b/src/test/resources/detection/migration-map.xml new file mode 100644 index 00000000..82555dab --- /dev/null +++ b/src/test/resources/detection/migration-map.xml @@ -0,0 +1,3 @@ + + + diff --git a/src/test/resources/read/repeated-elements/enigma.mappings b/src/test/resources/read/repeated-elements/enigma.mappings index bb32dba8..446a78c3 100644 --- a/src/test/resources/read/repeated-elements/enigma.mappings +++ b/src/test/resources/read/repeated-elements/enigma.mappings @@ -7,7 +7,10 @@ CLASS class_1 class1Ns0Rename ARG 1 param1Ns0Rename0 ARG 1 param1Ns0Rename CLASS class_2 class2Ns0Rename0 + CLASS class_2 class2Ns0Rename1 + COMMENT This is a comment. CLASS class_2 class2Ns0Rename + COMMENT This is a comment FIELD field_2 field2Ns0Rename0 I FIELD field_2 field2Ns0Rename I CLASS class_3 class3Ns0Rename0 diff --git a/src/test/resources/read/repeated-elements/migration-map.xml b/src/test/resources/read/repeated-elements/migration-map.xml new file mode 100644 index 00000000..dc689ab6 --- /dev/null +++ b/src/test/resources/read/repeated-elements/migration-map.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/test/resources/read/repeated-elements/tinyV2.tiny b/src/test/resources/read/repeated-elements/tinyV2.tiny index 258c4cd5..01be3216 100644 --- a/src/test/resources/read/repeated-elements/tinyV2.tiny +++ b/src/test/resources/read/repeated-elements/tinyV2.tiny @@ -11,6 +11,8 @@ c class_1 class1Ns0Rename class1Ns1Rename v 2 2 2 var_1 var1Ns0Rename var1Ns1Rename c class_1$class_2 class1Ns0Rename$class2Ns0Rename0 class1Ns1Rename$class2Ns1Rename0 c class_1$class_2 class1Ns0Rename$class2Ns0Rename class1Ns1Rename$class2Ns1Rename + c This is a comment. + c This is a comment f I field_2 field2Ns0Rename0 field2Ns1Rename0 f I field_2 field2Ns0Rename field2Ns1Rename c class_3 class3Ns0Rename0 class3Ns1Rename0 diff --git a/src/test/resources/read/valid-with-holes/migration-map.xml b/src/test/resources/read/valid-with-holes/migration-map.xml new file mode 100644 index 00000000..29d91399 --- /dev/null +++ b/src/test/resources/read/valid-with-holes/migration-map.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/read/valid/enigma-dir/class1Ns0Rename.mapping b/src/test/resources/read/valid/enigma-dir/class1Ns0Rename.mapping index a328505e..8a313665 100644 --- a/src/test/resources/read/valid/enigma-dir/class1Ns0Rename.mapping +++ b/src/test/resources/read/valid/enigma-dir/class1Ns0Rename.mapping @@ -3,4 +3,5 @@ CLASS class_1 class1Ns0Rename METHOD method_1 method1Ns0Rename ()I ARG 1 param1Ns0Rename CLASS class_2 class2Ns0Rename + COMMENT This is a comment FIELD field_2 field2Ns0Rename I diff --git a/src/test/resources/read/valid/enigma.mappings b/src/test/resources/read/valid/enigma.mappings index 426d0ef5..05963c4a 100644 --- a/src/test/resources/read/valid/enigma.mappings +++ b/src/test/resources/read/valid/enigma.mappings @@ -3,5 +3,6 @@ CLASS class_1 class1Ns0Rename METHOD method_1 method1Ns0Rename ()I ARG 1 param1Ns0Rename CLASS class_2 class2Ns0Rename + COMMENT This is a comment FIELD field_2 field2Ns0Rename I CLASS class_3 class3Ns0Rename diff --git a/src/test/resources/read/valid/migration-map.xml b/src/test/resources/read/valid/migration-map.xml new file mode 100644 index 00000000..161a8c9d --- /dev/null +++ b/src/test/resources/read/valid/migration-map.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/test/resources/read/valid/tinyV2.tiny b/src/test/resources/read/valid/tinyV2.tiny index 84237945..b8470282 100644 --- a/src/test/resources/read/valid/tinyV2.tiny +++ b/src/test/resources/read/valid/tinyV2.tiny @@ -5,5 +5,6 @@ c class_1 class1Ns0Rename class1Ns1Rename p 1 param_1 param1Ns0Rename param1Ns1Rename v 2 2 2 var_1 var1Ns0Rename var1Ns1Rename c class_1$class_2 class1Ns0Rename$class2Ns0Rename class1Ns1Rename$class2Ns1Rename + c This is a comment f I field_2 field2Ns0Rename field2Ns1Rename c class_3 class3Ns0Rename class3Ns1Rename