Repository: GradleUp/shadow (kotlin)

Issue:
Relocation does not work properly for some string constants
### Expected and Results

When attempting to shade and relocate a class that has string constants of descriptors & signatures for fields & methods, the constants are only partially relocated.

For the examples below, assume a shadowJar task of `shadowJar { relocate 'org.package', 'shadow.org.package' }`

Here are some examples of string constants that are or are not relocated properly
- `Lorg/package/ClassA;` is properly relocated to `Lshadow/org/package/ClassA;`
- `Lorg/package/ClassA;Lorg/package/ClassB;` fails to relocate `ClassB` resulting in `Lshadow/org/package/ClassA;Lorg/package/ClassB;`
- `Ljava/lang/Object;Lorg/package/ClassB;` fails to relocate `ClassB`
- `(Lorg/package/ClassA;Lorg/package/ClassB;)` fails to relocate both `ClassA` and `ClassB`
- `()Lorg/package/ClassA;Lorg/package/ClassB;` fails to relocate both `ClassA` and `ClassB`

I could list a lot of similar examples, but you probably get the point by now.

### Related environent and versions

**Environment**
- Shadow Gradle Plugin `9.0.0-beta12`
- Gradle `8.14-rc-1`
- JDK `GraalVM CE 24`
- IDE `IntelliJ Community 2025.1`
- OS `Windows 11`

### Reproduction steps

1. Add `org.mozilla:rhino:1.7.15` as a shaded dependency in gradle
2. Configure the shadowJar task for relocation `relocate 'org.mozilla', 'shadow.org.mozilla'`
3. Build the jar
4. Open `shadow.org.mozilla.javascript.optimizer.BodyCodegen.class` from the built jar and notice the many string constants that failed to relocate properly

### Anything else?

I tried using an `include` when relocating in hopes that using regex would be able to catch these cases, but from my understanding the relocate task only matches the start of a string. 

I also found that using  `relocate '()Lorg/mozilla', 'Lshadow/org.mozilla'` does catch stuff like `()Lorg/mozilla/javascript/Context;`, but it still only works for the first class in the case of multiple `org.mozilla` classes being present in the signature.

Similarly, I found that trying to use `relocate '(Lorg/mozilla', 'Lshadow/org.mozilla'` fails to build due to a regex parser failing, but I find it strange that the `()` in `()Lorg/mozilla` is treated as a string literal instead of a capturing group. From what I tested, regex does not seem to work with relocation other than `include`, `exclude`, and maybe any other closure parameters.

Pull request:
Closes #1403.

---

- [x] [CHANGELOG](https://github.com/GradleUp/shadow/blob/main/docs/changes/README.md)'s "Unreleased" section has been updated, if applicable.

Patch:
diff --git a/docs/changes/README.md b/docs/changes/README.md
index 2a417fbae..692d3477c 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -7,6 +7,7 @@
 
 - Support relocating Groovy extensions in Module descriptors. ([#1705](https://github.com/GradleUp/shadow/pull/1705))
 - Add extensions for `Iterable<Relocator>`. ([#1710](https://github.com/GradleUp/shadow/pull/1710))
+- Support relocating list of types in `RelocatorRemapper`. ([#1714](https://github.com/GradleUp/shadow/pull/1714))
 
 ### Changed
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt
index 61f824852..922cb4709 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt
@@ -16,7 +16,6 @@ internal class RelocatorRemapper(
   private val relocators: Set<Relocator>,
   private val onModified: () -> Unit = {},
 ) : Remapper() {
-  private val classPattern: Pattern = Pattern.compile("(\\[*)?L(.+)")
 
   override fun mapValue(value: Any): Any {
     return if (value is String) {
@@ -29,7 +28,11 @@ internal class RelocatorRemapper(
   override fun map(internalName: String): String = mapName(internalName)
 
   private fun mapName(name: String, mapLiterals: Boolean = false): String {
-    val newName = mapNameImpl(name, mapLiterals)
+    // Maybe a list of types.
+    val newName = name.split(';').joinToString(";") {
+      mapNameImpl(it, mapLiterals)
+    }
+
     if (newName != name) {
       onModified()
     }
@@ -60,4 +63,8 @@ internal class RelocatorRemapper(
 
     return name
   }
+
+  private companion object {
+    val classPattern: Pattern = Pattern.compile("([\\[()]*)?L([^;]+);?")
+  }
 }


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt
index fc466d1a8..b4a56bc68 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt
@@ -533,6 +533,45 @@ class RelocationTest : BasePluginTest() {
     }
   }
 
+  @Issue(
+    "https://github.com/GradleUp/shadow/issues/1403",
+  )
+  @Test
+  fun relocateMultiClassSignatureStringConstants() {
+    writeClass {
+      """
+        package my;
+        public class Main {
+          public static void main(String[] args) {
+            System.out.println("Lorg/package/ClassA;Lorg/package/ClassB;");
+            System.out.println("(Lorg/package/ClassC;Lorg/package/ClassD;)");
+            System.out.println("()Lorg/package/ClassE;Lorg/package/ClassF;");
+          }
+        }
+      """.trimIndent()
+    }
+    projectScript.appendText(
+      """
+        $shadowJarTask {
+          manifest {
+            attributes '$mainClassAttributeKey': 'my.Main'
+          }
+          relocate('org.package', 'shadow.org.package')
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarPath)
+    val result = runProcess("java", "-jar", outputShadowedJar.use { it.toString() })
+
+    // Just check that the jar can be executed without NoClassDefFoundError.
+    assertThat(result).contains(
+      "Lshadow/org/package/ClassA;Lshadow/org/package/ClassB",
+      "Lshadow/org/package/ClassC;Lshadow/org/package/ClassD",
+      "Lshadow/org/package/ClassE;Lshadow/org/package/ClassF",
+    )
+  }
+
   @Test
   fun classBytesUnchangedIfPossible() {
     val mainClassEntry = writeClass()
diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperTest.kt
new file mode 100644
index 000000000..7bd10f1c6
--- /dev/null
+++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperTest.kt
@@ -0,0 +1,41 @@
+package com.github.jengelman.gradle.plugins.shadow.relocation
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import com.github.jengelman.gradle.plugins.shadow.internal.RelocatorRemapper
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+
+class RelocatorRemapperTest {
+  @ParameterizedTest
+  @MethodSource("classSignatureStringConstants")
+  fun relocateSignaturePatterns(input: String, expected: String) {
+    val relocator = RelocatorRemapper(
+      relocators = setOf(
+        SimpleRelocator("org.package", "shadow.org.package"),
+      ),
+    )
+    assertThat(relocator.map(input)).isEqualTo(expected)
+  }
+
+  private companion object {
+    @JvmStatic
+    fun classSignatureStringConstants() = listOf(
+      // Normal class.
+      Arguments.of("Lorg/package/ClassA;", "Lshadow/org/package/ClassA;"),
+      // Array class.
+      Arguments.of("[Lorg/package/ClassA;", "[Lshadow/org/package/ClassA;"),
+      // Multidimensional array of class.
+      Arguments.of("[[Lorg/package/ClassA;", "[[Lshadow/org/package/ClassA;"),
+      // Multiple classes.
+      Arguments.of("Lorg/package/ClassA;Lorg/package/ClassB;", "Lshadow/org/package/ClassA;Lshadow/org/package/ClassB;"),
+      // Multiple classes.
+      Arguments.of("Ljava/lang/Object;Lorg/package/ClassB;", "Ljava/lang/Object;Lshadow/org/package/ClassB;"),
+      // Method arguments.
+      Arguments.of("(Lorg/package/ClassA;Lorg/package/ClassB;)", "(Lshadow/org/package/ClassA;Lshadow/org/package/ClassB;)"),
+      // Method return types.
+      Arguments.of("()Lorg/package/ClassA;Lorg/package/ClassB;", "()Lshadow/org/package/ClassA;Lshadow/org/package/ClassB;"),
+    )
+  }
+}