diff --git a/projectguard/api/projectguard.api b/projectguard/api/projectguard.api index 821d258..1b749fd 100644 --- a/projectguard/api/projectguard.api +++ b/projectguard/api/projectguard.api @@ -14,14 +14,15 @@ public final class com/rubensousa/projectguard/plugin/GuardRule { public abstract interface class com/rubensousa/projectguard/plugin/GuardScope { public abstract fun applyRule (Lcom/rubensousa/projectguard/plugin/GuardRule;)V + public fun deny (Ljava/lang/String;)V public fun deny (Ljava/lang/String;Lgroovy/lang/Closure;)V public abstract fun deny (Ljava/lang/String;Lorg/gradle/api/Action;)V + public fun deny (Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;)V + public fun deny (Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;Lgroovy/lang/Closure;)V public fun deny (Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;Lorg/gradle/api/Action;)V + public fun deny (Lorg/gradle/api/provider/Provider;)V public fun deny (Lorg/gradle/api/provider/Provider;Lgroovy/lang/Closure;)V public abstract fun deny (Lorg/gradle/api/provider/Provider;Lorg/gradle/api/Action;)V - public static synthetic fun deny$default (Lcom/rubensousa/projectguard/plugin/GuardScope;Ljava/lang/String;Lorg/gradle/api/Action;ILjava/lang/Object;)V - public static synthetic fun deny$default (Lcom/rubensousa/projectguard/plugin/GuardScope;Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;Lorg/gradle/api/Action;ILjava/lang/Object;)V - public static synthetic fun deny$default (Lcom/rubensousa/projectguard/plugin/GuardScope;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/Action;ILjava/lang/Object;)V } public abstract interface class com/rubensousa/projectguard/plugin/ModuleRestrictionScope { diff --git a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/GuardScope.kt b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/GuardScope.kt index e0e2bd9..ff4e536 100644 --- a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/GuardScope.kt +++ b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/GuardScope.kt @@ -23,38 +23,63 @@ import org.gradle.api.internal.catalog.DelegatingProjectDependency import org.gradle.api.provider.Provider import org.gradle.util.internal.ConfigureUtil +private val emptyDenyScope = Action { } + interface GuardScope { fun deny( dependencyPath: String, - action: Action = Action { }, + action: Action, ) + fun deny( + provider: Provider, + action: Action, + ) + + fun applyRule(rule: GuardRule) + fun deny( dependencyDelegation: DelegatingProjectDependency, - action: Action = Action { }, - ) = deny(dependencyPath = dependencyDelegation.path, action = action) + action: Action, + ) { + deny(dependencyPath = dependencyDelegation.path, action = action) + } + + // Required for groovy compatibility + fun deny(dependencyPath: String) { + deny(dependencyPath, emptyDenyScope) + } + + fun deny(dependencyDelegation: DelegatingProjectDependency) { + deny(dependencyDelegation.path, emptyDenyScope) + } fun deny( provider: Provider, - action: Action = Action { }, - ) + ) { + deny(provider, emptyDenyScope) + } + + fun deny( + dependencyDelegation: DelegatingProjectDependency, + closure: Closure, + ) { + deny(dependencyDelegation.path, ConfigureUtil.configureUsing(closure)) + } - // Required for groovy compatibility fun deny( dependencyPath: String, - closure: Closure + closure: Closure, ) { deny(dependencyPath, ConfigureUtil.configureUsing(closure)) } - // Required for groovy compatibility fun deny( provider: Provider, - closure: Closure + closure: Closure, ) { deny(provider, ConfigureUtil.configureUsing(closure)) } - fun applyRule(rule: GuardRule) } diff --git a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/GroovyIntegrationTest.kt b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/GroovyIntegrationTest.kt new file mode 100644 index 0000000..666554d --- /dev/null +++ b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/GroovyIntegrationTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2026 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.projectguard.plugin + +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class GroovyIntegrationTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + private val pluginRunner = PluginRunner(temporaryFolder) + private lateinit var rootBuildFile: File + + @Before + fun setup() { + rootBuildFile = temporaryFolder.newFile("build.gradle") + rootBuildFile.writeText( + """ + plugins { + id 'com.rubensousa.projectguard' + } + subprojects { + apply plugin: 'java-library' + apply plugin: 'com.rubensousa.projectguard' + } + + """.trimIndent() + ) + } + + @Test + fun `check fails for guard restriction`() { + // given + val module = "consumer" + val dependency = "libraryA" + val reason = "Bla bla" + pluginRunner.createModule(module) + pluginRunner.createModule(dependency) + rootBuildFile.appendText( + """ + projectGuard { + guard(":$module") { + deny(":$dependency") { + reason("$reason") + } + } + } + """.trimIndent() + ) + + // when + pluginRunner.addDependency(from = module, to = dependency) + + // then + pluginRunner.assertCheckFails(module) + pluginRunner.assertTaskOutputContains(reason) + } + + @Test + fun `check succeeds for guard restriction that does not match`() { + // given + val module = "consumer" + val dependency = "libraryA" + pluginRunner.createModule(module) + pluginRunner.createModule(dependency) + rootBuildFile.appendText( + """ + projectGuard { + guard(":$module") { + deny(":another") + } + } + """.trimIndent() + ) + + // when + pluginRunner.addDependency(from = module, to = dependency) + + // then + pluginRunner.assertCheckSucceeds(module) + pluginRunner.assertTaskOutputContains("No fatal matches found") + } + +} diff --git a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt index 2fb44b0..abb3422 100644 --- a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt +++ b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt @@ -16,10 +16,6 @@ package com.rubensousa.projectguard.plugin -import com.google.common.truth.Truth.assertThat -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome import org.junit.Before import org.junit.Rule import org.junit.Test @@ -31,19 +27,12 @@ class PluginIntegrationTest { @get:Rule val temporaryFolder = TemporaryFolder() + private val pluginRunner = PluginRunner(temporaryFolder) private lateinit var rootBuildFile: File - private lateinit var settingsFile: File - private lateinit var gradleRunner: GradleRunner - private val checkTask = ":consumer:projectGuardCheck" @Before fun setup() { - settingsFile = temporaryFolder.newFile("settings.gradle.kts") rootBuildFile = temporaryFolder.newFile("build.gradle.kts") - gradleRunner = GradleRunner.create() - .withProjectDir(temporaryFolder.root) - .withPluginClasspath() - .withGradleVersion("8.13") rootBuildFile.writeText( """ plugins { @@ -61,9 +50,9 @@ class PluginIntegrationTest { @Test fun `transitive dependencies are found and cause check to fail`() { // given - createModule("consumer") - createModule("libraryA") - createModule("libraryB") + pluginRunner.createModule("consumer") + pluginRunner.createModule("libraryA") + pluginRunner.createModule("libraryB") rootBuildFile.appendText( """ @@ -75,21 +64,19 @@ class PluginIntegrationTest { """.trimIndent() ) - addDependency(from = "consumer", to = "libraryA") - addDependency(from = "libraryA", to = "libraryB") - // when - val result = runCheckTask(expectSuccess = false) + pluginRunner.addDependency(from = "consumer", to = "libraryA") + pluginRunner.addDependency(from = "libraryA", to = "libraryB") // then - assertThat(result.task(checkTask)?.outcome!!).isEqualTo(TaskOutcome.FAILED) + pluginRunner.assertCheckFails("consumer") } @Test fun `direct dependencies are found and cause check to fail`() { // given - createModule("consumer") - createModule("library") + pluginRunner.createModule("consumer") + pluginRunner.createModule("library") rootBuildFile.appendText( """ @@ -99,20 +86,18 @@ class PluginIntegrationTest { """.trimIndent() ) - addDependency(from = "consumer", to = "library") - // when - val result = runCheckTask(expectSuccess = false) + pluginRunner.addDependency(from = "consumer", to = "library") // then - assertThat(result.task(checkTask)?.outcome!!).isEqualTo(TaskOutcome.FAILED) + pluginRunner.assertCheckFails("consumer") } @Test fun `check task succeeds when no matches are found`() { // given - createModule("consumer") - createModule("library") + pluginRunner.createModule("consumer") + pluginRunner.createModule("library") rootBuildFile.appendText( """ @@ -122,38 +107,10 @@ class PluginIntegrationTest { """.trimIndent() ) - addDependency(from = "consumer", to = "library") - // when - val result = runCheckTask(expectSuccess = true) + pluginRunner.addDependency(from = "consumer", to = "library") // then - assertThat(result.task(checkTask)!!.outcome).isEqualTo(TaskOutcome.SUCCESS) - } - - private fun runCheckTask(expectSuccess: Boolean): BuildResult { - val runner = gradleRunner.withArguments(checkTask) - return if (expectSuccess) { - runner.build() - } else { - runner.buildAndFail() - } - } - - private fun createModule(name: String) { - temporaryFolder.newFolder(name) - temporaryFolder.newFile("$name/build.gradle.kts") - settingsFile.appendText("\ninclude(\":$name\")") + pluginRunner.assertCheckSucceeds("consumer") } - - private fun addDependency(from: String, to: String, configuration: String = "implementation") { - temporaryFolder.getRoot().resolve("$from/build.gradle.kts").appendText( - """ - dependencies { - $configuration(project(":$to")) - } - """.trimIndent() - ) - } - } diff --git a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginRunner.kt b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginRunner.kt new file mode 100644 index 0000000..6654b8c --- /dev/null +++ b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginRunner.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2026 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.projectguard.plugin + +import com.google.common.truth.Truth.assertThat +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.rules.TemporaryFolder + +class PluginRunner( + private val temporaryFolder: TemporaryFolder, +) { + + private val settingsFile by lazy { temporaryFolder.newFile("settings.gradle.kts") } + private val gradleRunner by lazy { + GradleRunner.create() + .withProjectDir(temporaryFolder.root) + .withPluginClasspath() + .withGradleVersion("8.13") + } + private var lastResult: BuildResult? = null + + fun createModule(name: String) { + temporaryFolder.newFolder(name) + temporaryFolder.newFile("$name/build.gradle.kts") + settingsFile.appendText("\ninclude(\":$name\")") + } + + fun assertCheckFails(module: String) { + val task = createCheckTask(module) + val result = gradleRunner.withArguments(task).buildAndFail() + assertThat(result.task(task)!!.outcome).isEqualTo(TaskOutcome.FAILED) + lastResult = result + } + + fun assertCheckSucceeds(module: String) { + val task = createCheckTask(module) + val result = gradleRunner.withArguments(task).build() + assertThat(result.task(task)!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + lastResult = result + } + + fun assertTaskOutputContains(message: String){ + assertThat(lastResult!!.output).contains(message) + } + + fun addDependency(from: String, to: String, configuration: String = "implementation") { + temporaryFolder.getRoot().resolve("$from/build.gradle.kts").appendText( + """ + dependencies { + $configuration(project(":$to")) + } + """.trimIndent() + ) + } + + private fun createCheckTask(module: String): String { + return ":$module:projectGuardCheck" + } + +} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index dbfdc3f..5229dcc 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -16,8 +16,8 @@ projectGuard { deny(":legacy") } restrictModule(":android") { - // Test dependencies are fine - allow(libs.junit) + // All external libraries are fine for android modules + allowExternalLibraries() } restrictModule(":domain") { reason("Domain modules should not depend on other modules") diff --git a/sample/kmp/build.gradle.kts b/sample/kmp/build.gradle.kts index ef814d5..02db106 100644 --- a/sample/kmp/build.gradle.kts +++ b/sample/kmp/build.gradle.kts @@ -37,9 +37,8 @@ kotlin { androidMain { dependencies { - // Add Android-specific dependencies here. Note that this source set depends on - // commonMain by default and will correctly pull the Android artifacts of any KMP - // dependencies declared in commonMain. + // Just here to trigger a restriction + implementation(libs.mockk) } }