Repository: GradleUp/shadow (kotlin)

Issue:
Local submodules are not excluded when projects sets version with a `+`
I have noticed that excludes for local submodule dependencies are not working when version is set and it contains `+`. I noticed that there is a regex matching which is probably confused by that character. Here is [a sample project] (https://github.com/chali/shadow-jar-project-filtering) Running `./gradlew shadowJar` and opening `app/build/libs/app-0.7.0+1-all.jar` will show that it contains a class from module `common` which has been excluded. When a version without `+` is used everything works as expected.

### Shadow Version

Shadow version `7.0.0`

### Gradle Version

Gradle version `7.0.1`

### Expected Behavior

Local submodule project dependency should be filtered out.

### Actual Behavior

Classes from local submodule dependency are present.

### Gradle Build Script(s)

The interesting part of `build.gradle`

```
subprojects {
    apply plugin: 'java-library'

    version = "0.7.0+1"
}
```

The interesting part of `app/build.gradle`

```
dependencies {
    implementation(project(":common"))
}

shadowJar {
    dependencies {
        exclude(project(":common"))
    }
}
```

### Content of Shadow JAR

```
jar tf app/build/libs/app-0.7.0+1-all.jar
META-INF/
META-INF/MANIFEST.MF
Hello.class
```

Pull request:
Closes #671.

---

- [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 0b8a9ff86..5cc15ea96 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -15,6 +15,10 @@
   The Gradle Module descriptors (`org.codehaus.groovy.runtime.ExtensionModule` files) defined under `META-INF/services/`
   and `META-INF/groovy` will be merged into `META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule`.
 
+### Fixed
+
+- Fix excluding dependencies whose versions contain `+`. ([#1597](https://github.com/GradleUp/shadow/pull/1597))
+
 ## [9.1.0](https://github.com/GradleUp/shadow/compare/9.1.0) - 2025-08-29
 
 ### Added
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt
index 43f4677c6..8a0d7a683 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt
@@ -109,9 +109,19 @@ public interface DependencyFilter : Serializable {
 
     private fun Dependency.toSpec(): Spec<ResolvedDependency> {
       return Spec<ResolvedDependency> { resolvedDependency ->
-        (group == null || resolvedDependency.moduleGroup.matches(group!!.toRegex())) &&
-          resolvedDependency.moduleName.matches(name.toRegex()) &&
-          (version == null || resolvedDependency.moduleVersion.matches(version!!.toRegex()))
+        val groupMatch = group?.let {
+          it == resolvedDependency.moduleGroup || resolvedDependency.moduleGroup.matches(it.toRegex())
+        } ?: true
+        val nameMatch = name.let {
+          it == resolvedDependency.moduleName || resolvedDependency.moduleName.matches(it.toRegex())
+        }
+        val versionMatch = version?.let {
+          // Version like `1.0.0+1` can't be converted to regex directly because `+` is a special character in regex.
+          // So we check for exact match first, then fallback to regex match.
+          it == resolvedDependency.moduleVersion || resolvedDependency.moduleVersion.matches(it.toRegex())
+        } ?: true
+
+        groupMatch && nameMatch && versionMatch
       }
     }
   }


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt
index be0b10090..062259a00 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt
@@ -1,6 +1,7 @@
 package com.github.jengelman.gradle.plugins.shadow
 
 import assertk.assertThat
+import com.github.jengelman.gradle.plugins.shadow.util.Issue
 import com.github.jengelman.gradle.plugins.shadow.util.containsOnly
 import kotlin.io.path.appendText
 import kotlin.io.path.writeText
@@ -174,6 +175,32 @@ class FilteringTest : BasePluginTest() {
     }
   }
 
+  @Issue(
+    "https://github.com/GradleUp/shadow/issues/671",
+  )
+  @Test
+  fun filterProjectThatVersionContainsPlus() {
+    writeClientAndServerModules(
+      serverShadowBlock = """
+        dependencies {
+          exclude(project(':client'))
+        }
+      """.trimIndent(),
+    )
+    path("client/build.gradle").appendText("version = '1.0.0+1'")
+
+    run(serverShadowJarPath)
+
+    assertThat(outputServerShadowedJar).useAll {
+      containsOnly(
+        "server/",
+        "server/Server.class",
+        *junitEntries,
+        *manifestEntries,
+      )
+    }
+  }
+
   @Test
   fun excludeTransitiveProjectDependency() {
     writeClientAndServerModules(