Skip to content

Commit

Permalink
Add IntelliJ IDEA Migration Map reader and writer (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
NebelNidas authored Aug 25, 2024
1 parent 368598c commit a27d20a
Show file tree
Hide file tree
Showing 18 changed files with 477 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/net/fabricmc/mappingio/MappingReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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("<migrationMap>")) {
return MappingFormat.INTELLIJ_MIGRATION_MAP_FILE;
} else if ((headerStr.startsWith("p ")
|| headerStr.startsWith("c ")
|| headerStr.startsWith("f ")
|| headerStr.startsWith("m "))
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/fabricmc/mappingio/MappingWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ public enum MappingFormat {
.withSrcDescs(FeaturePresence.REQUIRED))
.withFileComments(true)),

/**
* The IntelliJ IDEA migration map format, as implemented <a href="https://github.com/JetBrains/intellij-community/tree/5b6191dd34e05de8897f5da68757146395a260cc/java/java-impl-refactorings/src/com/intellij/refactoring/migration">here</a>.
*/
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 <a href="https://github.com/Col-E/Recaf/blob/e9765d4e02991a9dd48e67c9572a063c14552e7c/src/main/java/me/coley/recaf/mapping/SimpleMappings.java#L14-L23">here</a>.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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 {
}
}
}
Original file line number Diff line number Diff line change
@@ -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<MappingFlag> getFlags() {
return flags;
}

@Override
public void visitNamespaces(String srcNamespace, List<String> 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<MappingFlag> flags = EnumSet.of(MappingFlag.NEEDS_ELEMENT_UNIQUENESS);

private final Writer writer;
private XMLStreamWriter xmlWriter;
private String srcName;
private String dstName;
}
Loading

0 comments on commit a27d20a

Please sign in to comment.