Lint check for usages of JetBrains nullability annotations in Java files.
Test: Unit tests of the annotations in Java and Kotlin code.
Bug: 221275214
Change-Id: I8f0df1c9878bebeece54f58697409b25788172ba
diff --git a/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsJava.java b/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsJava.java
new file mode 100644
index 0000000..4f32ad8
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsJava.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 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.sample;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Sample class for testing JetBrains nullability annotations.
+ */
+@SuppressWarnings("unused")
+public class NullabilityAnnotationsJava {
+ /**
+ * Sample method
+ * @param arg NotNull arg
+ */
+ private void method1(@NotNull String arg) {
+ }
+
+ /**
+ * Sample method
+ * @param arg Nullable arg
+ */
+ private void method2(@Nullable String arg) {
+ }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsKotlin.kt b/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsKotlin.kt
new file mode 100644
index 0000000..3f57835
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/NullabilityAnnotationsKotlin.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 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.sample
+
+import org.jetbrains.annotations.NotNull
+import org.jetbrains.annotations.Nullable
+
+@Suppress("unused", "UNUSED_PARAMETER")
+class NullabilityAnnotationsKotlin {
+ private fun method1(@NotNull arg: String) {}
+
+ private fun method2(@Nullable arg: String?) {}
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
new file mode 100644
index 0000000..6ed71ee
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 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.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+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.Incident
+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.isJava
+import org.jetbrains.uast.UAnnotation
+
+/**
+ * Checks for usages of JetBrains nullability annotations in Java code.
+ */
+class NullabilityAnnotationsDetector : Detector(), Detector.UastScanner {
+ override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return AnnotationChecker(context)
+ }
+
+ private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
+ override fun visitAnnotation(node: UAnnotation) {
+ if (isJava(node.sourcePsi)) {
+ checkForAnnotation(node, "NotNull", "NonNull")
+ checkForAnnotation(node, "Nullable", "Nullable")
+ }
+ }
+
+ /**
+ * Check if the node is org.jetbrains.annotations.$jetBrainsAnnotation, replace with
+ * androidx.annotation.$androidxAnnotation if so.
+ */
+ private fun checkForAnnotation(
+ node: UAnnotation,
+ jetBrainsAnnotation: String,
+ androidxAnnotation: String
+ ) {
+ val incorrectAnnotation = "org.jetbrains.annotations.$jetBrainsAnnotation"
+ val replacementAnnotation = "androidx.annotation.$androidxAnnotation"
+ val patternToReplace = "(?:org\\.jetbrains\\.annotations\\.)?$jetBrainsAnnotation"
+
+ if (node.qualifiedName == incorrectAnnotation) {
+ val lintFix = fix().name("Replace with `@$replacementAnnotation`")
+ .replace()
+ .pattern(patternToReplace)
+ .with(replacementAnnotation)
+ .shortenNames()
+ .autoFix(true, true)
+ .build()
+ val incident = Incident(context)
+ .issue(ISSUE)
+ .fix(lintFix)
+ .location(context.getNameLocation(node))
+ .message("Use `@$replacementAnnotation` instead of `@$incorrectAnnotation`")
+ .scope(node)
+ context.report(incident)
+ }
+ }
+ }
+
+ companion object {
+ val ISSUE = Issue.create(
+ "NullabilityAnnotationsDetector",
+ "Replace usages of JetBrains nullability annotations with androidx " +
+ "versions in Java code",
+ "The androidx nullability annotations should be used in androidx libraries " +
+ "instead of JetBrains annotations.",
+ Category.CORRECTNESS, 5, Severity.ERROR,
+ Implementation(NullabilityAnnotationsDetector::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/NullabilityAnnotationsDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/NullabilityAnnotationsDetectorTest.kt
new file mode 100644
index 0000000..c9c423c
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/NullabilityAnnotationsDetectorTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 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.lint
+
+import androidx.build.lint.Stubs.Companion.JetBrainsAnnotations
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class NullabilityAnnotationsDetectorTest : AbstractLintDetectorTest(
+ useDetector = NullabilityAnnotationsDetector(),
+ useIssues = listOf(NullabilityAnnotationsDetector.ISSUE),
+ ) {
+ @Test
+ fun `Detection of Jetbrains nullability usage in Java sources`() {
+ val input = arrayOf(
+ javaSample("androidx.NullabilityAnnotationsJava"),
+ JetBrainsAnnotations
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+src/androidx/sample/NullabilityAnnotationsJava.java:31: Error: Use @androidx.annotation.NonNull instead of @org.jetbrains.annotations.NotNull [NullabilityAnnotationsDetector]
+ private void method1(@NotNull String arg) {
+ ~~~~~~~~
+src/androidx/sample/NullabilityAnnotationsJava.java:38: Error: Use @androidx.annotation.Nullable instead of @org.jetbrains.annotations.Nullable [NullabilityAnnotationsDetector]
+ private void method2(@Nullable String arg) {
+ ~~~~~~~~~
+2 errors, 0 warnings
+ """.trimIndent()
+
+ val expectFixDiffs = """
+Autofix for src/androidx/sample/NullabilityAnnotationsJava.java line 31: Replace with `@androidx.annotation.NonNull`:
+@@ -31 +31
+- private void method1(@NotNull String arg) {
++ private void method1(@androidx.annotation.NonNull String arg) {
+Autofix for src/androidx/sample/NullabilityAnnotationsJava.java line 38: Replace with `@androidx.annotation.Nullable`:
+@@ -38 +38
+- private void method2(@Nullable String arg) {
++ private void method2(@androidx.annotation.Nullable String arg) {
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ check(*input)
+ .expect(expected)
+ .expectFixDiffs(expectFixDiffs)
+ }
+
+ @Test
+ fun `JetBrains annotations allowed in Kotlin sources`() {
+ val input = arrayOf(
+ ktSample("androidx.NullabilityAnnotationsKotlin"),
+ JetBrainsAnnotations
+ )
+ check(*input).expectClean()
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
index 4db14ac..06ae21f 100644
--- a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
@@ -245,6 +245,14 @@
"""
)
+ val JetBrainsAnnotations = TestFiles.kotlin(
+ """
+package org.jetbrains.annotations
+
+annotation class NotNull
+annotation class Nullable
+ """
+ )
/* ktlint-enable max-line-length */
}
}