blob: 28200ee42af2eb8fd576c76bdfe0eff592392bb6 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* 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 androidx.build
import com.android.build.api.dsl.Lint
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryPlugin
import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask
import com.android.build.gradle.internal.lint.AndroidLintTask
import com.android.build.gradle.internal.lint.LintModelWriterTask
import com.android.build.gradle.internal.lint.VariantInputs
import java.io.File
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.findByType
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.tooling.core.withClosure
/**
* Single entry point to Android Lint configuration.
*/
fun Project.configureLint() {
project.plugins.all { plugin ->
when (plugin) {
is AppPlugin -> configureAndroidProjectForLint(isLibrary = false)
is LibraryPlugin -> configureAndroidProjectForLint(isLibrary = true)
// Only configure non-multiplatform Java projects via JavaPlugin. Multiplatform
// projects targeting Java (e.g. `jvm { withJava() }`) are configured via
// KotlinBasePlugin.
is JavaPlugin -> if (project.multiplatformExtension == null) {
configureNonAndroidProjectForLint()
}
// Only configure non-Android multiplatform projects via KotlinBasePlugin.
// Multiplatform projects targeting Android (e.g. `id("com.android.library")`) are
// configured via AppPlugin or LibraryPlugin.
is KotlinBasePlugin -> if (
project.multiplatformExtension != null &&
!project.plugins.hasPlugin(AppPlugin::class.java) &&
!project.plugins.hasPlugin(LibraryPlugin::class.java)
) {
configureNonAndroidProjectForLint()
}
}
}
}
/**
* Android Lint configuration entry point for Android projects.
*/
private fun Project.configureAndroidProjectForLint(
isLibrary: Boolean
) = androidExtension.finalizeDsl { extension ->
// The lintAnalyze task is used by `androidx-studio-integration-lint.sh`.
tasks.register("lintAnalyze") { task -> task.enabled = false }
configureLint(extension.lint, isLibrary)
// We already run lintDebug, we don't need to run lint on the release variant.
tasks.named("lint").configure { task -> task.enabled = false }
afterEvaluate {
registerLintDebugIfNeededAfterEvaluate()
if (extension.buildFeatures.aidl == true) {
configureLintForAidlAfterEvaluate()
}
}
}
/**
* Android Lint configuration entry point for non-Android projects.
*/
private fun Project.configureNonAndroidProjectForLint() = afterEvaluate {
// The lint plugin expects certain configurations and source sets which are only added by
// the Java and Android plugins. If this is a multiplatform project targeting JVM, we'll
// need to manually create these configurations and source sets based on their multiplatform
// JVM equivalents.
addSourceSetsForMultiplatformAfterEvaluate()
// For Android projects, the Android Gradle Plugin is responsible for applying the lint plugin;
// however, we need to apply it ourselves for non-Android projects.
apply(mapOf("plugin" to "com.android.lint"))
// Create task aliases matching those creates by AGP for Android projects, since those are what
// developers expect to invoke. Redirect them to the "real" lint task.
val lintTask = tasks.named("lint")
tasks.register("lintDebug") {
it.dependsOn(lintTask)
it.enabled = false
}
tasks.register("lintRelease") {
it.dependsOn(lintTask)
it.enabled = false
}
// The lintAnalyzeDebug task is used by `androidx-studio-integration-lint.sh`.
tasks.register("lintAnalyzeDebug") { it.enabled = false }
addToBuildOnServer(lintTask)
// For Android projects, we can run lint configuration last using `DslLifecycle.finalizeDsl`;
// however, we need to run it using `Project.afterEvaluate` for non-Android projects.
configureLint(project.extensions.getByType(), isLibrary = true)
}
/**
* Registers the `lintDebug` task if there are debug variants present.
*
* This method *must* run after evaluation.
*/
private fun Project.registerLintDebugIfNeededAfterEvaluate() {
val variantNames = project.agpVariants.map { it.name }
if (!variantNames.contains("debug")) {
tasks.register("lintDebug") { task ->
// The lintDebug tasks depends on lint tasks for all debug variants.
variantNames
.filter { it.contains("debug", ignoreCase = true) }
.map { tasks.named("lint${it.camelCase()}") }
.forEach { task.dependsOn(it) }
}
}
}
private fun String.camelCase() =
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
/**
* If the project is targeting Android and using the AIDL build feature, installs AIDL source
* directories on lint tasks.
*
* Adapted from AndroidXComposeImplPlugin's `configureLintForMultiplatformLibrary` extension
* function. See b/189250111 for AGP feature request.
*
* The `UnstableAidlAnnotationDetector` check from `lint-checks` requires that _only_ unstable AIDL
* files are passed to Lint, e.g. files in the AGP-defined `aidl` source set but not files in the
* Stable AIDL plugin-defined `stableAidl` source set. If we decide to lint Stable AIDL files, we'll
* need some other way to distinguish stable from unstable AIDL.
*
* This method *must* run after evaluation.
*/
private fun Project.configureLintForAidlAfterEvaluate() {
// BaseExtension needed to access resolved source files on `aidl`.
val extension = project.extensions.findByType<BaseExtension>() ?: return
val mainAidl = extension.sourceSets.getByName("main").aidl.getSourceFiles()
/** Helper function to add the missing sourcesets to this [VariantInputs] */
fun VariantInputs.addSourceSets() {
// Each variant has a source provider for the variant (such as debug) and the 'main'
// variant. The actual files that Lint will run on is both of these providers
// combined - so we can just add the dependencies to the first we see.
val variantAidl = extension.sourceSets.getByName(name.get()).aidl.getSourceFiles()
val sourceProvider = sourceProviders.get().firstOrNull() ?: return
sourceProvider.javaDirectories.withChangesAllowed { from(mainAidl, variantAidl) }
}
// Lint for libraries is split into two tasks - analysis, and reporting. We need to
// add the new sources to both, so all parts of the pipeline are aware.
project.tasks.withType<AndroidLintAnalysisTask>().configureEach {
it.variantInputs.addSourceSets()
}
project.tasks.withType<AndroidLintTask>().configureEach {
it.variantInputs.addSourceSets()
}
// Also configure the model writing task, so that we don't run into mismatches between
// analyzed sources in one module and a downstream module
project.tasks.withType<LintModelWriterTask>().configureEach {
it.variantInputs.addSourceSets()
}
}
/**
* If the project is using multiplatform, adds configurations and source sets expected by the lint
* plugin, which allows it to configure itself when running against a non-Android multiplatform
* project.
*
* The version of lint that we're using does not directly support Kotlin multiplatform, but we can
* synthesize the necessary configurations and source sets from existing `jvm` configurations and
* `kotlinSourceSets`, respectively.
*
* This method *must* run after evaluation.
*/
private fun Project.addSourceSetsForMultiplatformAfterEvaluate() {
val kmpTargets = project.multiplatformExtension?.targets ?: return
// Synthesize target configurations based on multiplatform configurations.
val kmpApiElements = kmpTargets.map { it.apiElementsConfigurationName }
val kmpRuntimeElements = kmpTargets.map { it.runtimeElementsConfigurationName }
listOf(
kmpRuntimeElements to "runtimeElements",
kmpApiElements to "apiElements"
).forEach { (kmpConfigNames, targetConfigName) ->
project.configurations.maybeCreate(targetConfigName).apply {
kmpConfigNames
.mapNotNull { configName -> project.configurations.findByName(configName) }
.forEach { config -> extendsFrom(config) }
}
}
// Synthesize source sets based on multiplatform source sets.
val javaExtension = project.extensions.findByType(JavaPluginExtension::class.java)
?: throw GradleException("Failed to find extension of type 'JavaPluginExtension'")
listOf(
"main" to "main",
"test" to "test"
).forEach { (kmpCompilationName, targetSourceSetName) ->
javaExtension.sourceSets.maybeCreate(targetSourceSetName).apply {
kmpTargets
.mapNotNull { target -> target.compilations.findByName(kmpCompilationName) }
.flatMap { compilation -> compilation.kotlinSourceSets }
.flatMap { sourceSet -> sourceSet.kotlin.srcDirs }
.forEach { srcDirs -> java.srcDirs += srcDirs }
}
}
}
/**
* If the project is using multiplatform targeted to Android, adds source sets directly to lint
* tasks, which allows it to run against Android multiplatform projects.
*
* Lint is not aware of MPP, and MPP doesn't configure Lint. There is no built-in API to adjust the
* default Lint task's sources, so we use this hack to manually add sources for MPP source sets. In
* the future, with the new Kotlin Project Model (https://youtrack.jetbrains.com/issue/KT-42572) and
* an AGP / MPP integration plugin, this will no longer be needed. See also b/195329463.
*/
private fun Project.addSourceSetsForAndroidMultiplatformAfterEvaluate() {
val multiplatformExtension = project.multiplatformExtension ?: return
multiplatformExtension.targets.findByName("android") ?: return
val androidMain = multiplatformExtension.sourceSets.findByName("androidMain")
?: throw GradleException("Failed to find source set with name 'androidMain'")
// Get all the source sets androidMain transitively / directly depends on.
val dependencySourceSets = androidMain.withClosure(KotlinSourceSet::dependsOn)
/**
* Helper function to add the missing sourcesets to this [VariantInputs]
*/
fun VariantInputs.addSourceSets() {
// Each variant has a source provider for the variant (such as debug) and the 'main'
// variant. The actual files that Lint will run on is both of these providers
// combined - so we can just add the dependencies to the first we see.
val sourceProvider = sourceProviders.get().firstOrNull() ?: return
dependencySourceSets.forEach { sourceSet ->
sourceProvider.javaDirectories.withChangesAllowed {
from(sourceSet.kotlin.sourceDirectories)
}
}
}
// Lint for libraries is split into two tasks - analysis, and reporting. We need to
// add the new sources to both, so all parts of the pipeline are aware.
project.tasks.withType<AndroidLintAnalysisTask>().configureEach {
it.variantInputs.addSourceSets()
}
project.tasks.withType<AndroidLintTask>().configureEach { it.variantInputs.addSourceSets() }
// Also configure the model writing task, so that we don't run into mismatches between
// analyzed sources in one module and a downstream module
project.tasks.withType<LintModelWriterTask>().configureEach {
it.variantInputs.addSourceSets()
}
}
private fun Project.configureLint(lint: Lint, isLibrary: Boolean) {
val extension = project.androidXExtension
val isMultiplatform = project.multiplatformExtension != null
val lintChecksProject =
project.rootProject.findProject(":lint-checks")
?: if (allowMissingLintProject()) {
return
} else {
throw GradleException("Project :lint-checks does not exist")
}
project.dependencies.add("lintChecks", lintChecksProject)
afterEvaluate {
addSourceSetsForAndroidMultiplatformAfterEvaluate()
}
// The purpose of this specific project is to test that lint is running, so
// it contains expected violations that we do not want to trigger a build failure
val isTestingLintItself = (project.path == ":lint-checks:integration-tests")
lint.apply {
// Skip lintVital tasks on assemble. We explicitly run lintRelease for libraries.
checkReleaseBuilds = false
}
tasks.withType(AndroidLintTask::class.java).configureEach { task ->
// Remove the lint and column attributes from generated lint baseline XML.
if (task.name.startsWith("updateLintBaseline")) {
task.doLast {
task.projectInputs.lintOptions.baseline.orNull?.asFile?.let { file ->
if (file.exists()) {
file.writeText(removeLineAndColumnAttributes(file.readText()))
}
}
}
}
}
// Lint is configured entirely in finalizeDsl so that individual projects cannot easily
// disable individual checks in the DSL for any reason.
lint.apply {
if (!isTestingLintItself) {
abortOnError = true
}
ignoreWarnings = true
// Run lint on tests. Uses top-level lint.xml to specify checks.
checkTestSources = true
// Write output directly to the console (and nowhere else).
textReport = true
htmlReport = false
// Format output for convenience.
explainIssues = true
noLines = false
quiet = true
// We run lint on each library, so we don't want transitive checking of each dependency
checkDependencies = false
if (extension.type.allowCallingVisibleForTestsApis) {
// Test libraries are allowed to call @VisibleForTests code
disable.add("VisibleForTests")
} else {
fatal.add("VisibleForTests")
}
if (isMultiplatform) {
// Disable classfile-based checks because lint cannot find the class files for
// multiplatform projects and `SourceSet.java.classesDirectory` is not configurable.
// This is not ideal, but it's better than having no lint checks at all.
disable.add("LintError")
}
// Reenable after b/238892319 is resolved
disable.add("NotificationPermission")
// Disable dependency checks that suggest to change them. We want libraries to be
// intentional with their dependency version bumps.
disable.add("KtxExtensionAvailable")
disable.add("GradleDependency")
// Disable a check that's only relevant for real apps. For our test apps we're not
// concerned with drawables potentially being a little bit blurry
disable.add("IconMissingDensityFolder")
// Disable until it works for our projects, b/171986505
disable.add("JavaPluginLanguageLevel")
// Explicitly disable StopShip check (see b/244617216)
disable.add("StopShip")
// Broken in 7.0.0-alpha15 due to b/180408990
disable.add("RestrictedApi")
// Disable until ag/19949626 goes in (b/261918265)
disable.add("MissingQuantity")
// Provide stricter enforcement for project types intended to run on a device.
if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
fatal.add("Assert")
fatal.add("NewApi")
fatal.add("ObsoleteSdkInt")
fatal.add("NoHardKeywords")
fatal.add("UnusedResources")
fatal.add("KotlinPropertyAccess")
fatal.add("LambdaLast")
fatal.add("UnknownNullness")
// Too many Kotlin features require synthetic accessors - we want to rely on R8 to
// remove these accessors
disable.add("SyntheticAccessor")
// Only check for missing translations in finalized (beta and later) modules.
if (extension.mavenVersion?.isFinalApi() == true) {
fatal.add("MissingTranslation")
} else {
disable.add("MissingTranslation")
}
} else {
disable.add("BanUncheckedReflection")
}
// Broken in 7.0.0-alpha15 due to b/187343720
disable.add("UnusedResources")
// Disable NullAnnotationGroup check for :compose:ui:ui-text (b/233788571)
if (isLibrary && project.group == "androidx.compose.ui" && project.name == "ui-text") {
disable.add("NullAnnotationGroup")
}
if (extension.type == LibraryType.SAMPLES) {
// TODO: b/190833328 remove if / when AGP will analyze dependencies by default
// This is needed because SampledAnnotationDetector uses partial analysis, and
// hence requires dependencies to be analyzed.
checkDependencies = true
}
// Only run certain checks where API tracking is important.
if (extension.type.checkApi is RunApiTasks.No) {
disable.add("IllegalExperimentalApiUsage")
}
// If the project has not overridden the lint config, set the default one.
if (lintConfig == null) {
val lintXmlPath =
if (extension.type == LibraryType.SAMPLES) {
"buildSrc/lint_samples.xml"
} else {
"buildSrc/lint.xml"
}
// suppress warnings more specifically than issue-wide severity (regexes)
// Currently suppresses warnings from baseline files working as intended
lintConfig = File(project.getSupportRootFolder(), lintXmlPath)
}
baseline = lintBaseline.get().asFile
}
}
/**
* Lint uses [ConfigurableFileCollection.disallowChanges] during initialization, which prevents
* modifying the file collection separately (there is no time to configure it before AGP has
* initialized and disallowed changes). This uses reflection to temporarily allow changes, and apply
* [block].
*/
private fun ConfigurableFileCollection.withChangesAllowed(
block: ConfigurableFileCollection.() -> Unit
) {
val disallowChanges = this::class.java.getDeclaredField("disallowChanges")
disallowChanges.isAccessible = true
disallowChanges.set(this, false)
block()
disallowChanges.set(this, true)
}
private val Project.lintBaseline: RegularFileProperty
get() = project.objects.fileProperty().fileValue(File(projectDir, "lint-baseline.xml"))