Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ dokka { dokkaPublications.html { outputDirectory = rootDir.resolve("docs/api") }
kotlin {
explicitApi()
@OptIn(ExperimentalAbiValidation::class) abiValidation { enabled = true }
val jdkRelease = "17"
compilerOptions {
allWarningsAsErrors = true
// https://docs.gradle.org/current/userguide/compatibility.html#kotlin
apiVersion = KotlinVersion.KOTLIN_2_2
languageVersion = apiVersion
jvmTarget = JvmTarget.fromTarget(jdkRelease)
jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
freeCompilerArgs.add("-Xjdk-release=$jdkRelease")
}
target.compilations.configureEach {
compileJavaTaskProvider { options.release = jdkRelease.toInt() }
}
}

Expand Down Expand Up @@ -132,7 +138,12 @@ dependencies {
}

testing.suites {
getByName<JvmTestSuite>("test") { dependencies { implementation(libs.xmlunit) } }
getByName<JvmTestSuite>("test") {
dependencies {
implementation(libs.xmlunit)
implementation(libs.asm.commons)
}
}
register<JvmTestSuite>("documentTest") {
targets.configureEach {
testTask {
Expand Down Expand Up @@ -216,15 +227,6 @@ kotlin.target.compilations {
}
}

tasks.compileKotlin {
compilerOptions {
jvmTarget = JvmTarget.fromTarget(libs.versions.jdkRelease.get())
freeCompilerArgs.add("-Xjdk-release=${libs.versions.jdkRelease.get()}")
}
}

tasks.compileJava { options.release = libs.versions.jdkRelease.get().toInt() }

tasks.pluginUnderTestMetadata { pluginClasspath.from(testPluginClasspath) }

tasks.check { dependsOn(tasks.withType<Test>()) }
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ minGradle = "9.0.0"
kotlin = "2.3.20"
moshi = "1.15.2"
pluginPublish = "2.1.0"
jdkRelease = "17"

[libraries]
apache-ant = "org.apache.ant:ant:1.10.15"
Expand All @@ -20,6 +19,7 @@ plexus-xml = "org.codehaus.plexus:plexus-xml:4.1.1"
xmlunit = "org.xmlunit:xmlunit-legacy:2.11.0"
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
asm-commons = "org.ow2.asm:asm-commons:9.9.1"

foojayResolver = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.toolchains.foojay-resolver-convention.gradle.plugin:1.0.0"
develocity = "com.gradle:develocity-gradle-plugin:4.3.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator
import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath
import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate
import java.io.File
import java.lang.classfile.Attributes
import java.lang.classfile.ClassFile
import java.lang.classfile.instruction.InvokeInstruction
import java.lang.classfile.instruction.TypeCheckInstruction
import java.nio.file.Path
import kotlin.io.path.copyTo
import kotlin.io.path.createParentDirectories
Expand All @@ -22,6 +18,12 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode

/**
* The cases reflect the cases in
Expand Down Expand Up @@ -62,20 +64,17 @@ class BytecodeRemappingTest {
fun classNameIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
assertThat(classModel.thisClass().asInternalName())
val classNode = result.toClassNode()
assertThat(classNode.name)
.isEqualTo($$"com/example/relocated/BytecodeRemappingTest$FixtureSubject")
}

@Test
fun annotationIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val annotationsAttr = classModel.findAttribute(Attributes.runtimeVisibleAnnotations())
assertThat(annotationsAttr.isPresent).isTrue()
val annotationDescriptors =
annotationsAttr.get().annotations().map { it.className().stringValue() }
val classNode = result.toClassNode()
val annotationDescriptors = classNode.visibleAnnotations.orEmpty().map { it.desc }
assertThat(annotationDescriptors)
.contains($$"Lcom/example/relocated/BytecodeRemappingTest$FixtureAnnotation;")
}
Expand All @@ -88,60 +87,60 @@ class BytecodeRemappingTest {

val result = details.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
assertThat(classModel.thisClass().asInternalName()).isEqualTo(relocatedFixtureBase)
val classNode = result.toClassNode()
assertThat(classNode.name).isEqualTo(relocatedFixtureBase)
}

@Test
fun superclassIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
assertThat(classModel.superclass().get().asInternalName()).isEqualTo(relocatedFixtureBase)
val classNode = result.toClassNode()
assertThat(classNode.superName).isEqualTo(relocatedFixtureBase)
}

@Test
fun fieldDescriptorIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() }
val classNode = result.toClassNode()
val fieldDescriptors = classNode.fields.map { it.desc }
assertThat(fieldDescriptors).contains("L$relocatedFixtureBase;")
}

@Test
fun arrayFieldDescriptorIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() }
val classNode = result.toClassNode()
val fieldDescriptors = classNode.fields.map { it.desc }
assertThat(fieldDescriptors).contains("[L$relocatedFixtureBase;")
}

@Test
fun array2dFieldDescriptorIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() }
val classNode = result.toClassNode()
val fieldDescriptors = classNode.fields.map { it.desc }
assertThat(fieldDescriptors).contains("[[L$relocatedFixtureBase;")
}

@Test
fun methodDescriptorIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val methodDescriptors = classModel.methods().map { it.methodType().stringValue() }
val classNode = result.toClassNode()
val methodDescriptors = classNode.methods.map { it.desc }
assertThat(methodDescriptors).contains("(L$relocatedFixtureBase;)L$relocatedFixtureBase;")
}

@Test
fun methodMultipleArgsIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val methodDescriptors = classModel.methods().map { it.methodType().stringValue() }
val classNode = result.toClassNode()
val methodDescriptors = classNode.methods.map { it.desc }
assertThat(methodDescriptors)
.contains("(L$relocatedFixtureBase;L$relocatedFixtureBase;)L$relocatedFixtureBase;")
}
Expand All @@ -151,8 +150,8 @@ class BytecodeRemappingTest {
fun primitivePlusClassMethodIsRelocated(primitiveDescriptor: Char) {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val methodDescriptors = classModel.methods().map { it.methodType().stringValue() }
val classNode = result.toClassNode()
val methodDescriptors = classNode.methods.map { it.desc }
assertThat(methodDescriptors)
.contains("(${primitiveDescriptor}L$relocatedFixtureBase;)L$relocatedFixtureBase;")
}
Expand All @@ -161,12 +160,8 @@ class BytecodeRemappingTest {
fun stringConstantIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
// Find the constant string in the bytecode.
val stringConstants =
classModel.constantPool().mapNotNull { entry ->
if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null
}
val classNode = result.toClassNode()
val stringConstants = classNode.allStringConstants()
assertThat(stringConstants)
.contains($$"com.example.relocated.BytecodeRemappingTest$FixtureBase")
}
Expand All @@ -183,11 +178,8 @@ class BytecodeRemappingTest {
)
val result = fixtureSubjectDetails.remapClass(skipRelocators)

val classModel = ClassFile.of().parse(result)
val stringConstants =
classModel.constantPool().mapNotNull { entry ->
if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null
}
val classNode = result.toClassNode()
val stringConstants = classNode.allStringConstants()
assertThat(stringConstants)
.doesNotContain($$"com.example.relocated.BytecodeRemappingTest$FixtureBase")
}
Expand All @@ -196,11 +188,8 @@ class BytecodeRemappingTest {
fun multiClassDescriptorStringConstantIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val stringConstants =
classModel.constantPool().mapNotNull { entry ->
if (entry is java.lang.classfile.constantpool.StringEntry) entry.stringValue() else null
}
val classNode = result.toClassNode()
val stringConstants = classNode.allStringConstants()
// Verify that two adjacent class references in a single string constant are both relocated
// (regression test for the issue-1403 pattern).
assertThat(stringConstants)
Expand All @@ -213,59 +202,112 @@ class BytecodeRemappingTest {
fun interfaceIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val interfaces = classModel.interfaces().map { it.asInternalName() }
assertThat(interfaces)
val classNode = result.toClassNode()
assertThat(classNode.interfaces)
.contains($$"com/example/relocated/BytecodeRemappingTest$FixtureInterface")
}

@Test
fun signatureIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val method = classModel.methods().first { it.methodName().stringValue() == "methodWithGeneric" }
val signatureAttr = method.findAttribute(Attributes.signature())
assertThat(signatureAttr.isPresent).isTrue()
val sig = signatureAttr.get().signature().stringValue()
assertThat(sig).contains("L$relocatedFixtureBase;")
val classNode = result.toClassNode()
val method = classNode.methods.first { it.name == "methodWithGeneric" }
assertThat(method.signature).contains("L$relocatedFixtureBase;")
}

@Test
fun localVariableIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val method = classModel.methods().first { it.methodName().stringValue() == "method" }
val code = method.code().get()
val lvt = code.findAttribute(Attributes.localVariableTable())
assertThat(lvt.isPresent).isTrue()
val descriptors = lvt.get().localVariables().map { it.type().stringValue() }
val classNode = result.toClassNode()
val method = classNode.methods.first { it.name == "method" }
val descriptors = method.localVariables.orEmpty().map { it.desc }
assertThat(descriptors).contains("L$relocatedFixtureBase;")
}

@Test
fun instructionIsRelocated() {
val result = fixtureSubjectDetails.remapClass(relocators)

val classModel = ClassFile.of().parse(result)
val method =
classModel.methods().first { it.methodName().stringValue() == "methodWithCheckCast" }
val code = method.code().get()
val classReader = ClassReader(result)
var hasRelocatedCheckCast = false
var hasRelocatedInvoke = false

classReader.accept(
object : ClassVisitor(Opcodes.ASM9) {
override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?,
): MethodVisitor? {
if (name != "methodWithCheckCast") return null
return object : MethodVisitor(Opcodes.ASM9) {
override fun visitTypeInsn(opcode: Int, type: String) {
if (opcode == Opcodes.CHECKCAST && type == relocatedFixtureBase) {
hasRelocatedCheckCast = true
}
}

override fun visitMethodInsn(
opcode: Int,
owner: String,
name: String,
descriptor: String,
isInterface: Boolean,
) {
if (owner == relocatedFixtureBase) {
hasRelocatedInvoke = true
}
}
}
}
},
0,
)

val hasRelocatedCheckCast =
code.elementStream().anyMatch { element ->
element is TypeCheckInstruction && element.type().asInternalName() == relocatedFixtureBase
}
assertThat(hasRelocatedCheckCast).isTrue()
assertThat(hasRelocatedInvoke).isTrue()
}

val hasRelocatedInvoke =
code.elementStream().anyMatch { element ->
element is InvokeInstruction && element.owner().asInternalName() == relocatedFixtureBase
private fun ClassNode.allStringConstants(): List<String> {
val strings = mutableListOf<String>()
this.accept(
object : ClassVisitor(Opcodes.ASM9) {
override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?,
): MethodVisitor {
return object : MethodVisitor(Opcodes.ASM9) {
override fun visitLdcInsn(value: Any?) {
if (value is String) strings.add(value)
}
}
}

override fun visitField(
access: Int,
name: String,
desc: String,
signature: String?,
value: Any?,
): FieldVisitor? {
if (value is String) strings.add(value)
return null
}
}
assertThat(hasRelocatedInvoke).isTrue()
)
return strings
}

private fun ByteArray.toClassNode(): ClassNode =
ClassNode().also { ClassReader(this).accept(it, 0) }

private fun KClass<*>.toFileCopyDetails() =
object : FileCopyDetails by noOpDelegate() {
private val _path = java.name.replace('.', '/') + ".class"
Expand Down
Loading