diff --git a/CHANGELOG.md b/CHANGELOG.md index 020f1b74f..b0dfcae13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] [Unreleased]: https://github.com/cashapp/licensee/compare/1.14.1...HEAD] +**Added** + +- Support ignoring dependencies by regex. + **Changed** - The minimum-supported Gradle version is now 9.0. diff --git a/api/licensee.api b/api/licensee.api index 0fbb474c8..d83139f82 100644 --- a/api/licensee.api +++ b/api/licensee.api @@ -66,6 +66,8 @@ public abstract interface class app/cash/licensee/LicenseeExtension { public fun ignoreDependencies (Ljava/lang/String;Ljava/lang/String;)V public abstract fun ignoreDependencies (Ljava/lang/String;Ljava/lang/String;Lorg/gradle/api/Action;)V public fun ignoreDependencies (Ljava/lang/String;Lorg/gradle/api/Action;)V + public fun ignoreDependenciesByRegex (Ljava/lang/String;)V + public abstract fun ignoreDependenciesByRegex (Ljava/lang/String;Lorg/gradle/api/Action;)V public abstract fun unusedAction (Lapp/cash/licensee/UnusedAction;)V public abstract fun violationAction (Lapp/cash/licensee/ViolationAction;)V } diff --git a/src/main/kotlin/app/cash/licensee/dependencyGraph.kt b/src/main/kotlin/app/cash/licensee/dependencyGraph.kt index 82b2112f9..0bbb51e52 100644 --- a/src/main/kotlin/app/cash/licensee/dependencyGraph.kt +++ b/src/main/kotlin/app/cash/licensee/dependencyGraph.kt @@ -32,6 +32,7 @@ import org.gradle.api.logging.Logger internal data class DependencyConfig( val ignoredGroupIds: Map, val ignoredCoordinates: Map>, + val ignoredRegexes: Map, ) : Serializable internal data class IgnoredData(val reason: String?, val transitive: Boolean) : Serializable @@ -45,6 +46,7 @@ internal fun loadDependencyCoordinates( val unusedGroupIds = config.ignoredGroupIds.keys.toMutableSet() val unusedCoordinates = mutableSetOf>() + val unusedRegexes = config.ignoredRegexes.keys.toMutableSet() for ((groupId, artifacts) in config.ignoredCoordinates) { val redundant = groupId in config.ignoredGroupIds for (artifactId in artifacts.keys) { @@ -63,6 +65,7 @@ internal fun loadDependencyCoordinates( config, unusedGroupIds, unusedCoordinates, + unusedRegexes, coordinates, mutableSetOf(), depth = 1, @@ -74,6 +77,9 @@ internal fun loadDependencyCoordinates( for ((groupId, artifactId) in unusedCoordinates) { warnings += "Dependency ignore for $groupId:$artifactId is unused" } + for (unusedRegex in unusedRegexes) { + warnings += "Dependency ignore for regex '$unusedRegex' is unused" + } return DependencyResolutionResult(coordinates, warnings) } @@ -92,6 +98,7 @@ private fun loadDependencyCoordinates( config: DependencyConfig, unusedGroupIds: MutableSet, unusedCoordinates: MutableSet>, + unusedRegexes: MutableSet, destination: MutableSet, seen: MutableSet, depth: Int, @@ -116,12 +123,17 @@ private fun loadDependencyCoordinates( // Assuming flat-dir repository dependency, do nothing. ignoreSuffix = " ignoring because flat-dir repository artifact has no metadata" } else { + val moduleCoordinate = "${id.group}:${id.module}" val ignoredData = null ?: config.ignoredGroupIds[id.group]?.also { unusedGroupIds -= id.group } ?: config.ignoredCoordinates[id.group]?.get(id.module)?.also { unusedCoordinates -= id.group to id.module } + ?: config.ignoredRegexes.entries + .find { (regex, _) -> regex.matches(moduleCoordinate) } + ?.also { (regex, _) -> unusedRegexes -= regex } + ?.value if (ignoredData != null) { ignoreSuffix = buildString { append(" ignoring") @@ -166,6 +178,7 @@ private fun loadDependencyCoordinates( config, unusedGroupIds, unusedCoordinates, + unusedRegexes, destination, seen, depth + 1, diff --git a/src/main/kotlin/app/cash/licensee/pluginExtension.kt b/src/main/kotlin/app/cash/licensee/pluginExtension.kt index 639601011..5ce8b0a89 100644 --- a/src/main/kotlin/app/cash/licensee/pluginExtension.kt +++ b/src/main/kotlin/app/cash/licensee/pluginExtension.kt @@ -127,6 +127,45 @@ interface LicenseeExtension { allowDependency(dependencyProvider = dependencyProvider, options = {}) } + /** + * ```groovy + * licensee { + * ignoreDependenciesByRegex('com.mycompany.internal') + * ignoreDependenciesByRegex('com.mycompany.utils', 'utils') + * } + * ``` + * + * A reason string can be supplied to document why the dependencies are being ignored. + * + * ```groovy + * licensee { + * ignoreDependenciesByRegex('com.example.sdk', 'sdk') { + * because "commercial SDK" + * } + * } + * ``` + * + * An ignore can be marked as transitive which will ignore an entire branch of the dependency + * tree. This will ignore the target artifact's dependencies regardless of the artifact + * coordinates or license info. Since it is especially dangerous, a reason string is required. + * + * ```groovy + * licensee { + * ignoreDependenciesByRegex('com.other.sdk', 'sdk') { + * transitive = true + * because "commercial SDK" + * } + * } + * ``` + * + * @see ignoreDependencies + */ + fun ignoreDependenciesByRegex(regex: String, options: Action) + + fun ignoreDependenciesByRegex(regex: String) { + ignoreDependenciesByRegex(regex = regex, options = {}) + } + /** * Ignore a single dependency or group of dependencies during dependency graph resolution. * Artifacts targeted with this method will not be analyzed for license information and will not @@ -166,6 +205,8 @@ interface LicenseeExtension { * } * } * ``` + * + * @see ignoreDependenciesByRegex */ fun ignoreDependencies( groupId: String, @@ -262,6 +303,7 @@ internal abstract class MutableLicenseeExtension : LicenseeExtension { internal abstract val allowedUrls: MapProperty> internal abstract val allowedDependencies: MapProperty> internal abstract val ignoredGroupIds: MapProperty + internal abstract val ignoredRegexes: MapProperty internal abstract val ignoredCoordinates: NamedDomainObjectContainer internal abstract val violationAction: Property internal abstract val unusedAction: Property @@ -274,12 +316,13 @@ internal abstract class MutableLicenseeExtension : LicenseeExtension { } fun toDependencyTreeConfig(): Provider { - return ignoredGroupIds.map { ignoredGroupIds -> + return ignoredGroupIds.zip(ignoredRegexes) { ignoredGroupIds, ignoredRegexes -> DependencyConfig( ignoredGroupIds.toMap(), ignoredCoordinates .groupBy({ it.name }) { it.ignoredDatas.get() } .mapValues { it.value.single() }, + ignoredRegexes.mapKeys { (regex, _) -> regex.toRegex() }, ) } } @@ -367,17 +410,7 @@ internal abstract class MutableLicenseeExtension : LicenseeExtension { artifactId: String?, options: Action, ) { - val option = - object : IgnoreDependencyOptions { - var setReason: String? = null - - override fun because(reason: String) { - setReason = reason - } - - override var transitive: Boolean = false - } - + val option = DefaultIgnoreDependencyOptions() options.execute(option) if (option.transitive && option.setReason == null) { throw RuntimeException( @@ -400,6 +433,22 @@ internal abstract class MutableLicenseeExtension : LicenseeExtension { } } + override fun ignoreDependenciesByRegex(regex: String, options: Action) { + val option = DefaultIgnoreDependencyOptions() + options.execute(option) + if (option.transitive && option.setReason == null) { + throw RuntimeException( + buildString { + append("Transitive dependency ignore on regex '") + append(regex) + append("' is dangerous and requires a reason string") + } + ) + } + val ignoredData = IgnoredData(option.setReason, option.transitive) + ignoredRegexes.put(regex, ignoredData) + } + override fun violationAction(level: ViolationAction) { violationAction.set(level) } @@ -409,6 +458,16 @@ internal abstract class MutableLicenseeExtension : LicenseeExtension { } } +private class DefaultIgnoreDependencyOptions : IgnoreDependencyOptions { + var setReason: String? = null + + override fun because(reason: String) { + setReason = reason + } + + override var transitive: Boolean = false +} + private fun Provider.zip2( left: Provider, right: Provider, diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/build.gradle b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/build.gradle new file mode 100644 index 000000000..8d5b4d002 --- /dev/null +++ b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/build.gradle @@ -0,0 +1,15 @@ +plugins { + id("java-library") + alias(libs.plugins.licensee) +} + +dependencies { + implementation 'com.example:example-a:1.0.0' +} + +licensee { + allow('Apache-2.0') + ignoreDependenciesByRegex('com\\.example:.*') { + transitive = true + } +} diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-a/1.0.0/example-a-1.0.0.jar b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-a/1.0.0/example-a-1.0.0.jar new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-a/1.0.0/example-a-1.0.0.jar differ diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-a/1.0.0/example-a-1.0.0.pom b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-a/1.0.0/example-a-1.0.0.pom new file mode 100644 index 000000000..433eb066f --- /dev/null +++ b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-a/1.0.0/example-a-1.0.0.pom @@ -0,0 +1,24 @@ + + 4.0.0 + com.example + example-a + 1.0.0 + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + com.example + example-b + 1.0.0 + + + com.other + other + 1.0.0 + + + diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-b/1.0.0/example-b-1.0.0.jar b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-b/1.0.0/example-b-1.0.0.jar new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-b/1.0.0/example-b-1.0.0.jar differ diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-b/1.0.0/example-b-1.0.0.pom b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-b/1.0.0/example-b-1.0.0.pom new file mode 100644 index 000000000..0651beacb --- /dev/null +++ b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/example/example-b/1.0.0/example-b-1.0.0.pom @@ -0,0 +1,12 @@ + + 4.0.0 + com.example + example-b + 1.0.0 + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/other/other/1.0.0/other-1.0.0.jar b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/other/other/1.0.0/other-1.0.0.jar new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/other/other/1.0.0/other-1.0.0.jar differ diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/other/other/1.0.0/other-1.0.0.pom b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/other/other/1.0.0/other-1.0.0.pom new file mode 100644 index 000000000..144260ee2 --- /dev/null +++ b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/repo/com/other/other/1.0.0/other-1.0.0.pom @@ -0,0 +1,12 @@ + + 4.0.0 + com.other + other + 1.0.0 + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + diff --git a/src/test/fixtures/ignore-group-regex-transitive-requires-reason/settings.gradle b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/settings.gradle new file mode 100644 index 000000000..379dbf56d --- /dev/null +++ b/src/test/fixtures/ignore-group-regex-transitive-requires-reason/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + includeBuild("../../test-build-logic") +} + +plugins { + id("licenseeTests") +} diff --git a/src/test/fixtures/ignore-group-regex/build.gradle b/src/test/fixtures/ignore-group-regex/build.gradle new file mode 100644 index 000000000..8353bb7f8 --- /dev/null +++ b/src/test/fixtures/ignore-group-regex/build.gradle @@ -0,0 +1,13 @@ +plugins { + id("java-library") + alias(libs.plugins.licensee) +} + +dependencies { + implementation 'com.example:example-a:1.0.0' +} + +licensee { + allow('Apache-2.0') + ignoreDependenciesByRegex('com\\.example:.*') +} diff --git a/src/test/fixtures/ignore-group-regex/expected/build/reports/licensee/artifacts.json b/src/test/fixtures/ignore-group-regex/expected/build/reports/licensee/artifacts.json new file mode 100644 index 000000000..5d1d5e409 --- /dev/null +++ b/src/test/fixtures/ignore-group-regex/expected/build/reports/licensee/artifacts.json @@ -0,0 +1,14 @@ +[ + { + "groupId": "com.other", + "artifactId": "other", + "version": "1.0.0", + "spdxLicenses": [ + { + "identifier": "Apache-2.0", + "name": "Apache License 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + } + ] + } +] diff --git a/src/test/fixtures/ignore-group-regex/repo/com/example/example-a/1.0.0/example-a-1.0.0.jar b/src/test/fixtures/ignore-group-regex/repo/com/example/example-a/1.0.0/example-a-1.0.0.jar new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/src/test/fixtures/ignore-group-regex/repo/com/example/example-a/1.0.0/example-a-1.0.0.jar differ diff --git a/src/test/fixtures/ignore-group-regex/repo/com/example/example-a/1.0.0/example-a-1.0.0.pom b/src/test/fixtures/ignore-group-regex/repo/com/example/example-a/1.0.0/example-a-1.0.0.pom new file mode 100644 index 000000000..433eb066f --- /dev/null +++ b/src/test/fixtures/ignore-group-regex/repo/com/example/example-a/1.0.0/example-a-1.0.0.pom @@ -0,0 +1,24 @@ + + 4.0.0 + com.example + example-a + 1.0.0 + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + com.example + example-b + 1.0.0 + + + com.other + other + 1.0.0 + + + diff --git a/src/test/fixtures/ignore-group-regex/repo/com/example/example-b/1.0.0/example-b-1.0.0.jar b/src/test/fixtures/ignore-group-regex/repo/com/example/example-b/1.0.0/example-b-1.0.0.jar new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/src/test/fixtures/ignore-group-regex/repo/com/example/example-b/1.0.0/example-b-1.0.0.jar differ diff --git a/src/test/fixtures/ignore-group-regex/repo/com/example/example-b/1.0.0/example-b-1.0.0.pom b/src/test/fixtures/ignore-group-regex/repo/com/example/example-b/1.0.0/example-b-1.0.0.pom new file mode 100644 index 000000000..0651beacb --- /dev/null +++ b/src/test/fixtures/ignore-group-regex/repo/com/example/example-b/1.0.0/example-b-1.0.0.pom @@ -0,0 +1,12 @@ + + 4.0.0 + com.example + example-b + 1.0.0 + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + diff --git a/src/test/fixtures/ignore-group-regex/repo/com/other/other/1.0.0/other-1.0.0.jar b/src/test/fixtures/ignore-group-regex/repo/com/other/other/1.0.0/other-1.0.0.jar new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/src/test/fixtures/ignore-group-regex/repo/com/other/other/1.0.0/other-1.0.0.jar differ diff --git a/src/test/fixtures/ignore-group-regex/repo/com/other/other/1.0.0/other-1.0.0.pom b/src/test/fixtures/ignore-group-regex/repo/com/other/other/1.0.0/other-1.0.0.pom new file mode 100644 index 000000000..144260ee2 --- /dev/null +++ b/src/test/fixtures/ignore-group-regex/repo/com/other/other/1.0.0/other-1.0.0.pom @@ -0,0 +1,12 @@ + + 4.0.0 + com.other + other + 1.0.0 + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + diff --git a/src/test/fixtures/ignore-group-regex/settings.gradle b/src/test/fixtures/ignore-group-regex/settings.gradle new file mode 100644 index 000000000..379dbf56d --- /dev/null +++ b/src/test/fixtures/ignore-group-regex/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + includeBuild("../../test-build-logic") +} + +plugins { + id("licenseeTests") +} diff --git a/src/test/kotlin/app/cash/licensee/LicenseePluginFixtureTest.kt b/src/test/kotlin/app/cash/licensee/LicenseePluginFixtureTest.kt index fdfdeeb53..e9f0afd2b 100644 --- a/src/test/kotlin/app/cash/licensee/LicenseePluginFixtureTest.kt +++ b/src/test/kotlin/app/cash/licensee/LicenseePluginFixtureTest.kt @@ -66,6 +66,7 @@ class LicenseePluginFixtureTest( "flat-dir-repository-ignored", "gson-broken-plugin", "ignore-group", + "ignore-group-regex", "ignore-group-artifact", "ignore-group-artifact-kts", "ignore-group-artifact-transitive", @@ -187,16 +188,19 @@ class LicenseePluginFixtureTest( "ignore-group-artifact-transitive-requires-reason-kts", "ignore-group-transitive-requires-reason", "ignore-group-transitive-requires-reason-kts", + "ignore-group-regex-transitive-requires-reason", ) fixtureName: String ) { val fixtureDir = File(fixturesDir, fixtureName) val result = createRunner(fixtureDir).buildAndFail() - assertThat(result.output) - .containsMatch( + val expected = + if (fixtureName.contains("regex")) { + "Transitive dependency ignore on regex 'com\\\\.example:.*' is dangerous and requires a reason string" + } else { "Transitive dependency ignore on 'com\\.example(:example)?' is dangerous and requires a reason string" - .toRegex() - ) + } + assertThat(result.output).containsMatch(expected.toRegex()) } @Test