aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/AutoImports.kt141
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/ShortNameIndex.kt100
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt45
-rw-r--r--src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImports.kt344
-rw-r--r--src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImportsFormatting.kt142
-rw-r--r--src/test/kotlin/com/replaymod/gradle/remap/imports/TestKotlinAutoImports.kt26
-rw-r--r--src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinAnnotation.kt8
-rw-r--r--src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinInjections.kt18
-rw-r--r--src/test/kotlin/com/replaymod/gradle/remap/util/TestData.kt13
9 files changed, 812 insertions, 25 deletions
diff --git a/src/main/kotlin/com/replaymod/gradle/remap/AutoImports.kt b/src/main/kotlin/com/replaymod/gradle/remap/AutoImports.kt
new file mode 100644
index 0000000..eb57486
--- /dev/null
+++ b/src/main/kotlin/com/replaymod/gradle/remap/AutoImports.kt
@@ -0,0 +1,141 @@
+package com.replaymod.gradle.remap
+
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.com.intellij.psi.*
+import org.jetbrains.kotlin.psi.psiUtil.endOffset
+import org.jetbrains.kotlin.psi.psiUtil.startOffset
+
+internal class AutoImports(private val environment: KotlinCoreEnvironment) {
+
+ private val shortClassNames = ShortNameIndex(environment)
+
+ fun apply(originalFile: PsiFile, mappedFile: String, processedFile: String): String =
+ apply(originalFile, originalFile.text.lines(), mappedFile.lines(), processedFile.lines())
+
+ private fun apply(
+ originalFile: PsiFile,
+ originalLines: List<String>,
+ mappedLines: List<String>,
+ processedLines: List<String>,
+ ): String {
+ if (originalLines.size != mappedLines.size || originalLines.size != processedLines.size) {
+ return mappedLines.joinToString("\n")
+ }
+
+ val inputLines = processedLines.mapIndexed { index, processedLine ->
+ if (originalLines[index] == processedLine) {
+ mappedLines[index]
+ } else {
+ processedLine
+ }
+ }
+ val inputText = inputLines.joinToString("\n")
+
+ val psiFileFactory = PsiFileFactory.getInstance(environment.project)
+ val psiFile =
+ psiFileFactory.createFileFromText(originalFile.language, inputText) as? PsiJavaFile ?: return inputText
+ val pkg = psiFile.packageStatement?.packageReference?.resolve() as? PsiPackage
+
+ val references = findOutgoingReferences(psiFile)
+
+ val imports = psiFile.importList?.importStatements ?: emptyArray()
+ val onDemandImports = imports.filter { it.isOnDemand }.mapNotNull { it.qualifiedName }.map { "$it." }.toSet()
+ val existingImports = imports.filter { !it.isOnDemand }.mapNotNull { it.qualifiedName }.toSet()
+ val unusedImports = existingImports.filter { it.substringAfterLast(".") !in references }.toSet()
+
+ val implicitReferenceSources = listOfNotNull(
+ psiFile.classes.flatMap { it.allInnerClasses.asIterable() },
+ pkg?.classes?.asIterable(),
+ )
+ val implicitReferences = implicitReferenceSources.flatten().mapNotNull { it.name }.toSet()
+ val importedReferences = existingImports.map { it.substringAfterLast(".") }.toSet()
+ val missingReferences = references.asSequence() - importedReferences - implicitReferences
+ val newImports = missingReferences.mapNotNull { shortClassNames[it].singleOrNull()?.qualifiedName }
+ .filter { ref -> onDemandImports.none { ref.startsWith(it) } }
+ .filter { !it.startsWith("java.lang.") }
+
+ val finalImports = existingImports.toSet() - unusedImports.toSet() + newImports + onDemandImports.map { "$it*" }
+
+ val textBuilder = StringBuilder(inputText)
+
+ imports.map { it.textRange }.sortedByDescending { it.startOffset }.forEach { importRange ->
+ textBuilder.replace(importRange.startOffset, importRange.endOffset, "")
+
+ val start = importRange.startOffset
+ val whiteSpaceRange = start - 1..start
+ if (whiteSpaceRange.first in textBuilder.indices && whiteSpaceRange.last in textBuilder.indices) {
+ val whiteSpaceReplacement = when (textBuilder.substring(whiteSpaceRange)) {
+ "\n\n" -> "\n"
+ "\n " -> "\n"
+ " \n" -> "\n"
+ " " -> " "
+ else -> null
+ }
+ if (whiteSpaceReplacement != null) {
+ textBuilder.replace(whiteSpaceRange.first, whiteSpaceRange.last + 1, whiteSpaceReplacement)
+ }
+ }
+ }
+
+ val startOfImports = psiFile.importList?.takeIf { it.textLength > 0 }?.startOffset
+ val endOfPackage = psiFile.packageStatement?.endOffset ?: 0
+
+ val removedLineCount = inputLines.size - textBuilder.lineSequence().count()
+ textBuilder.insert(startOfImports ?: endOfPackage, "\n".repeat(removedLineCount))
+
+ var index = startOfImports ?: endOfPackage
+
+ if (startOfImports == null) {
+ repeat(2) {
+ if (textBuilder[index + 1] == '\n' && textBuilder[index + 2] == '\n') {
+ index++
+ }
+ }
+ }
+
+ val javaImports = finalImports.filter { it.startsWith("java.") || it.startsWith("javax.") }.toSet()
+ val otherImports = finalImports - javaImports
+ val importGroups = listOf(otherImports, javaImports).filter { it.isNotEmpty() }
+
+ for ((importGroupIndex, importGroup) in importGroups.withIndex()) {
+ val hasMoreGroups = importGroupIndex + 1 in importGroups.indices
+
+ for (import in importGroup.sorted()) {
+ val hasPrecedingStatement = index > 0 && textBuilder[index - 1] != '\n'
+ val canAdvanceToNextLine = textBuilder[index + 1] == '\n' && textBuilder[index + 2] == '\n'
+
+ val str = (if (hasPrecedingStatement) " " else "") + "import $import;"
+ textBuilder.insert(index, str)
+ index += str.length + if (canAdvanceToNextLine) 1 else 0
+ }
+
+ if (hasMoreGroups && textBuilder[index + 1] == '\n' && textBuilder[index + 2] == '\n') {
+ index++
+ }
+ }
+
+ return textBuilder.toString()
+ }
+
+ private fun findOutgoingReferences(file: PsiJavaFile): Set<String> {
+ val references = mutableSetOf<String>()
+
+ fun recordReference(reference: PsiJavaCodeReferenceElement) {
+ if (reference.isQualified) return
+ val name = reference.referenceName ?: return
+ if (!name.first().isUpperCase()) return
+ val resolved = reference.resolve()
+ if (resolved is PsiTypeParameter) return
+ if (resolved is PsiVariable) return
+ references.add(name)
+ }
+
+ file.accept(object : JavaRecursiveElementVisitor() {
+ override fun visitReferenceElement(reference: PsiJavaCodeReferenceElement) {
+ recordReference(reference)
+ super.visitReferenceElement(reference)
+ }
+ })
+ return references
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/com/replaymod/gradle/remap/ShortNameIndex.kt b/src/main/kotlin/com/replaymod/gradle/remap/ShortNameIndex.kt
new file mode 100644
index 0000000..c78256a
--- /dev/null
+++ b/src/main/kotlin/com/replaymod/gradle/remap/ShortNameIndex.kt
@@ -0,0 +1,100 @@
+package com.replaymod.gradle.remap
+
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.cli.jvm.config.javaSourceRoots
+import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots
+import org.jetbrains.kotlin.com.intellij.lang.jvm.JvmModifier
+import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems
+import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile
+import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager
+import org.jetbrains.kotlin.com.intellij.psi.PsiClass
+import org.jetbrains.kotlin.com.intellij.psi.PsiJavaFile
+import org.jetbrains.kotlin.com.intellij.psi.PsiManager
+import org.jetbrains.kotlin.config.JVMConfigurationKeys
+
+class ShortNameIndex(private val environment: KotlinCoreEnvironment) {
+ private val psiManager = PsiManager.getInstance(environment.project)
+
+ private val entries: Map<String, ShortNameEntry> = mutableMapOf<String, ShortNameEntry>().apply {
+ val localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
+ val jarFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.JAR_PROTOCOL)
+ val jrtFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.JRT_PROTOCOL)
+
+ val classpathRoots = environment.configuration.jvmClasspathRoots.mapNotNull { file ->
+ if (file.isFile) {
+ jarFileSystem.findFileByPath("${file.absolutePath}!/")
+ } else {
+ localFileSystem.findFileByPath(file.absolutePath)
+ }
+ }
+
+ val jdkHome = environment.configuration[JVMConfigurationKeys.JDK_HOME]
+ val allModuleRoots = jrtFileSystem.findFileByPath("$jdkHome!/modules")?.children ?: emptyArray()
+ val javaModuleRoots = allModuleRoots.filter { it.name.startsWith("java.") }
+
+ val sourcesRoots = environment.configuration.javaSourceRoots.mapNotNull { localFileSystem.findFileByPath(it) }
+
+ fun index(file: VirtualFile, pkgPrefix: String) {
+ if (file.isDirectory) {
+ val pkg = "$pkgPrefix${file.name}."
+ file.children.forEach { index(it, pkg) }
+ } else if (file.extension == "class") {
+ val fileName = file.nameWithoutExtension
+ val shortName = if ('$' in fileName) {
+ val innerName = fileName.substringAfterLast('$')
+ if (!innerName.first().isJavaIdentifierStart()) {
+ return
+ }
+ innerName
+ } else {
+ fileName
+ }
+ getOrPut(shortName, ::ShortNameEntry).files.add(file)
+ } else if (file.extension == "java") {
+ val psi = psiManager.findFile(file) as? PsiJavaFile ?: return
+ psi.classes.flatMap { listOf(it) + it.allInnerClasses }.forEach { psiClass ->
+ getOrPut(psiClass.name ?: return@forEach, ::ShortNameEntry).files.add(file)
+ }
+ }
+ }
+
+ (classpathRoots + javaModuleRoots + sourcesRoots).forEach { root ->
+ root.children.forEach { index(it, "") }
+ }
+ }
+
+ operator fun get(shortName: String): Set<PsiClass> {
+ val entry = entries[shortName] ?: return emptySet()
+ return entry.resolve(psiManager, shortName)
+ }
+
+ private class ShortNameEntry {
+ var files = mutableListOf<VirtualFile>()
+ private var classes: Set<PsiClass>? = null
+
+ fun resolve(psiManager: PsiManager, shortName: String): Set<PsiClass> {
+ return classes ?: resolveClasses(psiManager, shortName)
+ }
+
+ private fun resolveClasses(psiManager: PsiManager, shortName: String): Set<PsiClass> {
+ val result = files.flatMap { file ->
+ if (file.extension == "java" && file.nameWithoutExtension != shortName) {
+ val psi = psiManager.findFile(file) as? PsiJavaFile ?: return@flatMap emptyList()
+ psi.classes.flatMap { sequenceOf(it) + it.allInnerClasses.asIterable() }
+ .filter { it.qualifiedName?.endsWith(shortName) == true }
+ } else if ('$' in file.name) {
+ val className = file.nameWithoutExtension.replace('$', '.')
+ val outerName = className.substringBefore(".")
+ val outerFile = file.parent.findChild("$outerName.class") ?: return@flatMap emptyList()
+ val outerPsi = psiManager.findFile(outerFile) as? PsiJavaFile ?: return@flatMap emptyList()
+ outerPsi.classes.flatMap { it.allInnerClasses.asIterable() }
+ .filter { it.qualifiedName?.endsWith(className) == true }
+ } else {
+ (psiManager.findFile(file) as? PsiJavaFile)?.classes?.asIterable() ?: emptyList()
+ }
+ }.filter { it.hasModifier(JvmModifier.PUBLIC) }.toSet()
+ classes = result
+ return result
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt b/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt
index 67b065e..7fbec6f 100644
--- a/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt
+++ b/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt
@@ -17,7 +17,6 @@ import org.jetbrains.kotlin.com.intellij.mock.MockProject
import org.jetbrains.kotlin.com.intellij.openapi.Disposable
import org.jetbrains.kotlin.com.intellij.openapi.extensions.ExtensionPoint
import org.jetbrains.kotlin.com.intellij.openapi.extensions.Extensions
-import org.jetbrains.kotlin.com.intellij.openapi.project.Project
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems
import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager
@@ -34,6 +33,7 @@ import java.io.InputStreamReader
import java.lang.Exception
import java.nio.charset.StandardCharsets
import java.nio.file.Files
+import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.*
import kotlin.system.exitProcess
@@ -42,20 +42,27 @@ class Transformer(private val map: MappingSet) {
var classpath: Array<String>? = null
var remappedClasspath: Array<String>? = null
var patternAnnotation: String? = null
+ var manageImports = false
@Throws(IOException::class)
fun remap(sources: Map<String, String>): Map<String, Pair<String, List<Pair<Int, String>>>> =
remap(sources, emptyMap())
@Throws(IOException::class)
- fun remap(sources: Map<String, String>, processedSource: Map<String, String>): Map<String, Pair<String, List<Pair<Int, String>>>> {
+ fun remap(sources: Map<String, String>, processedSources: Map<String, String>): Map<String, Pair<String, List<Pair<Int, String>>>> {
val tmpDir = Files.createTempDirectory("remap")
+ val processedTmpDir = Files.createTempDirectory("remap-processed")
val disposable = Disposer.newDisposable()
try {
for ((unitName, source) in sources) {
val path = tmpDir.resolve(unitName)
Files.createDirectories(path.parent)
Files.write(path, source.toByteArray(StandardCharsets.UTF_8), StandardOpenOption.CREATE)
+
+ val processedSource = processedSources[unitName] ?: source
+ val processedPath = processedTmpDir.resolve(unitName)
+ Files.createDirectories(processedPath.parent)
+ Files.write(processedPath, processedSource.toByteArray(), StandardOpenOption.CREATE)
}
val config = CompilerConfiguration()
@@ -93,7 +100,9 @@ class Transformer(private val map: MappingSet) {
analyze1620(environment, ktFiles)
}
- val remappedProject = remappedClasspath?.let { setupRemappedProject(disposable, it) }
+ val remappedEnv = remappedClasspath?.let {
+ setupRemappedProject(disposable, it, processedTmpDir)
+ }
val patterns = patternAnnotation?.let { annotationFQN ->
val patterns = PsiPatterns(annotationFQN)
@@ -103,7 +112,7 @@ class Transformer(private val map: MappingSet) {
try {
val patternFile = vfs.findFileByIoFile(tmpDir.resolve(unitName).toFile())!!
val patternPsiFile = psiManager.findFile(patternFile)!!
- patterns.read(patternPsiFile, processedSource[unitName]!!)
+ patterns.read(patternPsiFile, processedSources[unitName]!!)
} catch (e: Exception) {
throw RuntimeException("Failed to read patterns from file \"$unitName\".", e)
}
@@ -111,29 +120,45 @@ class Transformer(private val map: MappingSet) {
patterns
}
+ val autoImports = if (manageImports && remappedEnv != null) {
+ AutoImports(remappedEnv)
+ } else {
+ null
+ }
+
val results = HashMap<String, Pair<String, List<Pair<Int, String>>>>()
for (name in sources.keys) {
val file = vfs.findFileByIoFile(tmpDir.resolve(name).toFile())!!
val psiFile = psiManager.findFile(file)!!
- val mapped = try {
- PsiMapper(map, remappedProject, psiFile, analysis.bindingContext, patterns).remapFile()
+ var (text, errors) = try {
+ PsiMapper(map, remappedEnv?.project, psiFile, analysis.bindingContext, patterns).remapFile()
} catch (e: Exception) {
throw RuntimeException("Failed to map file \"$name\".", e)
}
- results[name] = mapped
+
+ if (autoImports != null && "/* remap: no-manage-imports */" !in text) {
+ val processedText = processedSources[name] ?: text
+ text = autoImports.apply(psiFile, text, processedText)
+ }
+
+ results[name] = text to errors
}
return results
} finally {
- Files.walk(tmpDir).map<File> { it.toFile() }.sorted(Comparator.reverseOrder()).forEach { it.delete() }
+ Files.walk(tmpDir).sorted(Comparator.reverseOrder()).forEach { Files.delete(it) }
+ Files.walk(processedTmpDir).sorted(Comparator.reverseOrder()).forEach { Files.delete(it) }
Disposer.dispose(disposable)
}
}
- private fun setupRemappedProject(disposable: Disposable, classpath: Array<String>): Project {
+ private fun setupRemappedProject(disposable: Disposable, classpath: Array<String>, sourceRoot: Path): KotlinCoreEnvironment {
val config = CompilerConfiguration()
config.put(CommonConfigurationKeys.MODULE_NAME, "main")
config.addAll(CLIConfigurationKeys.CONTENT_ROOTS, classpath.map { JvmClasspathRoot(File(it)) })
+ if (manageImports) {
+ config.add(CLIConfigurationKeys.CONTENT_ROOTS, JavaSourceRoot(sourceRoot.toFile(), ""))
+ }
config.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, PrintingMessageCollector(System.err, MessageRenderer.GRADLE_STYLE, true))
val environment = KotlinCoreEnvironment.createForProduction(
@@ -146,7 +171,7 @@ class Transformer(private val map: MappingSet) {
} catch (e: NoSuchMethodError) {
analyze1620(environment, emptyList())
}
- return environment.project
+ return environment
}
companion object {
diff --git a/src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImports.kt b/src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImports.kt
new file mode 100644
index 0000000..f41767f
--- /dev/null
+++ b/src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImports.kt
@@ -0,0 +1,344 @@
+package com.replaymod.gradle.remap.imports
+
+import com.replaymod.gradle.remap.util.TestData
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Test
+
+class TestJavaAutoImports {
+ @Test
+ fun `should remove unused imports`() {
+ TestData.remap("""
+ package test;
+
+ import java.util.ArrayList;
+
+ class Test {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+
+
+ class Test {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should add unambiguous missing imports from JDK`() {
+ TestData.remap("""
+ package test;
+
+
+
+ class Test extends ArrayList {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.util.ArrayList;
+
+ class Test extends ArrayList {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should add unambiguous missing imports from remapped classpath`() {
+ TestData.remap("""
+ package test;
+
+
+
+ class Test extends B {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import b.pkg.B;
+
+ class Test extends B {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not add unambiguous missing imports from original classpath`() {
+ TestData.remap("""
+ package test;
+
+
+
+ class Test extends A {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+
+
+ class Test extends A {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should add unambiguous missing imports from the same source set`() {
+ val original = """
+ package test;
+
+
+
+ class Test extends Sample {
+ }
+ """.trimIndent()
+ TestData.transformer.remap(mapOf(
+ "test.java" to original,
+ "my/Sample.java" to "package my; public class Sample {}"
+ ))["test.java"]!!.first shouldBe """
+ package test;
+
+ import my.Sample;
+
+ class Test extends Sample {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should preserve existing ambiguous imports`() {
+ TestData.remap("""
+ package test;
+
+ import java.awt.List;
+
+ class Test implements List {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.awt.List;
+
+ class Test implements List {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should add new imports in empty lines`() {
+ TestData.remap("""
+ package test;
+
+
+
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.io.Closeable;
+ import java.util.ArrayList;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should add new imports in place of removed imports`() {
+ TestData.remap("""
+ package test;
+
+ import test.Unused1;
+ import test.Unused2;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.io.Closeable;
+ import java.util.ArrayList;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `preserves star imports`() {
+ TestData.remap("""
+ package test;
+
+ import test.Unused1;
+ import java.util.*;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.io.Closeable;
+ import java.util.*;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not import classes from the current package`() {
+ TestData.remap("""
+ package b.pkg;
+
+ class Test extends B implements BInterface {
+ }
+ """.trimIndent()) shouldBe """
+ package b.pkg;
+
+ class Test extends B implements BInterface {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not import self`() {
+ TestData.remap("test/Test.java", """
+ package test;
+
+ public class Test {
+ Test inner;
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ public class Test {
+ Test inner;
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not import java-lang package`() {
+ TestData.remap("test/Test.java", """
+ package test;
+
+ public class Test {
+ Object inner;
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ public class Test {
+ Object inner;
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not import own inner classes`() {
+ TestData.remap("""
+ package test;
+
+ class Test {
+ TestInner inner;
+ public class TestInner {}
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ class Test {
+ TestInner inner;
+ public class TestInner {}
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not import generic types`() {
+ TestData.remap("""
+ package test;
+
+ class Test<B> {
+ B inner;
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ class Test<B> {
+ B inner;
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not import variable references`() {
+ TestData.remap("""
+ package test;
+
+ class Test {
+ Object B;
+ { B = null; }
+ { Object BParent; BParent = B; }
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ class Test {
+ Object B;
+ { B = null; }
+ { Object BParent; BParent = B; }
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not import types that look like fields`() {
+ val content = """
+ package test;
+
+ import a.pkg.A;
+
+ class Test extends A {
+ { conflictingField.method(); }
+ }
+ """.trimIndent()
+ TestData.transformer.remap(mapOf(
+ "test/Test.java" to content,
+ "c/conflictingField.java" to "package c; public class conflictingField {}"
+ ))["test/Test.java"]!!.first shouldBe """
+ package test;
+
+ import b.pkg.B;
+
+ class Test extends B {
+ { conflictingField.method(); }
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should not touch static imports (yet)`() {
+ TestData.remap("""
+ package test;
+
+ import test.Unused1;
+ import test.Unused2;
+
+ import static test.Unused.method;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.io.Closeable;
+ import java.util.ArrayList;
+
+ import static test.Unused.method;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()
+ }
+} \ No newline at end of file
diff --git a/src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImportsFormatting.kt b/src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImportsFormatting.kt
new file mode 100644
index 0000000..848eff8
--- /dev/null
+++ b/src/test/kotlin/com/replaymod/gradle/remap/imports/TestJavaAutoImportsFormatting.kt
@@ -0,0 +1,142 @@
+package com.replaymod.gradle.remap.imports
+
+import com.replaymod.gradle.remap.util.TestData
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Test
+
+class TestJavaAutoImportsFormatting {
+ @Test
+ fun `should separate java(x) from other imports with an empty line if possible`() {
+ TestData.remap("""
+ package test;
+
+
+
+
+
+
+ class Test extends ArrayList implements Closeable, BInterface {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import b.pkg.BInterface;
+
+ import java.io.Closeable;
+ import java.util.ArrayList;
+
+ class Test extends ArrayList implements Closeable, BInterface {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should put new imports in single line if necessary to preserve original line count`() {
+ TestData.remap("""
+ package test;
+
+ import test.Unused1;
+ import test.Unused2;
+
+ class Test extends ArrayList implements Closeable, BInterface {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import b.pkg.BInterface;
+ import java.io.Closeable; import java.util.ArrayList;
+
+ class Test extends ArrayList implements Closeable, BInterface {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should always leave line after imports`() {
+ TestData.remap("""
+ package test;
+
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+ import java.io.Closeable; import java.util.ArrayList;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should put imports in same line as package if required`() {
+ TestData.remap("""
+ package test;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()) shouldBe """
+ package test; import java.io.Closeable; import java.util.ArrayList;
+
+ class Test extends ArrayList implements Closeable {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should remove unused imports from shared lines`() {
+ TestData.remap("""
+ package test;
+
+ import java.io.Closeable; import java.util.ArrayList;
+
+ class Test {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+
+
+ class Test {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should remove unused imports from end of shared lines`() {
+ TestData.remap("""
+ package test;
+
+ import java.io.Closeable; import java.util.ArrayList;
+
+ class Test implements Closeable {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.io.Closeable;
+
+ class Test implements Closeable {
+ }
+ """.trimIndent()
+ }
+
+ @Test
+ fun `should remove unused imports from start of shared lines`() {
+ TestData.remap("""
+ package test;
+
+ import java.io.Closeable; import java.util.ArrayList;
+
+ class Test extends ArrayList {
+ }
+ """.trimIndent()) shouldBe """
+ package test;
+
+ import java.util.ArrayList;
+
+ class Test extends ArrayList {
+ }
+ """.trimIndent()
+ }
+} \ No newline at end of file
diff --git a/src/test/kotlin/com/replaymod/gradle/remap/imports/TestKotlinAutoImports.kt b/src/test/kotlin/com/replaymod/gradle/remap/imports/TestKotlinAutoImports.kt
new file mode 100644
index 0000000..58e2a02
--- /dev/null
+++ b/src/test/kotlin/com/replaymod/gradle/remap/imports/TestKotlinAutoImports.kt
@@ -0,0 +1,26 @@
+package com.replaymod.gradle.remap.imports
+
+import com.replaymod.gradle.remap.util.TestData
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Test
+
+class TestKotlinAutoImports {
+ @Test
+ fun `should not touch Kotlin files (yet)`() {
+ TestData.remap("test.kt", """
+ package test
+
+ import java.util.ArrayList
+
+ class Test {
+ }
+ """.trimIndent()) shouldBe """
+ package test
+
+ import java.util.ArrayList
+
+ class Test {
+ }
+ """.trimIndent()
+ }
+} \ No newline at end of file
diff --git a/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinAnnotation.kt b/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinAnnotation.kt
index bc7cd8f..729b00d 100644
--- a/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinAnnotation.kt
+++ b/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinAnnotation.kt
@@ -8,9 +8,11 @@ class TestMixinAnnotation {
@Test
fun `remaps with class target`() {
TestData.remap("""
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(a.pkg.A.class)
class MixinA { @Shadow private int aField; }
""".trimIndent()) shouldBe """
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(b.pkg.B.class)
class MixinA { @Shadow private int bField; }
""".trimIndent()
@@ -19,9 +21,11 @@ class TestMixinAnnotation {
@Test
fun `remaps with string target`() {
TestData.remap("""
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(targets = "a.pkg.A")
class MixinA { @Shadow private int aField; }
""".trimIndent()) shouldBe """
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(targets = "b.pkg.B")
class MixinA { @Shadow private int bField; }
""".trimIndent()
@@ -31,9 +35,11 @@ class TestMixinAnnotation {
fun `remaps with inner class string target separated by dot`() {
// FIXME should probably keep the dot?
TestData.remap("""
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(targets = "a.pkg.A.Inner")
class MixinA { @Shadow private int aField; }
""".trimIndent()) shouldBe """
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(targets = "b.pkg.B${'$'}Inner")
class MixinA { @Shadow private int bField; }
""".trimIndent()
@@ -42,9 +48,11 @@ class TestMixinAnnotation {
@Test
fun `remaps with inner class string target separated by dollar`() {
TestData.remap("""
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(targets = "a.pkg.A${'$'}Inner")
class MixinA { @Shadow private int aField; }
""".trimIndent()) shouldBe """
+ import org.spongepowered.asm.mixin.Shadow;
@org.spongepowered.asm.mixin.Mixin(targets = "b.pkg.B${'$'}Inner")
class MixinA { @Shadow private int bField; }
""".trimIndent()
diff --git a/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinInjections.kt b/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinInjections.kt
index da68496..65fbc69 100644
--- a/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinInjections.kt
+++ b/src/test/kotlin/com/replaymod/gradle/remap/mapper/mixin/TestMixinInjections.kt
@@ -181,16 +181,14 @@ class TestMixinInjections {
@Test
fun `remaps @At target`() {
TestData.remap("""
- import org.spongepowered.asm.mixin.injection.At;
- import org.spongepowered.asm.mixin.injection.Inject;
+ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject;
@org.spongepowered.asm.mixin.Mixin(a.pkg.A.class)
class MixinA {
@Inject(method = "aMethod", at = @At(target = "La/pkg/A;aInterfaceMethod()V"))
private void test() {}
}
""".trimIndent()) shouldBe """
- import org.spongepowered.asm.mixin.injection.At;
- import org.spongepowered.asm.mixin.injection.Inject;
+ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject;
@org.spongepowered.asm.mixin.Mixin(b.pkg.B.class)
class MixinA {
@Inject(method = "bMethod", at = @At(target = "Lb/pkg/B;bInterfaceMethod()V"))
@@ -202,16 +200,14 @@ class TestMixinInjections {
@Test
fun `remaps @At target without mappings for target`() {
TestData.remap("""
- import org.spongepowered.asm.mixin.injection.At;
- import org.spongepowered.asm.mixin.injection.Inject;
+ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject;
@org.spongepowered.asm.mixin.Mixin(a.pkg.A.class)
class MixinA {
@Inject(method = "aMethod", at = @At(target = "La/pkg/A;unmappedOverloaded(La/pkg/A;)V"))
private void test() {}
}
""".trimIndent()) shouldBe """
- import org.spongepowered.asm.mixin.injection.At;
- import org.spongepowered.asm.mixin.injection.Inject;
+ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject;
@org.spongepowered.asm.mixin.Mixin(b.pkg.B.class)
class MixinA {
@Inject(method = "bMethod", at = @At(target = "Lb/pkg/B;unmappedOverloaded(Lb/pkg/B;)V"))
@@ -223,8 +219,7 @@ class TestMixinInjections {
@Test
fun `remaps @At target in constant`() {
TestData.remap("""
- import org.spongepowered.asm.mixin.injection.At;
- import org.spongepowered.asm.mixin.injection.Inject;
+ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject;
@org.spongepowered.asm.mixin.Mixin(a.pkg.A.class)
class MixinA {
private static final String TARGET = "La/pkg/A;aInterfaceMethod()V";
@@ -234,8 +229,7 @@ class TestMixinInjections {
private void test2() {}
}
""".trimIndent()) shouldBe """
- import org.spongepowered.asm.mixin.injection.At;
- import org.spongepowered.asm.mixin.injection.Inject;
+ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject;
@org.spongepowered.asm.mixin.Mixin(b.pkg.B.class)
class MixinA {
private static final String TARGET = "Lb/pkg/B;bInterfaceMethod()V";
diff --git a/src/test/kotlin/com/replaymod/gradle/remap/util/TestData.kt b/src/test/kotlin/com/replaymod/gradle/remap/util/TestData.kt
index c5b8851..d1ed905 100644
--- a/src/test/kotlin/com/replaymod/gradle/remap/util/TestData.kt
+++ b/src/test/kotlin/com/replaymod/gradle/remap/util/TestData.kt
@@ -48,14 +48,21 @@ object TestData {
findClasspathEntry("BMarkerKt"),
)
patternAnnotation = "remap.Pattern"
+ manageImports = true
}
- fun remap(content: String, patternsBefore: String = "", patternsAfter: String = ""): String = transformer.remap(mapOf(
- "test.java" to content,
+ fun remap(content: String): String =
+ remap("test.java", content)
+ fun remap(fileName: String, content: String): String =
+ remap(fileName, content, "", "")
+ fun remap(content: String, patternsBefore: String, patternsAfter: String): String =
+ remap("test.java", content, patternsBefore, patternsAfter)
+ fun remap(fileName: String, content: String, patternsBefore: String, patternsAfter: String): String = transformer.remap(mapOf(
+ fileName to content,
"pattern.java" to "class Patterns {\n$patternsBefore\n}",
), mapOf(
"pattern.java" to "class Patterns {\n$patternsAfter\n}",
- ))["test.java"]!!.first
+ ))[fileName]!!.first
fun remapWithErrors(content: String) = transformer.remap(mapOf("test.java" to content))["test.java"]!!
fun remapKt(content: String): String = transformer.remap(mapOf("test.kt" to content))["test.kt"]!!.first