blob: 38d3c51694c2dcb49d79f1beeb55bdcbe97d626a [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 androidx.build.dependencyTracker.AffectedModuleDetector
import com.android.build.gradle.internal.dsl.LintOptions
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.getByType
import java.io.File
import java.util.Locale
/**
* Setting this property means that lint will update lint-baseline.xml if it exists.
*/
private const val UPDATE_LINT_BASELINE = "updateLintBaseline"
/**
* Property used by Lint to continue creating baselines without failing lint, normally set by:
* -Dlint.baselines.continue=true from command line.
*/
private const val LINT_BASELINE_CONTINUE = "lint.baselines.continue"
fun Project.configureNonAndroidProjectForLint(extension: AndroidXExtension) {
apply(mapOf("plugin" to "com.android.lint"))
// Create fake variant tasks since that is what is invoked by developers.
val lintTask = tasks.named("lint")
lintTask.configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
}
afterEvaluate {
tasks.named("lintAnalyze").configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
}
/* TODO: uncomment when we upgrade to AGP 7.1.0-alpha04
tasks.named("lintReport").configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
}*/
}
tasks.register("lintDebug") {
it.dependsOn(lintTask)
it.enabled = false
}
tasks.register("lintAnalyzeDebug") {
it.dependsOn(lintTask)
it.enabled = false
}
tasks.register("lintRelease") {
it.dependsOn(lintTask)
it.enabled = false
}
addToBuildOnServer(lintTask)
val lintOptions = extensions.getByType<LintOptions>()
configureLint(lintOptions, extension)
}
fun Project.configureAndroidProjectForLint(lintOptions: LintOptions, extension: AndroidXExtension) {
project.afterEvaluate {
// makes sure that the lintDebug task will exist, so we can find it by name
setUpLintDebugIfNeeded()
}
tasks.register("lintAnalyze") {
it.dependsOn("lintDebug")
it.enabled = false
}
configureLint(lintOptions, extension)
tasks.named("lint").configure { task ->
// We already run lintDebug, we don't need to run lint which lints the release variant
task.enabled = false
}
afterEvaluate {
for (variant in project.agpVariants) {
tasks.named(
"lint${variant.name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString()
}}"
).configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
}
tasks.named(
"lintAnalyze${variant.name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString()
}}"
).configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
}
/* TODO: uncomment when we upgrade to AGP 7.1.0-alpha04
tasks.named("lintReport${variant.name.capitalize(Locale.US)}").configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
}*/
}
}
}
private fun Project.setUpLintDebugIfNeeded() {
val variants = project.agpVariants
val variantNames = variants.map { v -> v.name }
if (!variantNames.contains("debug")) {
tasks.register("lintDebug") {
for (variantName in variantNames) {
if (variantName.lowercase(Locale.US).contains("debug")) {
it.dependsOn(
tasks.named(
"lint${variantName.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString()
}}"
)
)
}
}
}
}
}
@Suppress("DEPRECATION") // lintOptions methods
fun Project.configureLint(lintOptions: LintOptions, extension: AndroidXExtension) {
project.dependencies.add(
"lintChecks",
project.rootProject.project(":lint-checks")
)
// 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")
// If -PupdateLintBaseline was set we should update the baseline if it exists
val updateLintBaseline = hasProperty(UPDATE_LINT_BASELINE) && !isTestingLintItself
lintOptions.apply {
// Skip lintVital tasks on assemble. We explicitly run lintRelease for libraries.
isCheckReleaseBuilds = false
}
// Lint is configured entirely in finalizeDsl so that individual projects cannot easily
// disable individual checks in the DSL for any reason.
val finalizeDsl: () -> Unit = {
lintOptions.apply {
if (!isTestingLintItself) {
isAbortOnError = true
}
isIgnoreWarnings = true
// Run lint on tests. Uses top-level lint.xml to specify checks.
isCheckTestSources = true
// Write output directly to the console (and nowhere else).
textReport = true
htmlReport = false
// Format output for convenience.
isExplainIssues = true
isNoLines = false
isQuiet = true
// We run lint on each library, so we don't want transitive checking of each dependency
isCheckDependencies = false
fatal("VisibleForTests")
// Disable dependency checks that suggest to change them. We want libraries to be
// intentional with their dependency version bumps.
disable("KtxExtensionAvailable")
disable("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("IconMissingDensityFolder")
// Disable a check that's only triggered by translation updates which are
// outside of library owners' control, b/174655193
disable("UnusedQuantity")
// Disable until it works for our projects, b/171986505
disable("JavaPluginLanguageLevel")
// Disable the TODO check until we have a policy that requires it.
disable("StopShip")
// Broken in 7.0.0-alpha15 due to b/180408990
disable("RestrictedApi")
// Broken in 7.0.0-alpha15 due to b/187508590
disable("InvalidPackage")
// Provide stricter enforcement for project types intended to run on a device.
if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
fatal("Assert")
fatal("NewApi")
fatal("ObsoleteSdkInt")
fatal("NoHardKeywords")
fatal("UnusedResources")
fatal("KotlinPropertyAccess")
fatal("LambdaLast")
fatal("UnknownNullness")
fatal("SupportAnnotationUsage")
// Only override if not set explicitly.
// Some Kotlin projects may wish to disable this.
if (
severityOverrides!!["SyntheticAccessor"] == null &&
extension.type != LibraryType.SAMPLES
) {
fatal("SyntheticAccessor")
}
// Only check for missing translations in finalized (beta and later) modules.
if (extension.mavenVersion?.isFinalApi() == true) {
fatal("MissingTranslation")
} else {
disable("MissingTranslation")
}
} else {
disable("BanUncheckedReflection")
}
// Broken in 7.0.0-alpha15 due to b/187343720
disable("UnusedResources")
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.
isCheckDependencies = true
// TODO: baselines from dependencies aren't used when we run lint with
// isCheckDependencies = true. NewApi was recently enabled for tests, and so
// there are a large amount of baselined issues that would be reported here
// again, and we don't want to add them to the baseline for the sample modules.
// Instead just temporarily disable this lint check until the underlying issues
// are fixed.
disable("NewApi")
}
// Only run certain checks where API tracking is important.
if (extension.type.checkApi is RunApiTasks.No) {
disable("IllegalExperimentalApiUsage")
}
// If the project has not overridden the lint config, set the default one.
if (lintConfig == null) {
// suppress warnings more specifically than issue-wide severity (regexes)
// Currently suppresses warnings from baseline files working as intended
lintConfig = File(project.getSupportRootFolder(), "buildSrc/lint.xml")
}
// Ideally, teams aren't able to add new violations to a baseline file; they should only
// be able to burn down existing violations. That's hard to enforce, though, so we'll
// generally allow teams to update their baseline files with a publicly-known flag.
if (updateLintBaseline) {
// Continue generating baselines regardless of errors.
isAbortOnError = false
// Avoid printing every single lint error to the terminal.
textReport = false
// Analyze tasks are responsible for reading baselines and detecting issues, but
// they won't detect any issues that are already in the baselines. Delete them
// before the task evaluates up-to-date-ness using:
//
// find . -type f -name lint-baseline.xml -exec rm -f {} \;
// Regular lint tasks are responsible for reading the output of analyze tasks and
// generating baseline files. They will fail if they generate a new baseline but
// there are no issues, so we need to delete the file as a finalization step using:
//
// find . -type f -name lint-baseline.xml \
// -exec awk -v x=5 'NR==x{exit 1}' {} \; \
// -exec rm -f {} \;
// Continue running after errors or after creating a new, blank baseline file.
// This doesn't work right now due to b/188545420, but it's technically correct.
System.setProperty(LINT_BASELINE_CONTINUE, "true")
}
// Lint complains when it generates a new, blank baseline file so we'll just avoid
// telling it about the baseline if one doesn't already exist OR we're explicitly
// updating (and creating) baseline files.
if (updateLintBaseline or lintBaseline.exists()) {
baseline(lintBaseline)
}
}
}
// TODO(aurimas): migrate away from this when upgrading to AGP 7.1.0-alpha03 or newer
@Suppress("UnstableApiUsage", "DEPRECATION")
val androidComponents = extensions.findByType(
com.android.build.api.extension.AndroidComponentsExtension::class.java
)
if (null != androidComponents) {
@Suppress("UnstableApiUsage")
androidComponents.finalizeDsl { finalizeDsl() }
} else {
// Support the lint standalone plugin case which, as yet, lacks AndroidComponents DSL
afterEvaluate { finalizeDsl() }
}
}
val Project.lintBaseline get() = File(projectDir, "/lint-baseline.xml")
/**
* Task that removes the specified `lint-baseline.xml` file if it does not contain any issues.
*/
abstract class RemoveEmptyBaselineTask : DefaultTask() {
@get:InputFile
abstract val baselineFile: RegularFileProperty
@TaskAction
fun removeEmptyBaseline() {
val lintBaseline = baselineFile.get().asFile
if (lintBaseline.exists()) {
// Does the baseline contain any issues?
val hasAnyIssues = lintBaseline.reader().useLines { lines ->
lines.any { line ->
line.endsWith("<issue")
}
}
if (!hasAnyIssues) {
lintBaseline.delete()
println("Deleted empty baseline file ${lintBaseline.path}")
}
}
}
}
/**
* Task that removes the specified `lint-baseline.xml` file.
*/
abstract class RemoveBaselineTask : DefaultTask() {
@get:InputFiles // allows missing files
abstract val baselineFile: RegularFileProperty
@TaskAction
fun removeBaseline() {
val lintBaseline = baselineFile.get().asFile
if (lintBaseline.exists()) {
lintBaseline.delete()
}
}
}