Add checks to ensure atomic library groups are read in properly
The checks ensure that the:
1) filename property is properly set in lint.xml
2) file exists
3) file should not be empty
This change also:
- Updates the atomic library group text file location from scoped (generated once per library and stored under each library's subdirectory) to unscoped (single location not nested under a library)
- Changes the export task to only run in the root plugin, rather than per lint project
- Sets the system OUT_DIR environment variable if it isn't set.
Bug: 162069062
Bug: 204371669
Test: BanInappropriateExperimentalUsageTest
Test: bOS
Change-Id: I7c6c00eb4492bc63714c341a1944c58da36b7e5a
diff --git a/buildSrc/lint.xml b/buildSrc/lint.xml
index 5ec6632..08fefd5 100644
--- a/buildSrc/lint.xml
+++ b/buildSrc/lint.xml
@@ -32,6 +32,19 @@
<!-- Required for AppSearch icing tests. -->
<ignore path="**/java/tests/**" />
</issue>
+
+ <issue id="IllegalExperimentalApiUsage">
+
+ <!--
+ The placeholder string "USE_SYSTEM_OUT_DIR" must match the string
+ ATOMIC_LIBRARY_GROUP_FILE_PLACEHOLDER defined in BanInappropriateExperimentalUsage.
+ The suffix must match `fileLocation` defined in AndroidXRootImplPlugin.
+ -->
+ <option
+ name="atomicLibraryGroupFilename"
+ value="USE_SYSTEM_OUT_DIR/lint/atomic-library-groups.txt"/>
+ </issue>
+
<!-- Re-enable high-priority checks for tests (and everything else). -->
<issue id="NewApi" severity="fatal" />
<issue id="WrongThread" severity="fatal" />
diff --git a/buildSrc/out.gradle b/buildSrc/out.gradle
index be42e80..10c8ad8 100644
--- a/buildSrc/out.gradle
+++ b/buildSrc/out.gradle
@@ -29,6 +29,7 @@
}
outDir = new File("${checkoutRoot}/out${subdir}")
project.ext.buildSrcOut = new File("${checkoutRoot}/out/buildSrc")
+ environment "OUT_DIR", "${checkoutRoot}/out${subdir}"
} else {
outDir = new File(outDir)
project.ext.buildSrcOut = new File("${outDir}/buildSrc")
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 291728eb..33f88d0 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -113,7 +113,6 @@
project.configureTaskTimeouts()
project.configureMavenArtifactUpload(extension)
- project.configureExportAtomicLibraryGroupsToText()
project.configureExternalDependencyLicenseCheck()
project.configureProjectStructureValidation(extension)
project.configureProjectVersionValidation(extension)
@@ -393,17 +392,6 @@
project.addToProjectMap(extension)
}
- private fun Project.configureExportAtomicLibraryGroupsToText() {
- project.tasks.register(
- "exportAtomicLibraryGroupsToText",
- ExportAtomicLibraryGroupsToTextTask::class.java
- ) { task ->
- task.libraryGroupFile = project.file("${project.getSupportRootFolder()}" +
- "/buildSrc/public/src/main/kotlin/androidx/build/LibraryGroups.kt")
- task.textOutputFile = project.file("${project.buildDir}/lint/atomic-library-groups.txt")
- }
- }
-
private fun Project.configureProjectStructureValidation(
extension: AndroidXExtension
) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 07922a9..00a1892 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -92,6 +92,20 @@
tasks.register(AndroidXImplPlugin.CREATE_LIBRARY_BUILD_INFO_FILES_TASK)
)
+ buildOnServerTask.dependsOn(
+ tasks.register(
+ "exportAtomicLibraryGroupsToTextRoot",
+ ExportAtomicLibraryGroupsToTextTask::class.java
+ ) { task ->
+ task.libraryGroupFile = project.file("${project.getSupportRootFolder()}" +
+ "/buildSrc/public/src/main/kotlin/androidx/build/LibraryGroups.kt")
+
+ // This suffix must match the suffix in buildSrc/lint.xml
+ val fileLocation = "${getRootOutDirectory()}/lint/atomic-library-groups.txt"
+ task.textOutputFile = project.file(fileLocation)
+ }
+ )
+
VerifyPlaygroundGradleConfigurationTask.createIfNecessary(project)?.let {
buildOnServerTask.dependsOn(it)
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 6345639..53d7c42 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -72,7 +72,6 @@
// Create fake variant tasks since that is what is invoked by developers.
val lintTask = tasks.named("lint")
lintTask.configure { task ->
- task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
AffectedModuleDetector.configureTaskGuard(task)
}
afterEvaluate {
@@ -123,7 +122,6 @@
}}"
).configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
- task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
}
tasks.named(
"lintAnalyze${variant.name.replaceFirstChar {
@@ -131,7 +129,6 @@
}}"
).configure { task ->
AffectedModuleDetector.configureTaskGuard(task)
- task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
}
/* TODO: uncomment when we upgrade to AGP 7.1.0-alpha04
tasks.named("lintReport${variant.name.capitalize(Locale.US)}").configure { task ->
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
index c5ac47b..37e2930 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
@@ -18,6 +18,7 @@
package androidx.build.lint
+import com.android.tools.lint.client.api.Configuration
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
@@ -27,6 +28,8 @@
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
+import java.io.File
+import java.io.FileNotFoundException
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UClass
@@ -45,6 +48,10 @@
}
private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
+ val atomicGroupList: List<String> by lazy {
+ loadAtomicLibraryGroupData(context.configuration, ISSUE)
+ }
+
override fun visitAnnotation(node: UAnnotation) {
if (DEBUG) {
if (APPLICABLE_ANNOTATIONS.contains(node.qualifiedName) && node.sourcePsi != null) {
@@ -68,17 +75,55 @@
"${context.project}"
)
}
- verifyUsageOfElementIsWithinSameGroup(context, node, annotation, ISSUE)
+
+ verifyUsageOfElementIsWithinSameGroup(
+ context,
+ node,
+ annotation,
+ ISSUE,
+ atomicGroupList
+ )
}
}
}
+
+ private fun loadAtomicLibraryGroupData(
+ configuration: Configuration,
+ issue: Issue
+ ): List<String> {
+ val filename = configuration.getOption(issue, ATOMIC_LIBGROUP_FILE_PROPERTY, null)
+ ?: throw RuntimeException(
+ "Property $ATOMIC_LIBGROUP_FILE_PROPERTY is not set in lint.xml.")
+
+ val libGroupFile = if (filename.contains(OUT_DIR_PLACEHOLDER)) {
+ val fileLocation = filename.replace(OUT_DIR_PLACEHOLDER, System.getenv("OUT_DIR"))
+ val file = File(fileLocation)
+ if (!file.exists()) {
+ throw FileNotFoundException("Couldn't find atomic library group file $filename")
+ }
+ file
+ } else {
+ configuration.getOptionAsFile(issue, ATOMIC_LIBGROUP_FILE_PROPERTY, null)
+ ?: throw FileNotFoundException(
+ "Couldn't find atomic library group file $filename")
+ }
+
+ val atomicLibraryGroups = libGroupFile.readLines()
+ if (atomicLibraryGroups.isEmpty()) {
+ throw RuntimeException("Atomic library group file should not be empty")
+ }
+
+ return atomicLibraryGroups
+ }
}
+ @Suppress("UNUSED_PARAMETER") // TODO: write logic + tests in future CL that uses groupList
fun verifyUsageOfElementIsWithinSameGroup(
context: JavaContext,
usage: UElement,
annotation: UElement,
issue: Issue,
+ atomicGroupList: List<String>,
) {
val evaluator = context.evaluator
val usageCoordinates = evaluator.getLibrary(usage) ?: context.project.mavenCoordinate
@@ -105,6 +150,17 @@
private const val DEBUG = false
/**
+ * This string must match the value defined in buildSrc/lint.xml
+ *
+ * This is needed as we need to define the location of the atomic library group file for
+ * non-test usage. For tests, we can directly use the value defined in test lint.xml files.
+ */
+ private const val OUT_DIR_PLACEHOLDER = "USE_SYSTEM_OUT_DIR"
+
+ // This must match the setting in buildSrc/lint.xml
+ private const val ATOMIC_LIBGROUP_FILE_PROPERTY = "atomicLibraryGroupFilename"
+
+ /**
* Even though Kotlin's [Experimental] annotation is deprecated in favor of [RequiresOptIn],
* we still want to check for its use in Lint.
*/
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
index 3003f41..a0c0e6f 100644
--- a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
@@ -19,10 +19,13 @@
package androidx.build.lint
import com.android.tools.lint.checks.infrastructure.ProjectDescription
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestMode
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import java.io.FileNotFoundException
@RunWith(JUnit4::class)
class BanInappropriateExperimentalUsageTest : AbstractLintDetectorTest(
@@ -30,6 +33,27 @@
useIssues = listOf(BanInappropriateExperimentalUsage.ISSUE),
stubs = arrayOf(Stubs.OptIn),
) {
+ // This must match the setting in buildSrc/lint.xml
+ private val validLintConfig: TestFile = TestFiles.xml(
+ "lint.xml",
+ """
+<lint>
+ <issue id="IllegalExperimentalApiUsage">
+ <option name="atomicLibraryGroupFilename" value="atomic-library-groups.txt"/>
+ </issue>
+</lint>
+ """.trimIndent()
+ )
+
+ private val libraryGroups = TestFiles.source(
+ "atomic-library-groups.txt",
+ """
+androidx.a
+androidx.b
+ """.trimIndent()
+ )
+
+ private val UNUSED_PLACEHOLDER = "unused"
@Test
fun `Test within-module Experimental usage via Gradle model`() {
@@ -44,6 +68,8 @@
group=sample.annotation.provider
"""
).indented(),
+ validLintConfig,
+ libraryGroups,
)
/* ktlint-disable max-line-length */
@@ -85,6 +111,8 @@
group=androidx.sample.consumer
"""
).indented(),
+ validLintConfig,
+ libraryGroups,
)
/* ktlint-disable max-line-length */
@@ -102,4 +130,128 @@
// TODO: Using TestMode.DEFAULT due to b/188814760; remove testModes once bug is resolved
check(provider, consumer, testModes = listOf(TestMode.DEFAULT)).expect(expected)
}
+
+ @Test(expected = RuntimeException::class)
+ fun `Missing atomicLibraryGroupFilename property should throw an exception`() {
+ val invalidLintConfig: TestFile = TestFiles.xml(
+ "lint.xml",
+ """
+<lint>
+ <issue id="IllegalExperimentalApiUsage">
+ <option name="foo" value="bar"/>
+ </issue>
+</lint>
+ """.trimIndent()
+ )
+ val provider = project()
+ .name("provider")
+ .type(ProjectDescription.Type.LIBRARY)
+ .report(false)
+ .files(
+ ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
+ javaSample("sample.annotation.provider.ExperimentalSampleAnnotationJava"),
+ javaSample("sample.annotation.provider.RequiresOptInSampleAnnotationJava"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=sample.annotation.provider
+ """
+ ).indented(),
+ )
+
+ val consumer = project()
+ .name("consumer")
+ .type(ProjectDescription.Type.LIBRARY)
+ .dependsOn(provider)
+ .files(
+ ktSample("androidx.sample.consumer.OutsideGroupExperimentalAnnotatedClass"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=androidx.sample.consumer
+ """
+ ).indented(),
+ invalidLintConfig,
+ )
+
+ // TODO: Using TestMode.DEFAULT due to b/188814760; remove testModes once bug is resolved
+ check(provider, consumer, testModes = listOf(TestMode.DEFAULT)).expect(UNUSED_PLACEHOLDER)
+ }
+
+ @Test(expected = FileNotFoundException::class)
+ fun `Missing atomic library group file should throw an exception`() {
+ val provider = project()
+ .name("provider")
+ .type(ProjectDescription.Type.LIBRARY)
+ .report(false)
+ .files(
+ ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
+ javaSample("sample.annotation.provider.ExperimentalSampleAnnotationJava"),
+ javaSample("sample.annotation.provider.RequiresOptInSampleAnnotationJava"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=sample.annotation.provider
+ """
+ ).indented(),
+ )
+
+ val consumer = project()
+ .name("consumer")
+ .type(ProjectDescription.Type.LIBRARY)
+ .dependsOn(provider)
+ .files(
+ ktSample("androidx.sample.consumer.OutsideGroupExperimentalAnnotatedClass"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=androidx.sample.consumer
+ """
+ ).indented(),
+ validLintConfig,
+ )
+
+ // TODO: Using TestMode.DEFAULT due to b/188814760; remove testModes once bug is resolved
+ check(provider, consumer, testModes = listOf(TestMode.DEFAULT)).expect(UNUSED_PLACEHOLDER)
+ }
+
+ @Test(expected = RuntimeException::class)
+ fun `Empty atomic library group file should throw an exception`() {
+ val emptyLibraryGroups = TestFiles.source("atomic-library-groups.txt", "")
+
+ val provider = project()
+ .name("provider")
+ .type(ProjectDescription.Type.LIBRARY)
+ .report(false)
+ .files(
+ ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
+ javaSample("sample.annotation.provider.ExperimentalSampleAnnotationJava"),
+ javaSample("sample.annotation.provider.RequiresOptInSampleAnnotationJava"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=sample.annotation.provider
+ """
+ ).indented(),
+ )
+
+ val consumer = project()
+ .name("consumer")
+ .type(ProjectDescription.Type.LIBRARY)
+ .dependsOn(provider)
+ .files(
+ ktSample("androidx.sample.consumer.OutsideGroupExperimentalAnnotatedClass"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=androidx.sample.consumer
+ """
+ ).indented(),
+ validLintConfig,
+ emptyLibraryGroups,
+ )
+
+ // TODO: Using TestMode.DEFAULT due to b/188814760; remove testModes once bug is resolved
+ check(provider, consumer, testModes = listOf(TestMode.DEFAULT)).expect(UNUSED_PLACEHOLDER)
+ }
}