Repository: GradleUp/shadow (kotlin)

Issue:
Support excluding type-safe dependency accessors from dendencies
### Shadow Version
8.1.1

### Gradle Version
8.6

### Expected Behavior
I'm using gradle/libs to share dependencies in my multi-project-build (see https://docs.gradle.org/current/userguide/platforms.html).
I'd like to exclude some dependencies for some sub-projects (see https://imperceptiblethoughts.com/shadow/configuration/dependencies/#filtering-dependencies)... 

### Actual Behavior
... but for example `exclude(dependency(libs.log4j.api))` raises an error: `org.gradle.internal.typeconversion.UnsupportedNotationException: Cannot convert the provided notation to an object of type Dependency: map(valueof(DependencyValueSource)).`
According to the libs-documentation (first link above), the call of `libs.log4j.api` should perform as if it was the plain string-dependency, but obviously, it doesn't.

Any ideas on this? Thanks!

Pull request description:
Closes #923.

---

- [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/api/shadow.api b/api/shadow.api
index ddb51769e..907cb3006 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -153,6 +153,7 @@ public abstract interface class com/github/jengelman/gradle/plugins/shadow/tasks
 	public abstract fun dependency (Lorg/gradle/api/artifacts/Dependency;)Lorg/gradle/api/specs/Spec;
 	public abstract fun exclude (Lorg/gradle/api/specs/Spec;)V
 	public abstract fun include (Lorg/gradle/api/specs/Spec;)V
+	public abstract fun project (Ljava/lang/Object;)Lorg/gradle/api/specs/Spec;
 	public abstract fun project (Ljava/lang/String;)Lorg/gradle/api/specs/Spec;
 	public abstract fun project (Ljava/util/Map;)Lorg/gradle/api/specs/Spec;
 	public abstract fun resolve (Ljava/util/Collection;)Lorg/gradle/api/file/FileCollection;
diff --git a/docs/changes/README.md b/docs/changes/README.md
index e26e172c2..226935ee1 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -6,6 +6,7 @@
 **Added**
 
 - Add Kotlin DSL examples in docs. ([#1306](https://github.com/GradleUp/shadow/pull/1306))
+- Support using type-safe dependency accessors in `ShadowJar.dependencies`. ([#1322](https://github.com/GradleUp/shadow/pull/1322))
 
 **Changed**
 
diff --git a/docs/configuration/dependencies/README.md b/docs/configuration/dependencies/README.md
index 92bf039da..8b8441f57 100644
--- a/docs/configuration/dependencies/README.md
+++ b/docs/configuration/dependencies/README.md
@@ -219,6 +219,44 @@ This same pattern can be used for any of the dependency notation fields.
     }
     ```
 
+### Using type-safe dependency accessors
+
+You can also use type-safe project accessors or version catalog accessors to filter dependencies.
+
+=== "Kotlin"
+
+    ```kotlin
+    dependencies {
+      // Have to declare this dependency in your libs.versions.toml
+      implementation(libs.log4j.core)
+      // Have to enable `TYPESAFE_PROJECT_ACCESSORS` flag in your settings.gradle.kts
+      implementation(projects.api)
+    }
+    tasks.shadowJar {
+      dependencies {
+        exclude(dependency(libs.log4j.core))
+        exclude(project(projects.api))
+      }
+    }
+    ```
+
+=== "Groovy"
+
+    ```groovy
+    dependencies {
+      // Have to declare this dependency in your libs.versions.toml
+      implementation libs.log4j.core
+      // Have to enable `TYPESAFE_PROJECT_ACCESSORS` flag in your settings.gradle
+      implementation projects.api
+    }
+    tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+      dependencies {
+        exclude(dependency(libs.log4j.core))
+        exclude(project(projects.api))
+      }
+    }
+    ```
+
 ### Programmatically Selecting Dependencies to Filter
 
 If more complex decisions are needed to select the dependencies to be included, the
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.kt
index 6709c625c..a81189fef 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.kt
@@ -4,9 +4,11 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.DependencyFilter
 import org.gradle.api.Project
 import org.gradle.api.artifacts.Configuration
 import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.ProjectDependency
 import org.gradle.api.artifacts.ResolvedArtifact
 import org.gradle.api.artifacts.ResolvedDependency
 import org.gradle.api.file.FileCollection
+import org.gradle.api.provider.Provider
 import org.gradle.api.specs.Spec
 
 internal sealed class AbstractDependencyFilter(
@@ -43,6 +45,17 @@ internal sealed class AbstractDependencyFilter(
     includeSpecs.add(spec)
   }
 
+  override fun project(notation: Any): Spec<ResolvedDependency> {
+    @Suppress("UNCHECKED_CAST")
+    return when (notation) {
+      is ProjectDependency -> dependency(notation)
+      is Provider<*> -> project(notation.get() as String)
+      is String -> project(notation)
+      is Map<*, *> -> project(notation as Map<String, *>)
+      else -> error("Unsupported notation type: ${notation::class.java}")
+    }
+  }
+
   override fun project(notation: Map<String, *>): Spec<ResolvedDependency> {
     return dependency(project.dependencies.project(notation))
   }
@@ -52,7 +65,11 @@ internal sealed class AbstractDependencyFilter(
   }
 
   override fun dependency(dependencyNotation: Any): Spec<ResolvedDependency> {
-    return dependency(project.dependencies.create(dependencyNotation))
+    val realNotation = when (dependencyNotation) {
+      is Provider<*> -> dependencyNotation.get()
+      else -> dependencyNotation
+    }
+    return dependency(project.dependencies.create(realNotation))
   }
 
   override fun dependency(dependency: Dependency): Spec<ResolvedDependency> {
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 90d3a2d82..cf74e3b23 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
@@ -29,6 +29,11 @@ public interface DependencyFilter : Serializable {
    */
   public fun include(spec: Spec<ResolvedDependency>)
 
+  /**
+   * Create a [Spec] that matches the provided project [notation].
+   */
+  public fun project(notation: Any): Spec<ResolvedDependency>
+
   /**
    * Create a [Spec] that matches the provided project [notation].
    */


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt
index 8a1b285dd..3128c0fd4 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt
@@ -141,6 +141,7 @@ abstract class BasePluginTest {
       buildCache {
         $buildCacheBlock
       }
+      enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS'
       $endBlock
     """.trimIndent() + System.lineSeparator()
   }
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 0863939d5..ca30840cf 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
@@ -8,6 +8,8 @@ import kotlin.io.path.writeText
 import org.junit.jupiter.api.BeforeAll
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
 
 class FilteringTest : BasePluginTest() {
   @BeforeAll
@@ -60,9 +62,10 @@ class FilteringTest : BasePluginTest() {
     }
   }
 
-  @Test
-  fun excludeDependency() {
-    dependOnAndExcludeArtifactD()
+  @ParameterizedTest
+  @ValueSource(booleans = [false, true])
+  fun excludeDependency(useAccessor: Boolean) {
+    dependOnAndExcludeArtifactD(useAccessor)
 
     run(shadowJarTask)
 
@@ -124,12 +127,14 @@ class FilteringTest : BasePluginTest() {
     }
   }
 
-  @Test
-  fun filterProjectDependencies() {
+  @ParameterizedTest
+  @ValueSource(booleans = [false, true])
+  fun filterProjectDependencies(useAccessor: Boolean) {
+    val clientProject = if (useAccessor) "project(projects.client)" else "project(':client')"
     writeClientAndServerModules(
       serverShadowBlock = """
         dependencies {
-          exclude(project(':client'))
+          exclude($clientProject)
         }
       """.trimIndent(),
     )
@@ -205,15 +210,25 @@ class FilteringTest : BasePluginTest() {
     commonAssertions()
   }
 
-  private fun dependOnAndExcludeArtifactD() {
+  private fun dependOnAndExcludeArtifactD(useAccessor: Boolean = false) {
+    settingsScriptPath.appendText(
+      """
+        dependencyResolutionManagement {
+          versionCatalogs.create('libs') {
+            library('my-d', 'my:d:1.0')
+          }
+        }
+      """.trimIndent(),
+    )
+    val dependency = if (useAccessor) "libs.my.d" else "'my:d:1.0'"
     projectScriptPath.appendText(
       """
         dependencies {
-          implementation 'my:d:1.0'
+          implementation $dependency
         }
         $shadowJar {
           dependencies {
-            exclude(dependency('my:d:1.0'))
+            exclude(dependency($dependency))
           }
         }
       """.trimIndent(),
diff --git a/src/integrationTest/kotlin/com/github/jengelman/gradle/plugins/shadow/snippet/SnippetExecutable.kt b/src/integrationTest/kotlin/com/github/jengelman/gradle/plugins/shadow/snippet/SnippetExecutable.kt
index 311f1305e..17130c574 100644
--- a/src/integrationTest/kotlin/com/github/jengelman/gradle/plugins/shadow/snippet/SnippetExecutable.kt
+++ b/src/integrationTest/kotlin/com/github/jengelman/gradle/plugins/shadow/snippet/SnippetExecutable.kt
@@ -34,8 +34,13 @@ sealed class SnippetExecutable : Executable {
             mavenLocal()
             mavenCentral()
           }
+          versionCatalogs.create('libs') {
+            library('log4j-core', 'org.apache.logging.log4j:log4j-core:2.11.1')
+          }
         }
-        include 'api', 'main'
+        include ':api', ':main'
+        rootProject.name = 'snippet'
+        enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS'
       """.trimIndent(),
     )
     projectRoot.addSubProject("api", pluginsBlock)