Skip to content

Commit

Permalink
Rework: dependency format check
Browse files Browse the repository at this point in the history
  • Loading branch information
jjohannes committed Dec 4, 2023
1 parent 563bf9f commit 93dabf6
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tasks.register("check") {
group = mainBuildGroup
description = "Runs all checks and produces test summary and code coverage reports"
dependsOn(subprojects.map { ":${it.name}:$name" })
dependsOn(gradle.includedBuild("platform").task(":check"))
doLast {
println("Unit test summary: app/build/reports/tests/unit-test/aggregated-results/index.html")
println("Unit test code coverage: app/build/reports/jacoco/testCodeCoverageReport/html/index.html")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import org.example.dependencyanalysis.DependencyFormatCheck
import org.example.dependencyanalysis.DependencyVersionUpgradesCheck

plugins {
id("java-platform")
id("org.example.dependency-analysis")
}

val checkDependencyFormatting = tasks.register<DependencyFormatCheck>("checkDependencyFormatting") {
group = LifecycleBasePlugin.VERIFICATION_GROUP

buildFilePath.set(project.buildFile.absolutePath)
shouldNotHaveVersions.set(false)
declaredDependencies.put("api", provider { configurations.api.get().dependencies.map { d -> d.toDeclaredString() } })
declaredDependencies.put("runtime", provider { configurations.runtime.get().dependencies.map { d -> d.toDeclaredString() } })
declaredConstraints.put("api", provider { configurations.api.get().dependencyConstraints.map { d -> d.toDeclaredString() } })
declaredConstraints.put("runtime", provider { configurations.runtime.get().dependencyConstraints.map { d -> d.toDeclaredString() } })
}

// Install a task that checks if new versions are available for what is declared in the platform
Expand All @@ -14,3 +25,10 @@ tasks.register<DependencyVersionUpgradesCheck>("checkForDependencyVersionUpgrade
apiDependencies.set(configurations.api.get().dependencies.map { "${it.group}:${it.name}:${it.version}" })
apiDependencyConstraints.set(configurations.api.get().dependencyConstraints.map { "${it.group}:${it.name}:${it.version}" })
}

tasks.check {
dependsOn(checkDependencyFormatting)
}

fun Dependency.toDeclaredString() = "$group:$name:$version"
fun DependencyConstraint.toDeclaredString() = "$group:$name:$version"
Original file line number Diff line number Diff line change
@@ -1,49 +1,40 @@
import com.autonomousapps.DependencyAnalysisSubExtension
import org.example.dependencyanalysis.DependencyFormatCheck
import org.example.dependencyanalysis.DependencyScopeCheck
import org.example.dependencyanalysis.configurationPrefixesToSkip

plugins {
id("base")
id("org.example.dependency-analysis")
id("java")
}

// Check that dependencies are always declared without version.
configurations.all {
if (configurationPrefixesToSkip.any { name.startsWith(it) }) {
return@all
}
val checkDependencyFormatting = tasks.register<DependencyFormatCheck>("checkDependencyFormatting") {
group = LifecycleBasePlugin.VERIFICATION_GROUP

val configuration = this
withDependencies {
forEach { dependency ->
if (dependency is ExternalModuleDependency && !dependency.version.isNullOrEmpty()) {
throw RuntimeException("""
${project.name}/build.gradle.kts
Dependencies with versions are not allowed. Please declare the dependency as follows:
${configuration.name}("${dependency.group}:${dependency.name}")
All versions must be declared in 'gradle/platform'.
If the version is not yet defined there, add the following to 'gradle/build.gradle.kts':
api("${dependency.group}:${dependency.name}:${dependency.version}")
""".trimIndent())
}
}
buildFilePath.set(project.buildFile.absolutePath)
shouldNotHaveVersions.set(true)
sourceSets.all {
declaredDependencies.put(implementationConfigurationName, provider { configurations.getByName(implementationConfigurationName).dependencies.map { d -> d.toDeclaredString() } })
declaredDependencies.put(runtimeOnlyConfigurationName, provider { configurations.getByName(runtimeOnlyConfigurationName).dependencies.map { d -> d.toDeclaredString() } })
declaredDependencies.put(compileOnlyConfigurationName, provider { configurations.getByName(compileOnlyConfigurationName).dependencies.map { d -> d.toDeclaredString() } })
declaredDependencies.put(apiConfigurationName, provider { configurations.findByName(apiConfigurationName)?.dependencies?.map { d -> d.toDeclaredString() } ?: emptyList() })
declaredDependencies.put(compileOnlyApiConfigurationName, provider { configurations.findByName(compileOnlyApiConfigurationName)?.dependencies?.map { d -> d.toDeclaredString() } ?: emptyList() })
}
}

// Configure a 'checkDependencyScopes' tasks that uses the 'com.autonomousapps.dependency-analysis' plugin.
// To find unused dependencies and check 'api' vs. 'implementation' scopes.
val dependencyScopesCheck = tasks.register<DependencyScopeCheck>("checkDependencyScopes") {
val checkDependencyScopes = tasks.register<DependencyScopeCheck>("checkDependencyScopes") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
shouldRunAfter(checkDependencyFormatting)
}

tasks.check {
dependsOn(dependencyScopesCheck)
dependsOn(checkDependencyFormatting)
dependsOn(checkDependencyScopes)
}

plugins.withId("com.autonomousapps.dependency-analysis") {
extensions.getByType<DependencyAnalysisSubExtension>().registerPostProcessingTask(dependencyScopesCheck)
extensions.getByType<DependencyAnalysisSubExtension>().registerPostProcessingTask(checkDependencyScopes)
}

fun Dependency.toDeclaredString() = when(this) {
is ProjectDependency -> ":$name"
else -> "$group:$name${if (version == null) "" else ":$version"}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ tasks.register("checkDependencyScopes") {
description = "Check all dependency scopes (api vs implementation) and find unused dependencies"
dependsOn(subprojects.map { "${it.path}:checkDependencyScopes"})
}

tasks.register("checkDependencyFormatting") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Check format of all dependency declarations"
dependsOn(subprojects.map { "${it.path}:checkDependencyFormatting"})
dependsOn(gradle.includedBuild("platform").task(":checkDependencyFormatting"))
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.example.dependencyanalysis

import org.gradle.api.DefaultTask
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

/**
* Check that 'dependencies' are defined in alphabetical order and without version.
*/
abstract class DependencyFormatCheck : DefaultTask() {

@get:Input
abstract val buildFilePath : Property<String>

@get:Input
abstract val declaredDependencies : MapProperty<String, List<String>> // Map of 'scope' to 'coordinates'

@get:Input
abstract val declaredConstraints : MapProperty<String, List<String>> // Map of 'scope' to 'coordinates'

@get:Input
abstract val shouldNotHaveVersions : Property<Boolean>

@TaskAction
fun check() {
declaredDependencies.get().forEach { (scope, dependencies) ->
if (shouldNotHaveVersions.get()) {
dependencies.forEach { coordinates ->
if (coordinates.count { it == ':' } == 2 && !coordinates.startsWith("org.jetbrains.kotlin:kotlin-stdlib:")) {
throw RuntimeException("""
${buildFilePath.get()}
Dependencies with versions are not allowed. Please declare the dependency as follows:
${scope}("${coordinates.substring(0, coordinates.lastIndexOf(':'))}")
All versions must be declared in 'gradle/platform'.
If the version is not yet defined there, add the following to 'gradle/platform/build.gradle.kts':
api("$coordinates")
""".trimIndent())
}
}
}

val declaredInBuildFile = dependencies.filter {
// Ignore dependencies that are defined in our plugins
it !in listOf(
"org.example.product:platform",
"org.slf4j:slf4j-simple",
"org.junit.jupiter:junit-jupiter-engine",
"org.junit.jupiter:junit-jupiter")
}
val sortedProject = declaredInBuildFile.filter { it.startsWith(":") }.sorted()
val sortedExternal = declaredInBuildFile.filter { !it.startsWith(":") }.sorted()
if (declaredInBuildFile != sortedProject + sortedExternal) {
throw RuntimeException("""
${buildFilePath.get()}
$scope dependencies are not declared in alphabetical order. Please use this order:
${sortedProject.joinToString("\n ") {"${scope}(project(\"${it}\"))"}}
${sortedExternal.joinToString("\n ") {"${scope}(\"${it}\")"}}
""".trimIndent())
}
}

declaredConstraints.get().forEach { (scope, constraints) ->
val sortedConstraints = constraints.sorted()
if (constraints != sortedConstraints) {
throw RuntimeException("""
${buildFilePath.get()}
$scope dependency constraints are not declared in alphabetical order. Please use this order:
${sortedConstraints.joinToString("\n ") {"${scope}(\"${it}\")"}}
""".trimIndent())
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import com.autonomousapps.model.Advice
import com.autonomousapps.model.ProjectCoordinates
import org.gradle.api.tasks.TaskAction

/**
* Task that uses the 'com.autonomousapps.dependency-analysis' plugin to find unused dependencies and check
* 'api' vs. 'implementation' scopes.
*/
abstract class DependencyScopeCheck : AbstractPostProcessingTask() {

@TaskAction
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ plugins {
}

// Expose the ':app' project runtime classpath in every project
val appRuntimeClasspath = configurations.create("appRuntimeClasspath") {
val appRuntimeClasspath = configurations.resolvable("appRuntimeClasspath") {
description = "Runtime classpath of the complete application"
isCanBeConsumed = false
isCanBeResolved = true
attributes {
// We want the runtime classpath represented by Usage.JAVA_RUNTIME
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
Expand All @@ -21,8 +19,8 @@ val appRuntimeClasspath = configurations.create("appRuntimeClasspath") {

// Every compile classpath and runtime classpath uses the versions of the
sourceSets.all {
configurations[compileClasspathConfigurationName].shouldResolveConsistentlyWith(appRuntimeClasspath)
configurations[runtimeClasspathConfigurationName].shouldResolveConsistentlyWith(appRuntimeClasspath)
configurations[compileClasspathConfigurationName].shouldResolveConsistentlyWith(appRuntimeClasspath.get())
configurations[runtimeClasspathConfigurationName].shouldResolveConsistentlyWith(appRuntimeClasspath.get())
// Source sets without production code (tests / fixtures) are allowed to have dependencies that are
// not part of the consistent resolution result and might need additional version information
if (this != sourceSets["main"]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ publishing.publications.create<MavenPublication>("mavenJava") {
// We use consistent resolution + a platform for controlling versions
// -> Publish the versions that are the result of the consistent resolution
versionMapping {
usage("java-api") {
fromResolutionResult()
}
usage("java-runtime") {
allVariants {
fromResolutionResult()
}
}
Expand Down

0 comments on commit 93dabf6

Please sign in to comment.