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
15 changes: 9 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,12 @@ 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 @@ -222,6 +216,15 @@ 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
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
package com.github.jengelman.gradle.plugins.shadow.internal

import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.doesNotContain
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
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
import kotlin.reflect.KClass
import org.gradle.api.file.FileCopyDetails
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

/**
* The cases reflect the cases in
* [com.github.jengelman.gradle.plugins.shadow.relocation.RelocatorsTest], but operate on the
* bytecode level to verify that the remapper correctly transforms class names in all relevant
* bytecode structures.
*/
class BytecodeRemappingTest {
@TempDir lateinit var tempDir: Path

// Relocator used across all relocation tests: moves the test package to a distinct target.
private val relocators =
setOf(
SimpleRelocator(
"com.github.jengelman.gradle.plugins.shadow.internal",
"com.example.relocated",
)
)

// Internal name of the relocated FixtureBase for use in assertions.
private val relocatedFixtureBase = $$"com/example/relocated/BytecodeRemappingTest$FixtureBase"

private val fixtureSubjectDetails
get() = FixtureSubject::class.toFileCopyDetails()

@Test
fun classNotModified() {
val details = fixtureSubjectDetails
// Relocator pattern does not match – original bytes must be returned as-is.
val noMatchRelocators = setOf(SimpleRelocator("org.unrelated", "org.other"))

val result = details.remapClass(noMatchRelocators)

assertThat(result).isEqualTo(details.file.readBytes())
}

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

val classModel = ClassFile.of().parse(result)
assertThat(classModel.thisClass().asInternalName())
.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() }
assertThat(annotationDescriptors)
.contains($$"Lcom/example/relocated/BytecodeRemappingTest$FixtureAnnotation;")
}

@Test
fun baseClassNameIsRelocated() {
// Verify relocation also works on a simple class (FixtureBase has no fields/methods
// referencing the target package beyond its own class name).
val details = FixtureBase::class.toFileCopyDetails()

val result = details.remapClass(relocators)

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

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

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

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

val classModel = ClassFile.of().parse(result)
val fieldDescriptors = classModel.fields().map { it.fieldType().stringValue() }
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() }
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() }
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() }
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() }
assertThat(methodDescriptors)
.contains("(L$relocatedFixtureBase;L$relocatedFixtureBase;)L$relocatedFixtureBase;")
}

@ParameterizedTest
@ValueSource(chars = ['B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z'])
fun primitivePlusClassMethodIsRelocated(primitiveDescriptor: Char) {
val result = fixtureSubjectDetails.remapClass(relocators)

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

@Test
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
}
assertThat(stringConstants)
.contains($$"com.example.relocated.BytecodeRemappingTest$FixtureBase")
}

@Test
fun stringConstantNotRelocatedWhenSkipEnabled() {
val skipRelocators =
setOf(
SimpleRelocator(
"com.github.jengelman.gradle.plugins.shadow.internal",
"com.example.relocated",
skipStringConstants = true,
)
)
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
}
assertThat(stringConstants)
.doesNotContain($$"com.example.relocated.BytecodeRemappingTest$FixtureBase")
}

@Test
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
}
// Verify that two adjacent class references in a single string constant are both relocated
// (regression test for the issue-1403 pattern).
assertThat(stringConstants)
.contains(
$$"()Lcom/example/relocated/BytecodeRemappingTest$FixtureBase;Lcom/example/relocated/BytecodeRemappingTest$FixtureBase;"
)
}

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

val classModel = ClassFile.of().parse(result)
val interfaces = classModel.interfaces().map { it.asInternalName() }
assertThat(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;")
}

@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() }
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 hasRelocatedCheckCast =
code.elementStream().anyMatch { element ->
element is TypeCheckInstruction && element.type().asInternalName() == relocatedFixtureBase
}
assertThat(hasRelocatedCheckCast).isTrue()

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

private fun KClass<*>.toFileCopyDetails() =
object : FileCopyDetails by noOpDelegate() {
private val _path = java.name.replace('.', '/') + ".class"
private val _file =
tempDir
.resolve(_path)
.createParentDirectories()
.also { requireResourceAsPath(_path).copyTo(it) }
.toFile()

override fun getPath(): String = _path

override fun getFile(): File = _file
}

// ---------------------------------------------------------------------------
// Fixture classes – declared as nested classes so their bytecode is compiled
// into the test output directory and can be fetched via requireResourceAsPath.
// ---------------------------------------------------------------------------

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class FixtureAnnotation

interface FixtureInterface

open class FixtureBase

@Suppress("unused") // Used by parsing bytecode.
@FixtureAnnotation
class FixtureSubject : FixtureBase(), FixtureInterface {
val field: FixtureBase = FixtureBase()
val arrayField: Array<FixtureBase> = emptyArray()
val array2dField: Array<Array<FixtureBase>> = emptyArray()
val stringConstant: String =
$$"com.github.jengelman.gradle.plugins.shadow.internal.BytecodeRemappingTest$FixtureBase"
val multiClassDescriptor: String =
$$"()Lcom/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase;Lcom/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase;"

fun method(arg: FixtureBase): FixtureBase = arg

fun methodMultiArgs(a: FixtureBase, b: FixtureBase): FixtureBase = a

fun methodWithPrimitivePlusClass(b: Byte, arg: FixtureBase): FixtureBase = arg

fun methodWithCharPlusClass(c: Char, arg: FixtureBase): FixtureBase = arg

fun methodWithDoublePlusClass(d: Double, arg: FixtureBase): FixtureBase = arg

fun methodWithFloatPlusClass(f: Float, arg: FixtureBase): FixtureBase = arg

fun methodWithIntPlusClass(i: Int, arg: FixtureBase): FixtureBase = arg

fun methodWithLongPlusClass(l: Long, arg: FixtureBase): FixtureBase = arg

fun methodWithShortPlusClass(s: Short, arg: FixtureBase): FixtureBase = arg

fun methodWithBooleanPlusClass(z: Boolean, arg: FixtureBase): FixtureBase = arg

fun methodWithCheckCast(arg: Any): FixtureBase {
(arg as FixtureBase).toString()
return arg
}

fun methodWithGeneric(list: List<FixtureBase>): FixtureBase = list[0]
}
}