How to enable Lint GradleDependency when use dependency substitution
In these day, there are many ways to make your gradle file looks clean and more structured. Even more, if you have many module and there will be many duplication dependencies you will write in your gradle file. One of the way, you can use is dependency substitution. Which means you will have an extra in our buildScript to store list of your dependencies.
But if you this, you will face an issue on your linter, especially on GradleDependency. Let’s deep dive how GradleDependency.
Problem
Example you have an extra like below:
def versions = [
kotlin: '1.7.20'
]
ext.deps = [:]
deps.versions = versions
deps.kotlin = [
runtime : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
]
And implement it in our app/build.gradle:
...
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${deps.versions.kotlin}"
implementation deps.kotlin.runtime
}
And, let’s sync gradle, and see what happen in your dependency.
You will see the two of three dependency, will highlighted as a warning to tell you there is new version for that dependency. But not for your dependency substitution (deps.kotlin.runtime).
First example org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20. Gradle lint will parse into 3 part:
- groupId: org.jetbrains.kotlin
- artifactId: kotlin-stdlib-jdk7
- version: 1.7.20
And after that, gradle lint will lookup into remote repository to get latest version base on groupId and artifactId.
Second example org.jetbrains.kotlin:kotlin-stdlib-jdk7:${deps.version.kotlin}. Gradle lint can still showing the warning even using version substitution. How it can be? Same as before, gradle lint will parse into 3 part:
- groupId: org.jetbrains.kotlin
- artifactId: kotlin-stdlib-jdk7
- version: ${deps.versions.kotlin}
After that, to get actual version, gradle lint will lookup into project artifactory base on groupId and artifactId.
And then, gradle lint will lookup into remote repository to get latest version.
Third example deps.kotlin.runtime. Gradle lint can not showing the warning. Why? Because not like the second example, gradle lint can not lookup into project artifactory, and find any substitution for deps.kotlin.runtime.
Solution
Custom Lint Issue
First think you need to do is to create custom lint issue. You can refer here for the details.
Basically you need create a new module, let say :lint. And create new Detector let say DepsDetector.
class DepsDetector : GradleDetector() {
/**
* Gradle lint will invoke this method when dependencies on gradle.
*/
override fun checkDslPropertyAssignment(
context: GradleContext,
property: String,
value: String,
parent: String,
parentParent: String?,
propertyCookie: Any,
valueCookie: Any,
statementCookie: Any
) {
// I just want focus on dependencies, so will ignore else
if (parent != "dependencies") {
return
}
super.checkDslPropertyAssignment(
context,
property,
// first example contains "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20" (including double quotes)
// second example contains "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${deps.versions.kotlin}"
// third example contains deps.kotlin.runtime
value,
parent,
parentParent,
propertyCookie,
valueCookie,
statementCookie
)
}
From code above, you need to have mapping from deps.kotlin.runtime into actual value into “org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20” (include double quotes).
Deps Collector
To have mapping from deps, you can create gradle task to generate class for your mapping from deps into Kotlin class and will use by DepsDetector. Put script below in lint/build.gradle.
// This function to parse deps into pair of maps. And it depend on your deps structure
private String getBody(String parent, Map<String, Object> map) {
def body = ""
map.each {
if (it.value instanceof Map) {
body += getBody((parent + "." + it.key), it.value as Map<String, Object>)
} else {
body += "\t\t\"${parent}.${it.key}\" to \"${it.value}\"\n"
}
}
return body
}
// Let say we create new task with name depsCollector to generate class
// contains mapping of your deps
project.task("depsCollector") {
def parent = project.file("$buildDir/generated/depsCollector/com/deps")
if (!parent.exists()) {
parent.mkdirs()
}
def file = new File(parent, "DepsConfig.kt")
if (file.exists()) {
file.delete()
}
file.write(
"//This is generated class. Do not modify!\n" +
"package com.deps\n\n" +
"object DepsConfig {\n" +
"\tval MAP_OF_DEPS = mutableMapOf(\n" +
getBody("deps", deps) +
"\t)\n" +
"}\n"
)
}
Try to run ./gradlew :lint:depsCollector , it will generate class under ./lint/build/generated/depsCollector/com/deps
But currently you can not use it for know, you need to set ./lint/build/generated/depsCollect as a main sourceSet.
sourceSets {
main {
java {
srcDir "$buildDir/generated/depsCollector"
}
}
}
And final step, you need to set depsCollector task always run before compileKotlin. It’s to make sure not failing when you build the module.
project.task("compileKotlin").dependsOn("depsCollector")
Back to Custom Lint Issue
After you have generated mapping from deps, you can going back into DepsCollector to use DepsConfig.
class DepsDetector : GradleDetector() {
/**
* Gradle lint will invoke this method when dependencies on gradle.
*/
override fun checkDslPropertyAssignment(
context: GradleContext,
property: String,
value: String,
parent: String,
parentParent: String?,
propertyCookie: Any,
valueCookie: Any,
statementCookie: Any
) {
// Just focus on dependencies, so ignore else
if (parent != "dependencies") {
return
}
super.checkDslPropertyAssignment(
context,
property,
// To make sure it final value,
// from GradleDetector required to add double quotes
// first and last character
DepsConfig.MAP_OF_DEPS[value]?.let { "\"$it\"" } ?: value,,
parent,
parentParent,
propertyCookie,
valueCookie,
statementCookie
)
}
}
And then, you need to add new IssueRegistry to register DepsDetector.
class AdditionalIssueRegistry : IssueRegistry() {
override val api: Int = CURRENT_API
override val issues: List<Issue> = listOf(depsCollector)
companion object {
val depsCollector = Issue.create(
"DepsCollector",
"Deps mapper to help detector to find obsolete gradle dependency",
"""
This detector is base on GradleDetector,
and help to map and convert from deps plain text into real
group:artifact:version base on artifactory.
""",
Category.CORRECTNESS,
4,
Severity.WARNING,
Implementation(
DepsDetector::class.java,
Scope.GRADLE_SCOPE
)
)
}
}
And publish AdditionalIssueRegistry into jar on lint/build.gradle.
// put inside lint/build.gradle
jar {
manifest {
attributes("Lint-Registry": "com.yourpackage.AdditionalIssueRegistry")
}
}
Use Lint Module as Additional Rule
And final step, you can use module :lint as a lintChecks in app/build.gradle.
And finally you will get warning for your dependency substitutions.
Hope this article help you, and make your gradle file more clean and structured.
Clap if you like it, and more claps if you helped by this article.