Add @Experimental annotation and Lint check
Bug: 135553341
Test: ExperimentalChecksTest
Change-Id: I25317eeeaf4e65fac6dcd485c87218f3b08037b1
diff --git a/annotation/annotation-experimental-lint/build.gradle b/annotation/annotation-experimental-lint/build.gradle
new file mode 100644
index 0000000..5d9a4a13
--- /dev/null
+++ b/annotation/annotation-experimental-lint/build.gradle
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.CompilationTarget
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+sourceSets {
+ // Make sample code and annotations available within JAR resources.
+ test.resources.srcDirs(
+ project(":annotation:annotation-experimental-lint-integration-tests").projectDir.toString() + "/src/main/java",
+ project(":annotation:annotation-experimental").projectDir.toString() + "/src/main/java"
+ )
+}
+
+dependencies {
+ compileOnly LINT_API_LATEST
+ compileOnly KOTLIN_STDLIB
+
+ testImplementation KOTLIN_STDLIB
+ testImplementation LINT_CORE
+ testImplementation LINT_TESTS
+}
+
+androidx {
+ name = "Experimental annotation lint checks"
+ toolingProject = true
+ publish = Publish.NONE
+ mavenVersion = LibraryVersions.ANNOTATION
+ mavenGroup = LibraryGroups.ANNOTATION
+ inceptionYear = "2019"
+ description = "Lint checks for the experimental annotation library"
+ url = ARCHITECTURE_URL
+ compilationTarget = CompilationTarget.HOST
+}
diff --git a/annotation/annotation-experimental-lint/integration-tests/build.gradle b/annotation/annotation-experimental-lint/integration-tests/build.gradle
new file mode 100644
index 0000000..434e274
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ implementation(project(":annotation"))
+ implementation(project(":annotation:annotation-experimental"))
+}
+
+androidx {
+ name = "Integration test for Experimental"
+ publish = Publish.NONE
+ mavenVersion = LibraryVersions.ANNOTATION
+ mavenGroup = LibraryGroups.ANNOTATION
+ inceptionYear = "2019"
+ description = "Suite of integration tests for the Experimental annotation"
+}
diff --git a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
new file mode 100644
index 0000000..2fe55b4
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 3.5.0-beta04" client="gradle" variant="debug" version="3.5.0-beta04">
+
+ <issue
+ id="UnsafeExperimentalUsageError"
+ message="This declaration is experimental and its usage should be marked with
'@sample.ExperimentalDateTime' or '@UseExperimental(sample.ExperimentalDateTime.class)'"
+ errorLine1=" DateProvider provider = new DateProvider();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/sample/UseExperimentalClassUnchecked.java"
+ line="29"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="UnsafeExperimentalUsageError"
+ message="This declaration is experimental and its usage should be marked with
'@sample.ExperimentalDateTime' or '@UseExperimental(sample.ExperimentalDateTime.class)'"
+ errorLine1=" return provider.getDate();"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/sample/UseExperimentalClassUnchecked.java"
+ line="30"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="UnsafeExperimentalUsageError"
+ message="This declaration is experimental and its usage should be marked with
'@sample.ExperimentalDateTime' or '@UseExperimental(sample.ExperimentalDateTime.class)'"
+ errorLine1=" System.out.println(getDate());"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/sample/UseExperimentalMethodUnchecked.java"
+ line="35"
+ column="28"/>
+ </issue>
+
+</issues>
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/AndroidManifest.xml b/annotation/annotation-experimental-lint/integration-tests/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ed18b0e
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+-->
+<manifest package="androidx.annotation.experimental.lint.integrationtests" />
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/ExperimentalDateTime.java b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/ExperimentalDateTime.java
new file mode 100644
index 0000000..e5d7b5da
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/ExperimentalDateTime.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 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 sample;
+
+import static androidx.annotation.Experimental.Level.ERROR;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.Experimental;
+
+import java.lang.annotation.Retention;
+
+@Retention(CLASS)
+@Experimental(level = ERROR)
+@interface ExperimentalDateTime {}
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassChecked.java b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassChecked.java
new file mode 100644
index 0000000..981acf3
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassChecked.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 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 sample;
+
+@SuppressWarnings("unused")
+class UseExperimentalClassChecked {
+ @ExperimentalDateTime
+ class DateProvider {
+ int getDate() {
+ return -1;
+ }
+ }
+
+ @ExperimentalDateTime
+ int getDate() {
+ DateProvider provider = new DateProvider();
+ return provider.getDate();
+ }
+}
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassCheckedUses.java b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassCheckedUses.java
new file mode 100644
index 0000000..b192512
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassCheckedUses.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 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 sample;
+
+import androidx.annotation.UseExperimental;
+
+@SuppressWarnings("unused")
+class UseExperimentalClassCheckedUses {
+ @ExperimentalDateTime
+ class DateProvider {
+ int getDate() {
+ return -1;
+ }
+ }
+
+ @UseExperimental(markerClass = ExperimentalDateTime.class)
+ int getDate() {
+ DateProvider provider = new DateProvider();
+ return provider.getDate();
+ }
+}
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassUnchecked.java b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassUnchecked.java
new file mode 100644
index 0000000..758e2f3
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalClassUnchecked.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 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 sample;
+
+@SuppressWarnings("unused")
+class UseExperimentalClassUnchecked {
+ @ExperimentalDateTime
+ class DateProvider {
+ int getDate() {
+ return -1;
+ }
+ }
+
+ int getDate() {
+ DateProvider provider = new DateProvider();
+ return provider.getDate();
+ }
+}
diff --git a/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalMethodUnchecked.java b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalMethodUnchecked.java
new file mode 100644
index 0000000..6ca57f7
--- /dev/null
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/UseExperimentalMethodUnchecked.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 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 sample;
+
+@SuppressWarnings("unused")
+class UseExperimentalMethodUnchecked {
+ @ExperimentalDateTime
+ class DateProvider {
+ int getDate() {
+ return -1;
+ }
+ }
+
+ @ExperimentalDateTime
+ int getDate() {
+ DateProvider provider = new DateProvider();
+ return provider.getDate();
+ }
+
+ void displayDate() {
+ System.out.println(getDate());
+ }
+}
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/lint/ExperimentalDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/lint/ExperimentalDetector.kt
new file mode 100644
index 0000000..16ae8f6
--- /dev/null
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/lint/ExperimentalDetector.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2019 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.annotation.lint
+
+import com.android.tools.lint.detector.api.AnnotationUsageType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+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 com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UAnnotated
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UNamedExpression
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.getParentOfType
+
+class ExperimentalDetector : Detector(), SourceCodeScanner {
+ override fun applicableAnnotations(): List<String>? = listOf(
+ EXPERIMENTAL_ANNOTATION
+ )
+
+ override fun inheritAnnotation(annotation: String): Boolean = true
+
+ override fun visitAnnotationUsage(
+ context: JavaContext,
+ usage: UElement,
+ type: AnnotationUsageType,
+ annotation: UAnnotation,
+ qualifiedName: String,
+ method: PsiMethod?,
+ referenced: PsiElement?,
+ annotations: List<UAnnotation>,
+ allMemberAnnotations: List<UAnnotation>,
+ allClassAnnotations: List<UAnnotation>,
+ allPackageAnnotations: List<UAnnotation>
+ ) {
+ when (qualifiedName) {
+ EXPERIMENTAL_ANNOTATION -> {
+ checkExperimentalUsage(context, annotation, usage)
+ }
+ }
+ }
+
+ /**
+ * Check whether the given experimental API [annotation] can be referenced from [usage] call
+ * site.
+ *
+ * @param context the lint scanning context
+ * @param annotation the experimental annotation detected on the referenced element
+ * @param usage the element whose usage should be checked
+ */
+ private fun checkExperimentalUsage(
+ context: JavaContext,
+ annotation: UAnnotation,
+ usage: UElement
+ ) {
+ val useAnnotation = (annotation.uastParent as? UClass)?.qualifiedName ?: return
+ if (!hasOrUsesAnnotation(context, usage, useAnnotation)) {
+ val level = annotation.attributeValues[0].simpleName
+ ?: throw IllegalStateException("Failed to extract level from annotation")
+ report(context, usage, """
+ This declaration is experimental and its usage should be marked with
+ '@$useAnnotation' or '@UseExperimental($useAnnotation.class)'
+ """, level)
+ }
+ }
+
+ private fun hasOrUsesAnnotation(
+ context: JavaContext,
+ usage: UElement,
+ annotationName: String
+ ): Boolean {
+ var element: UAnnotated? = if (usage is UAnnotated) {
+ usage
+ } else {
+ usage.getParentOfType(UAnnotated::class.java)
+ }
+
+ while (element != null) {
+ val annotations = context.evaluator.getAllAnnotations(element, false)
+ val matchName = annotations.any { it.qualifiedName == annotationName }
+ val matchUse = annotations
+ .filter { annotation -> annotation.qualifiedName == USE_EXPERIMENTAL_ANNOTATION }
+ .mapNotNull { annotation -> annotation.attributeValues.getOrNull(0) }
+ .any { attrValue -> attrValue.getFullyQualifiedName(context) == annotationName }
+ if (matchName || matchUse) return true
+ element = element.getParentOfType(UAnnotated::class.java)
+ }
+ return false
+ }
+
+ /**
+ * Reports an issue and trims indentation on the [message].
+ */
+ private fun report(
+ context: JavaContext,
+ usage: UElement,
+ message: String,
+ level: String
+
+ ) {
+ val issue = when (level) {
+ "ERROR" -> ISSUE_ERROR
+ "WARNING" -> ISSUE_WARNING
+ else -> throw IllegalArgumentException("Level must be one of ERROR, WARNING")
+ }
+ context.report(issue, usage, context.getNameLocation(usage), message.trimIndent())
+ }
+
+ companion object {
+ private val IMPLEMENTATION = Implementation(
+ ExperimentalDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+
+ private const val EXPERIMENTAL_ANNOTATION = "androidx.annotation.Experimental"
+ private const val USE_EXPERIMENTAL_ANNOTATION = "androidx.annotation.UseExperimental"
+
+ private fun issueForLevel(level: String, severity: Severity): Issue = Issue.create(
+ id = "UnsafeExperimentalUsage${level.capitalize()}",
+ briefDescription = "Unsafe experimental usage intended to be $level-level severity",
+ explanation = """
+ This API has been flagged as experimental with $level-level severity.
+
+ Any declaration annotated with this marker is considered part of an unstable API \
+ surface and its call sites should accept the experimental aspect of it either by \
+ using `@UseExperimental`, or by being annotated with that marker themselves, \
+ effectively causing further propagation of that experimental aspect.
+ """,
+ category = Category.CORRECTNESS,
+ priority = 4,
+ severity = severity,
+ implementation = IMPLEMENTATION
+ )
+
+ val ISSUE_ERROR = issueForLevel("error", Severity.ERROR)
+ val ISSUE_WARNING = issueForLevel("warning", Severity.WARNING)
+ val ISSUES = listOf(ISSUE_ERROR, ISSUE_WARNING)
+ }
+}
+
+/**
+ * Returns the simple name (not qualified) associated with an expression, if any.
+ */
+private val UNamedExpression?.simpleName: String? get() = this?.let {
+ (expression as? USimpleNameReferenceExpression)?.identifier
+}
+
+/**
+ * Returns the fully-qualified class name for a given attribute value, if any.
+ */
+private fun UNamedExpression?.getFullyQualifiedName(context: JavaContext): String? =
+ this?.let { (evaluate() as? PsiClassType)?.let { context.evaluator.getQualifiedName(it) } }
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/lint/ExperimentalIssueRegistry.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/lint/ExperimentalIssueRegistry.kt
new file mode 100644
index 0000000..7c311b6
--- /dev/null
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/lint/ExperimentalIssueRegistry.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2019 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.annotation.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+
+class ExperimentalIssueRegistry : IssueRegistry() {
+ override val issues get() = ExperimentalDetector.ISSUES
+}
diff --git a/annotation/annotation-experimental-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/annotation/annotation-experimental-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
new file mode 100644
index 0000000..636ae52
--- /dev/null
+++ b/annotation/annotation-experimental-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
@@ -0,0 +1 @@
+androidx.annotation.lint.ExperimentalIssueRegistry
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/lint/ExperimentalDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/lint/ExperimentalDetectorTest.kt
new file mode 100644
index 0000000..2a00f5e
--- /dev/null
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/lint/ExperimentalDetectorTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2019 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.annotation.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles.java
+import com.android.tools.lint.checks.infrastructure.TestLintResult
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ExperimentalDetectorTest {
+
+ private fun checkJava(vararg testFiles: TestFile): TestLintResult {
+ return lint()
+ .files(
+ javaSample("androidx.annotation.Experimental"),
+ javaSample("androidx.annotation.UseExperimental"),
+ *testFiles
+ )
+ .allowMissingSdk(true)
+ .issues(*ExperimentalDetector.ISSUES.toTypedArray())
+ .run()
+ }
+
+ /**
+ * Loads a [TestFile] from Java source code included in the JAR resources.
+ */
+ private fun javaSample(className: String): TestFile {
+ return java(javaClass.getResource("/${className.replace('.','/')}.java").readText())
+ }
+
+ @Test
+ fun useExperimentalClassUnchecked() {
+ val input = arrayOf(
+ javaSample("sample.ExperimentalDateTime"),
+ javaSample("sample.UseExperimentalClassUnchecked")
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+src/sample/UseExperimentalClassUnchecked.java:29: Error: This declaration is experimental and its usage should be marked with
+'@sample.ExperimentalDateTime' or '@UseExperimental(sample.ExperimentalDateTime.class)' [UnsafeExperimentalUsageError]
+ DateProvider provider = new DateProvider();
+ ~~~~~~~~~~~~~~~~~~
+src/sample/UseExperimentalClassUnchecked.java:30: Error: This declaration is experimental and its usage should be marked with
+'@sample.ExperimentalDateTime' or '@UseExperimental(sample.ExperimentalDateTime.class)' [UnsafeExperimentalUsageError]
+ return provider.getDate();
+ ~~~~~~~
+2 errors, 0 warnings
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ checkJava(*input).expect(expected)
+ }
+
+ @Test
+ fun useExperimentalClassChecked() {
+ val input = arrayOf(
+ javaSample("sample.ExperimentalDateTime"),
+ javaSample("sample.UseExperimentalClassChecked")
+ )
+
+ val expected = "No warnings."
+
+ checkJava(*input).expect(expected)
+ }
+
+ @Test
+ fun useExperimentalMethodUnchecked() {
+ val input = arrayOf(
+ javaSample("sample.ExperimentalDateTime"),
+ javaSample("sample.UseExperimentalMethodUnchecked")
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+src/sample/UseExperimentalMethodUnchecked.java:35: Error: This declaration is experimental and its usage should be marked with
+'@sample.ExperimentalDateTime' or '@UseExperimental(sample.ExperimentalDateTime.class)' [UnsafeExperimentalUsageError]
+ System.out.println(getDate());
+ ~~~~~~~
+1 errors, 0 warnings
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ checkJava(*input).expect(expected)
+ }
+
+ @Test
+ fun useExperimentalClassCheckedUses() {
+ val input = arrayOf(
+ javaSample("sample.ExperimentalDateTime"),
+ javaSample("sample.UseExperimentalClassCheckedUses")
+ )
+
+ val expected = "No warnings."
+
+ checkJava(*input).expect(expected)
+ }
+}
diff --git a/annotation/annotation-experimental/build.gradle b/annotation/annotation-experimental/build.gradle
new file mode 100644
index 0000000..922edae
--- /dev/null
+++ b/annotation/annotation-experimental/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ lintPublish(project(":annotation:annotation-experimental-lint"))
+}
+
+androidx {
+ name = "Experimental annotation"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenVersion = LibraryVersions.ANNOTATION
+ mavenGroup = LibraryGroups.ANNOTATION
+ inceptionYear = "2019"
+ description = "Annotation for use on unstable API surfaces"
+}
diff --git a/annotation/annotation-experimental/src/main/AndroidManifest.xml b/annotation/annotation-experimental/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0294c29
--- /dev/null
+++ b/annotation/annotation-experimental/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+-->
+<manifest package="androidx.annotation.experimental" />
diff --git a/annotation/annotation-experimental/src/main/java/androidx/annotation/Experimental.java b/annotation/annotation-experimental/src/main/java/androidx/annotation/Experimental.java
new file mode 100644
index 0000000..7986afe3
--- /dev/null
+++ b/annotation/annotation-experimental/src/main/java/androidx/annotation/Experimental.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 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.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the annotated element is a marker of an experimental API.
+ *
+ * Any declaration annotated with this marker is considered part of an unstable API surface and its
+ * call sites should accept the experimental aspect of it either by using {@link UseExperimental},
+ * or by being annotated with that marker themselves, effectively causing further propagation of
+ * that experimental aspect.
+ *
+ * Example:
+ * <pre><code>
+ * // Library code
+ * @Retention(CLASS)
+ * @Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
+ * @Experimental(level = Level.ERROR)
+ * public @interface ExperimentalDateTime {}
+ *
+ * @ExperimentalDateTime
+ * public class DateProvider {
+ * // ...
+ * }
+ * </code></pre>
+ *
+ * <pre><code>
+ * // Client code
+ * int getYear() {
+ * DateProvider provider; // Error: DateProvider is experimental
+ * // ...
+ * }
+ *
+ * @ExperimentalDateTime
+ * Date getDate() {
+ * DateProvider provider; // OK: the function is marked as experimental
+ * // ...
+ * }
+ *
+ * void displayDate() {
+ * System.out.println(getDate()); // Error: getDate() is experimental, acceptance is required
+ * }
+ * </code></pre>
+ *
+ */
+@Retention(CLASS)
+@Target({ANNOTATION_TYPE})
+public @interface Experimental {
+ /**
+ * Severity of the diagnostic that should be reported on usages of experimental API which did
+ * not explicitly accept the experimental aspect of that API either by using
+ * {@link UseExperimental} or by being annotated with the corresponding marker annotation.
+ */
+ enum Level {
+ /**
+ * Specifies that a warning should be reported on incorrect usages of this experimental API.
+ */
+ WARNING,
+
+ /**
+ * Specifies that an error should be reported on incorrect usages of this experimental API.
+ */
+ ERROR,
+ }
+
+ /**
+ * Defines the reporting level for incorrect usages of this experimental API.
+ */
+ Level level() default Level.ERROR;
+}
diff --git a/annotation/annotation-experimental/src/main/java/androidx/annotation/UseExperimental.java b/annotation/annotation-experimental/src/main/java/androidx/annotation/UseExperimental.java
new file mode 100644
index 0000000..293e390
--- /dev/null
+++ b/annotation/annotation-experimental/src/main/java/androidx/annotation/UseExperimental.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 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.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Allows use of an experimental API denoted by the given markers in the annotated file,
+ * declaration, or expression. If a declaration is annotated with {@link UseExperimental}, its
+ * usages are <strong>not</strong> required to opt-in to that experimental API.
+ */
+@Retention(CLASS)
+@Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
+public @interface UseExperimental {
+ /**
+ * Defines the experimental API whose usage this annotation allows.
+ */
+ Class<?> markerClass();
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
index adc1007..99f9209 100644
--- a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
@@ -29,7 +29,7 @@
*/
val RELEASE_RULE = docsRules("public", false) {
prebuilts(LibraryGroups.ACTIVITY, "1.0.0-beta01")
- prebuilts(LibraryGroups.ANNOTATION, "1.1.0")
+ prebuilts(LibraryGroups.ANNOTATION, "annotation", "1.1.0")
prebuilts(LibraryGroups.APPCOMPAT, "1.1.0-beta01")
prebuilts(LibraryGroups.ARCH_CORE, "2.1.0-rc01")
prebuilts(LibraryGroups.ASYNCLAYOUTINFLATER, "1.0.0")
diff --git a/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/metainf/MetaInfTransformer.kt b/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/metainf/MetaInfTransformer.kt
index bba3289..82c658e 100644
--- a/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/metainf/MetaInfTransformer.kt
+++ b/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/metainf/MetaInfTransformer.kt
@@ -42,7 +42,8 @@
"androidx.car_car-moderator.version",
"androidx.activity_activity-ktx.version",
"androidx.lifecycle_lifecycle-runtime-ktx.version",
- "androidx.dynamicanimation_dynamicanimation-ktx.version"
+ "androidx.dynamicanimation_dynamicanimation-ktx.version",
+ "androidx.annotation_annotation-experimental.version"
)
}
@@ -83,4 +84,4 @@
val newPath = Paths.get(dirPath, newFileName)
file.updateRelativePath(newPath)
}
-}
\ No newline at end of file
+}
diff --git a/settings.gradle b/settings.gradle
index 0dbef5c..67c8116 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -41,6 +41,9 @@
includeProject(":ads-identifier", "ads/ads-identifier")
includeProject(":annotation", "annotations")
includeProject(":annotation:annotation-sampled", "annotation/annotation-sampled")
+includeProject(":annotation:annotation-experimental", "annotation/annotation-experimental")
+includeProject(":annotation:annotation-experimental-lint", "annotation/annotation-experimental-lint")
+includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
includeProject(":animation", "animation")
includeProject(":animation:testing", "animation/testing")
includeProject(":animation:integration-tests:testapp", "animation/integration-tests/testapp")