Last year we successfully implemented some custom lint rules for our multi-module Android project by following https://proandroiddev.com/implementing-your-first-android-lint-rule-6e572383b292. At some point the lint rules stopped being applied, which I discovered by chance. By using git bisect
on my repository, I was able to determine that the rules stopped being applied in a commit which upgraded the following:
- Gradle in gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
=>gradle-6.7.1-all.zip
- Android Gradle plugin in our top-level build.gradle:
classpath "com.android.tools.build:gradle:4.1.3"'
=>gradle:4.2.2
Someone asked a similarly worded question 3 years ago but in their case they did not isolate a potential cause as the upgrade of Gradle or Android Gradle plugin.
Update 12/29/2021
I figured out that this has to do with multi-module projects. I compared the call stack that calls my custom lint Detector.GradleScanner
and discovered that it changed from Android Gradle Plugin 4.1.3 to 4.2.2:
com.android.tools.build:gradle:4.1.3
at com.locuslabs.lintrules.GradleLibraryNonStableVersionDetector.checkDslPropertyAssignment(GradleLibraryNonStableVersionDetector.kt:22)
...
at com.android.tools.lint.client.api.LintDriver.checkBuildScripts(LintDriver.kt:1203)
at com.android.tools.lint.client.api.LintDriver.runFileDetectors(LintDriver.kt:1151)
at com.android.tools.lint.client.api.LintDriver.checkProject(LintDriver.kt:926)
at com.android.tools.lint.client.api.LintDriver.analyze(LintDriver.kt:446)
at com.android.tools.lint.LintCliClient.run(LintCliClient.kt:256)
at com.android.tools.lint.LintCliClient.run(LintCliClient.kt:237)
at com.android.tools.lint.gradle.LintGradleClient.run(LintGradleClient.kt:255)
at com.android.tools.lint.gradle.LintGradleExecution.runLint(LintGradleExecution.kt:259)
at com.android.tools.lint.gradle.LintGradleExecution.lintAllVariants(LintGradleExecution.kt:318)
at com.android.tools.lint.gradle.LintGradleExecution.analyze(LintGradleExecution.kt:68)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at com.android.tools.lint.gradle.api.ReflectiveLintRunner.runLint(ReflectiveLintRunner.kt:38)
at com.android.build.gradle.tasks.LintBaseTask.runLint(LintBaseTask.java:117)
at com.android.build.gradle.tasks.LintGlobalTask.lint(LintGlobalTask.java:51)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:104)
...
com.android.tools.build:gradle:4.2.2
at com.locuslabs.lintrules.GradleLibraryNonStableVersionDetector.checkDslPropertyAssignment(GradleLibraryNonStableVersionDetector.kt:22)
...
at com.android.tools.lint.client.api.LintDriver$checkBuildScripts$2.run(LintDriver.kt:1490)
at com.android.tools.lint.client.api.LintClient.runReadAction(LintClient.kt:1780)
at com.android.tools.lint.client.api.LintDriver$LintClientWrapper.runReadAction(LintDriver.kt:2634)
at com.android.tools.lint.client.api.LintDriver.checkBuildScripts(LintDriver.kt:1483)
at com.android.tools.lint.client.api.LintDriver.runFileDetectors(LintDriver.kt:1401)
at com.android.tools.lint.client.api.LintDriver.checkProject(LintDriver.kt:1151)
at com.android.tools.lint.client.api.LintDriver.checkProjectRoot(LintDriver.kt:628)
at com.android.tools.lint.client.api.LintDriver.access$checkProjectRoot(LintDriver.kt:153)
at com.android.tools.lint.client.api.LintDriver$analyzeOnly$1.invoke(LintDriver.kt:442)
at com.android.tools.lint.client.api.LintDriver$analyzeOnly$1.invoke(LintDriver.kt:435)
at com.android.tools.lint.client.api.LintDriver.doAnalyze(LintDriver.kt:502)
at com.android.tools.lint.client.api.LintDriver.analyzeOnly(LintDriver.kt:434)
at com.android.tools.lint.LintCliClient$analyzeOnly$1.invoke(LintCliClient.kt:241)
at com.android.tools.lint.LintCliClient$analyzeOnly$1.invoke(LintCliClient.kt:241)
at com.android.tools.lint.LintCliClient.run(LintCliClient.kt:283)
at com.android.tools.lint.LintCliClient.run$default(LintCliClient.kt:266)
at com.android.tools.lint.LintCliClient.analyzeOnly(LintCliClient.kt:241)
at com.android.tools.lint.Main.run(Main.java:1536)
at com.android.tools.lint.Main.run(Main.java:269)
...
So the analyze()
function was changed to start calling a new function checkProjectRoot()
. I investigated when the function checkProjectRoot()
was introduced by going though the Android Gradle Plugin source code. The command git log -S checkProjectRoot
indicates the following commit introduced checkProjectRoot()
which implies that multi-module analysis changed:
base[mirror-goog-studio-main]10:06:10 git log -S checkProjectRoot
commit 1d01e75bf984568ecdf198cd35bfe05c8b0cce9f
Author: Tor Norbye <tnorbye@google.com>
Date: Tue Jan 19 16:18:19 2021 -0800
178514993: Support module-independent analysis in Lint
This CL adds support into lint for "provisional reporting"; this is a
big architectural change which is key to significantly speeding up
the performance (and incrementality) of lint in Gradle.
The key idea is that, for lint embedding clients which support it such
as Gradle, modules are analyzed one at a time, independently (and
possibly in parallel), without access to sources from any upstream
dependencies. In some cases that means they can't conclusively report
issues (for example, if they depend on the consuming module's
minSdkVersion), but the detectors record enough state such that in a
merging phase (reporting), they can look at their previously computed
results and map it into definite warnings.
This requires many detectors to be updated to explicitly handle this new
mode. There are a handful of typical scenarios where a detector needs to
be updated: accessing the minSdkVersion of the consuming module, or
depending on the type of the consuming mode (e.g. is it also a library?
or an Android module?).
To simplify this, most lint checks can simply pass in one of a number of
built-in constraints, e.g.
context.report(incident, minSdkAtLeast(minSdk))
There are several mechanisms included to help catch detectors that need
to be updated. First, various lint APIs are not valid to be called when
in analysis rather than reporting mode, and if a detector calls them,
lint will emit a warning message.
Private.java: Error: The lint detector
com.android.tools.lint.checks.PrivateResourceDetector
called context.getMainProject() during module analysis.
This does not work correctly when running in Lint Unit Tests.
In particular, there may be false positives or false negatives
[...]
Second, there's a new TestMode for lint's unit tests which causes all
unit tests to be analyzed both with and without module-independent
analysis. To catch common problems, in addition to running the
infrastructure with module-independent analysis enabled and in separate
and analysis phases, it also varies the minSdkVersion between the two
phases to ensure that the lint checks aren't just basing their analysis
on the module's minSdkVersion.
NOTE: This CL only adds support for module-independent analysis in lint
itself, and the detectors, and the unit testing infrastructure. It does
not hook this up to for example AGP, which will be done separately.
Test: Existing
Bug: 178514993
Change-Id: Iffa25707792e416eab3ce446c1f8d9d8cc830295
Does anyone know what changes are needed to make custom lint rules apply to all modules of a project, not just the top-level? Currently, I've implemented my rule as follows:
internal const val PRIORITY = 10
@Suppress("UnstableApiUsage")
class GradleLibraryNonStableVersionDetector : Detector(), Detector.GradleScanner {
override fun checkDslPropertyAssignment(
context: GradleContext,
property: String,
value: String,
parent: String,
parentParent: String?,
propertyCookie: Any,
valueCookie: Any,
statementCookie: Any
) {
super.checkDslPropertyAssignment(context, property, value, parent, parentParent, propertyCookie, valueCookie, statementCookie)
if (property == "implementation"
&& (value.contains("-alpha")
|| value.contains("-beta")
|| value.contains("-rc"))) {
context.report(ISSUE,
statementCookie, context.getLocation(statementCookie),
"Use only stable versions of the library")
}
}
companion object {
@JvmField
val ISSUE: Issue = Issue.create("NonStableLibraryVersion",
"Marks usage of unstable version of dependency library.",
"It's not recommended to use alpha, beta or release candidate library versions in production",
CORRECTNESS, PRIORITY, Severity.ERROR,
Implementation(GradleLibraryNonStableVersionDetector::class.java,
Scope.GRADLE_SCOPE))
}
}
And I expect when I call the lint tool as follows, it should apply to all build.gradle files across all modules:
./gradlew -Pci --console=plain :sdk:lint -PbuildDir=lint
Update 1/7/2022
I reported a similar issue on the Google Sample project for lint rules.