aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-prod (mdb) <android-build-team-robot@google.com>2020-02-11 06:18:09 +0000
committerandroid-build-prod (mdb) <android-build-team-robot@google.com>2020-02-11 06:18:09 +0000
commit75b75ee76ce1f0e85b90fd5b329c1234eea69abe (patch)
tree6877d8e518d1064c02553f1c964d0b1ad32dc092
parentf8ef1988158a6de6316aac7d8c8dd7db00407b91 (diff)
parent7e016f08973140eda32b174c420934f7c77fc5fa (diff)
downloadsupport-75b75ee76ce1f0e85b90fd5b329c1234eea69abe.tar.gz
Merge cherrypicks of [1231549, 1231550, 1232111, 1232112, 1232113, 1232114, 1232115, 1232116, 1232117] into androidx-fragment-release
Change-Id: I16720f080170c7cc31568e7e9780ee1d55683145
-rw-r--r--buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt1
-rw-r--r--fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt7
-rw-r--r--fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentLiveDataObserverDetector.kt165
-rw-r--r--fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentTagDetector.kt10
-rw-r--r--fragment/fragment-lint/src/main/java/androidx/fragment/lint/LintUtils.kt50
-rw-r--r--fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt203
-rw-r--r--fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt265
-rw-r--r--fragment/fragment-lint/src/test/java/androidx/fragment/lint/BackPressedDispatcherCallbackDetectorTest.kt290
-rw-r--r--fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt61
-rw-r--r--fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentTagDetectorTest.kt2
-rw-r--r--fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt956
-rw-r--r--fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt29
-rw-r--r--fragment/fragment-testing-lint/OWNERS1
-rw-r--r--fragment/fragment-testing-lint/build.gradle72
-rw-r--r--fragment/fragment-testing-lint/src/main/AndroidManifest.xml17
-rw-r--r--fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/FragmentTestingIssueRegistry.kt26
-rw-r--r--fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.kt88
-rw-r--r--fragment/fragment-testing-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry1
-rw-r--r--fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/ApiLintVersionsTest.kt38
-rw-r--r--fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/GradleConfigurationDetectorTest.kt81
-rw-r--r--fragment/fragment-testing/build.gradle2
-rw-r--r--lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt6
-rw-r--r--settings.gradle1
23 files changed, 2160 insertions, 212 deletions
diff --git a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
index 39b1b1f86ee..508d9a89385 100644
--- a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
@@ -68,6 +68,7 @@ val RELEASE_RULE = docsRules("public", false) {
prebuilts(LibraryGroups.ENTERPRISE, "1.0.0-rc01")
prebuilts(LibraryGroups.EXIFINTERFACE, "1.1.0-rc01")
ignore(LibraryGroups.FRAGMENT.group, "fragment-lint")
+ ignore(LibraryGroups.FRAGMENT.group, "fragment-testing-lint")
ignore(LibraryGroups.FRAGMENT.group, "fragment-truth")
prebuilts(LibraryGroups.FRAGMENT, "1.2.0-beta02")
prebuilts(LibraryGroups.GRIDLAYOUT, "1.0.0")
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
index 61485b1719d..e5c8b7b2757 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
@@ -27,6 +27,9 @@ class FragmentIssueRegistry : IssueRegistry() {
override val api = 6
override val minApi = CURRENT_API
override val issues get() = listOf(
- FragmentLiveDataObserverDetector.ISSUE,
- FragmentTagDetector.ISSUE)
+ FragmentTagDetector.ISSUE,
+ UnsafeFragmentLifecycleObserverDetector.BACK_PRESSED_ISSUE,
+ UnsafeFragmentLifecycleObserverDetector.LIVEDATA_ISSUE,
+ UseRequireInsteadOfGet.ISSUE
+ )
}
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentLiveDataObserverDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentLiveDataObserverDetector.kt
deleted file mode 100644
index de26c0378e9..00000000000
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentLiveDataObserverDetector.kt
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * 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.fragment.lint
-
-import androidx.fragment.lint.FragmentLiveDataObserverDetector.Companion.ISSUE
-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.LintFix
-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.android.tools.lint.detector.api.isKotlin
-import com.intellij.psi.util.PsiTypesUtil
-import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.UClass
-import org.jetbrains.uast.getContainingUClass
-import org.jetbrains.uast.visitor.AbstractUastVisitor
-
-/**
- * Lint check for detecting calls to [LiveData.observe] with a [Fragment] instance as the
- * lifecycle owner inside the [Fragment]'s [Fragment.onCreateView], [Fragment.onViewCreated],
- * [Fragment.onActivityCreated], or [Fragment.onViewStateRestored].
- */
-class FragmentLiveDataObserverDetector : Detector(), SourceCodeScanner {
-
- companion object {
- val ISSUE = Issue.create(
- id = "FragmentLiveDataObserve",
- briefDescription = "Use getViewLifecycleOwner() as the LifecycleOwner instead of " +
- "a Fragment instance when observing a LiveData object.",
- explanation = """When observing a LiveData object from a fragment's onCreateView,
- | onViewCreated, onActivityCreated, or onViewStateRestored method
- | getViewLifecycleOwner() should be used as the LifecycleOwner rather than the
- | Fragment instance. The Fragment lifecycle can result in the Fragment being
- | active longer than its view. This can lead to unexpected behavior from
- | LiveData objects being observed longer than the Fragment's view is active.""",
- category = Category.CORRECTNESS,
- severity = Severity.ERROR,
- implementation = Implementation(
- FragmentLiveDataObserverDetector::class.java, Scope.JAVA_FILE_SCOPE
- ),
- androidSpecific = true
- )
- }
-
- private val lifecycleMethods = setOf("onCreateView", "onViewCreated", "onActivityCreated",
- "onViewStateRestored")
-
- override fun applicableSuperClasses(): List<String>? = listOf(FRAGMENT_CLASS)
-
- override fun visitClass(context: JavaContext, declaration: UClass) {
- declaration.methods.forEach {
- if (lifecycleMethods.contains(it.name)) {
- val visitor = RecursiveMethodVisitor(context, declaration.name, it.name)
- it.uastBody?.accept(visitor)
- }
- }
- }
-}
-
-/**
- * A UAST Visitor that recursively explores all method calls within a method to check for a call to
- * [LiveData.observe] with a [Fragment] instance as the lifecycle owner.
- *
- * @param context The context of the lint request.
- * @param originFragmentName The name of the Fragment class being checked.
- * @param lifecycleMethod The name of the originating Fragment lifecycle method.
- */
-private class RecursiveMethodVisitor(
- private val context: JavaContext,
- private val originFragmentName: String?,
- private val lifecycleMethod: String
-) : AbstractUastVisitor() {
- private val visitedMethods = mutableSetOf<UCallExpression>()
-
- override fun visitCallExpression(node: UCallExpression): Boolean {
- if (visitedMethods.contains(node)) {
- return super.visitCallExpression(node)
- }
- if (node.isLiveDataObserve(context)) {
- val lifecycleOwner = node.valueArguments[0]
- val lifecycleOwnerType = PsiTypesUtil.getPsiClass(lifecycleOwner.getExpressionType())
- if (lifecycleOwner.getExpressionType().extends(context, FRAGMENT_CLASS)) {
- if (lifecycleOwnerType == node.getContainingUClass()?.javaPsi) {
- val methodFix = if (isKotlin(context.psiFile)) {
- "viewLifecycleOwner"
- } else {
- "getViewLifecycleOwner()"
- }
- context.report(ISSUE, context.getLocation(lifecycleOwner),
- "Use $methodFix as the LifecycleOwner.",
- LintFix.create()
- .replace()
- .with(methodFix)
- .build())
- } else {
- context.report(ISSUE, context.getLocation(node),
- "Unsafe call to observe with Fragment instance from $originFragmentName" +
- ".$lifecycleMethod.")
- }
- }
- } else if (node.isInteresting(context)) {
- visitedMethods.add(node)
- val psiMethod = node.resolve() ?: return super.visitCallExpression(node)
- val uastNode = context.uastContext.getMethod(psiMethod)
- uastNode.uastBody?.accept(this)
- visitedMethods.remove(node)
- }
- return super.visitCallExpression(node)
- }
-}
-
-/**
- * Checks if the [UCallExpression] is a call that should be explored. If the call chain
- * will exit the current class without reference to the [Fragment] instance then the call chain
- * does not need to be explored further.
- *
- * @return Whether this [UCallExpression] is to a call within the Fragment class or has a
- * reference to the Fragment passed as a parameter.
- */
-internal fun UCallExpression.isInteresting(context: JavaContext): Boolean {
- if (PsiTypesUtil.getPsiClass(receiverType) == this.getContainingUClass()?.javaPsi) {
- return true
- }
- if (valueArgumentCount > 0) {
- valueArguments.forEach {
- if (it.getExpressionType().extends(context, FRAGMENT_CLASS)) {
- return true
- }
- }
- }
- return false
-}
-
-/**
- * Checks if the [UCallExpression] is a [LiveData.observe] call.
- */
-internal fun UCallExpression.isLiveDataObserve(context: JavaContext): Boolean {
- if (methodName != "observe" ||
- !receiverType.extends(context, "androidx.lifecycle.LiveData") ||
- valueArgumentCount != 2) {
- return false
- }
- val psiParameters = resolve()?.parameterList?.parameters ?: return false
- return psiParameters[0].type.extends(context, "androidx.lifecycle.LifecycleOwner") &&
- psiParameters[1].type.extends(context, "androidx.lifecycle.Observer")
-}
-
-private const val FRAGMENT_CLASS = "androidx.fragment.app.Fragment"
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentTagDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentTagDetector.kt
index 7730d755f11..2c31fb911fd 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentTagDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentTagDetector.kt
@@ -39,11 +39,11 @@ class FragmentTagDetector : ResourceXmlDetector() {
val ISSUE = Issue.create(
id = "FragmentTagUsage",
briefDescription = "Use FragmentContainerView instead of the <fragment> tag",
- explanation = """FragmentContainerView replaces the <fragment> tag as the preferred way
- | of adding fragments via XML. Unlike the <fragment> tag, FragmentContainerView uses
- | a normal `FragmentTransaction` under the hood to add the initial fragment,
- | allowing further FragmentTransaction operations on the FragmentContainerView
- | and providing a consistent timing for lifecycle events.""",
+ explanation = """FragmentContainerView replaces the <fragment> tag as the preferred \
+ way of adding fragments via XML. Unlike the <fragment> tag, FragmentContainerView \
+ uses a normal `FragmentTransaction` under the hood to add the initial fragment, \
+ allowing further FragmentTransaction operations on the FragmentContainerView \
+ and providing a consistent timing for lifecycle events.""",
category = Category.CORRECTNESS,
severity = Severity.WARNING,
implementation = Implementation(
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/LintUtils.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/LintUtils.kt
index 8b9cd079e76..6bd7c959876 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/LintUtils.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/LintUtils.kt
@@ -19,16 +19,62 @@ package androidx.fragment.lint
import com.android.tools.lint.detector.api.JavaContext
import com.intellij.psi.PsiType
import com.intellij.psi.util.PsiTypesUtil
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UQualifiedReferenceExpression
/**
- * Checks if the [PsiType] is a subclass of class with canonical name {@code superName}.
+ * Checks if the [PsiType] is a subclass of class with canonical name [superName].
*
* @param context The context of the lint request.
* @param superName The canonical name to check that the [PsiType] is a subclass of.
- * @param strict Whether {@code superName} is inclusive.
+ * @param strict Whether [superName] is inclusive.
*/
internal fun PsiType?.extends(
context: JavaContext,
superName: String,
strict: Boolean = false
): Boolean = context.evaluator.extendsClass(PsiTypesUtil.getPsiClass(this), superName, strict)
+
+/**
+ * Walks up the uastParent hierarchy from this element.
+ */
+internal fun UElement.walkUp(): Sequence<UElement> = generateSequence(uastParent) { it.uastParent }
+
+/**
+ * This is useful if you're in a nested call expression and want to find the nearest parent while ignoring this call.
+ *
+ * For example, if you have the following two cases of a `foo()` expression:
+ * - `checkNotNull(fragment.foo())`
+ * - `checkNotNull(foo())` // if foo() is a local function
+ *
+ * Calling this from `foo()` in both cases will drop you at the outer `checkNotNull()` expression.
+ */
+val UElement.nearestNonQualifiedReferenceParent: UElement?
+ get() = walkUp().first {
+ it !is UQualifiedReferenceExpression
+ }
+
+/**
+ * @see [fullyQualifiedNearestParentOrNull]
+ */
+internal fun UElement.fullyQualifiedNearestParent(includeSelf: Boolean = true): UElement {
+ return fullyQualifiedNearestParentOrNull(includeSelf)!!
+}
+
+/**
+ * Given an element, returns the nearest fully qualified parent.
+ *
+ * Examples where [this] is a `UCallExpression` representing `bar()`:
+ * - `Foo.bar()` -> `Foo.bar()`
+ * - `bar()` -> `bar()`
+ *
+ * @param includeSelf Whether or not to include [this] element in the checks.
+ */
+internal fun UElement.fullyQualifiedNearestParentOrNull(includeSelf: Boolean = true): UElement? {
+ val node = if (includeSelf) this else uastParent ?: return null
+ return if (node is UQualifiedReferenceExpression) {
+ node.uastParent
+ } else {
+ node
+ }
+} \ No newline at end of file
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
new file mode 100644
index 00000000000..33c4b7c741d
--- /dev/null
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.fragment.lint
+
+import androidx.fragment.lint.UnsafeFragmentLifecycleObserverDetector.Issues.BACK_PRESSED_ISSUE
+import androidx.fragment.lint.UnsafeFragmentLifecycleObserverDetector.Issues.LIVEDATA_ISSUE
+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.LintFix
+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.android.tools.lint.detector.api.isKotlin
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.util.PsiTypesUtil
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.getContainingUClass
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * Lint check for detecting calls to lifecycle aware components with a
+ * [androidx.fragment.app.Fragment] instance as the [androidx.lifecycle.LifecycleOwner] while
+ * inside the [androidx.fragment.app.Fragment]'s [androidx.fragment.app.Fragment.onCreateView],
+ * [androidx.fragment.app.Fragment.onViewCreated],
+ * [androidx.fragment.app.Fragment.onActivityCreated], or
+ * [androidx.fragment.app.Fragment.onViewStateRestored].
+ */
+class UnsafeFragmentLifecycleObserverDetector : Detector(), SourceCodeScanner {
+
+ companion object Issues {
+ val LIVEDATA_ISSUE = Issue.create(
+ id = "FragmentLiveDataObserve",
+ briefDescription = "Use getViewLifecycleOwner() as the LifecycleOwner instead of " +
+ "a Fragment instance when observing a LiveData object.",
+ explanation = """When observing a LiveData object from a fragment's onCreateView, \
+ onViewCreated, onActivityCreated, or onViewStateRestored method \
+ getViewLifecycleOwner() should be used as the LifecycleOwner rather than the \
+ Fragment instance. The Fragment lifecycle can result in the Fragment being \
+ active longer than its view. This can lead to unexpected behavior from \
+ LiveData objects being observed longer than the Fragment's view is active.""",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ UnsafeFragmentLifecycleObserverDetector::class.java, Scope.JAVA_FILE_SCOPE
+ ),
+ androidSpecific = true
+ )
+
+ val BACK_PRESSED_ISSUE = Issue.create(
+ id = "FragmentBackPressedCallback",
+ briefDescription = "Use getViewLifecycleOwner() as the LifecycleOwner instead of " +
+ "a Fragment instance.",
+ explanation = """The Fragment lifecycle can result in a Fragment being active \
+ longer than its view. This can lead to unexpected behavior from lifecycle aware \
+ objects remaining active longer than the Fragment's view. To solve this issue, \
+ getViewLifecycleOwner() should be used as a LifecycleOwner rather than the \
+ Fragment instance once it is safe to access the view lifecycle in a \
+ Fragment's onCreateView, onViewCreated, onActivityCreated, or \
+ onViewStateRestored methods.""",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ UnsafeFragmentLifecycleObserverDetector::class.java, Scope.JAVA_FILE_SCOPE
+ ),
+ androidSpecific = true
+ )
+ }
+
+ private val lifecycleMethods = setOf("onCreateView", "onViewCreated", "onActivityCreated",
+ "onViewStateRestored")
+
+ override fun applicableSuperClasses(): List<String>? = listOf(FRAGMENT_CLASS)
+
+ override fun visitClass(context: JavaContext, declaration: UClass) {
+ declaration.methods.forEach {
+ if (lifecycleMethods.contains(it.name)) {
+ val visitor = RecursiveMethodVisitor(context, declaration.name, it.name)
+ it.uastBody?.accept(visitor)
+ }
+ }
+ }
+}
+
+/**
+ * A UAST Visitor that recursively explores all method calls within a
+ * [androidx.fragment.app.Fragment] lifecycle method to check for an unsafe method call
+ * ([UNSAFE_METHODS]) with a [androidx.fragment.app.Fragment] instance as the lifecycle owner.
+ *
+ * @param context The context of the lint request.
+ * @param originFragmentName The name of the Fragment class being checked.
+ * @param lifecycleMethod The name of the originating Fragment lifecycle method.
+ */
+private class RecursiveMethodVisitor(
+ private val context: JavaContext,
+ private val originFragmentName: String?,
+ private val lifecycleMethod: String
+) : AbstractUastVisitor() {
+ private val visitedMethods = mutableSetOf<UCallExpression>()
+
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ if (visitedMethods.contains(node)) {
+ return super.visitCallExpression(node)
+ }
+ val psiMethod = node.resolve() ?: return super.visitCallExpression(node)
+ if (!checkCall(node, psiMethod) && node.isInteresting(context)) {
+ val uastNode = context.uastContext.getMethod(psiMethod)
+ visitedMethods.add(node)
+ uastNode.uastBody?.accept(this)
+ visitedMethods.remove(node)
+ }
+ return super.visitCallExpression(node)
+ }
+
+ /**
+ * Checks if the current method call is unsafe.
+ *
+ * Returns `true` and report the appropriate lint issue if an error is found, otherwise return
+ * `false`.
+ *
+ * @param call The [UCallExpression] to check.
+ * @param psiMethod The resolved [PsiMethod] of [call].
+ * @return `true` if a lint error was found and reported, `false` otherwise.
+ */
+ private fun checkCall(call: UCallExpression, psiMethod: PsiMethod): Boolean {
+ val method = Method(psiMethod.containingClass?.qualifiedName, psiMethod.name)
+ val issue = UNSAFE_METHODS[method] ?: return false
+ val argMap = context.evaluator.computeArgumentMapping(call, psiMethod)
+ argMap.forEach { (arg, param) ->
+ if (arg.getExpressionType().extends(context, FRAGMENT_CLASS) &&
+ param.type.extends(context, "androidx.lifecycle.LifecycleOwner")) {
+ val argType = PsiTypesUtil.getPsiClass(arg.getExpressionType())
+ if (argType == call.getContainingUClass()?.javaPsi) {
+ val methodFix = if (isKotlin(context.psiFile)) {
+ "viewLifecycleOwner"
+ } else {
+ "getViewLifecycleOwner()"
+ }
+ context.report(issue, context.getLocation(arg),
+ "Use $methodFix as the LifecycleOwner.",
+ LintFix.create()
+ .replace()
+ .with(methodFix)
+ .build())
+ } else {
+ context.report(issue, context.getLocation(call),
+ "Unsafe call to ${call.methodName} with Fragment instance as " +
+ "LifecycleOwner from $originFragmentName.$lifecycleMethod.")
+ }
+ return true
+ }
+ }
+ return false
+ }
+}
+
+/**
+ * Checks if the [UCallExpression] is a call that should be explored. If the call chain
+ * will exit the current class without reference to the [androidx.fragment.app.Fragment] instance
+ * then the call chain does not need to be explored further.
+ *
+ * @return Whether this [UCallExpression] is to a call within the Fragment class or has a
+ * reference to the Fragment passed as a parameter.
+ */
+internal fun UCallExpression.isInteresting(context: JavaContext): Boolean {
+ if (PsiTypesUtil.getPsiClass(receiverType) == this.getContainingUClass()?.javaPsi) {
+ return true
+ }
+ if (valueArgumentCount > 0) {
+ valueArguments.forEach {
+ if (it.getExpressionType().extends(context, FRAGMENT_CLASS)) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+internal data class Method(val cls: String?, val name: String)
+
+internal val UNSAFE_METHODS = mapOf(
+ Method("androidx.lifecycle.LiveData", "observe") to LIVEDATA_ISSUE,
+ Method("androidx.lifecycle.LiveDataKt", "observe") to LIVEDATA_ISSUE,
+ Method("androidx.activity.OnBackPressedDispatcher", "addCallback") to BACK_PRESSED_ISSUE
+)
+
+private const val FRAGMENT_CLASS = "androidx.fragment.app.Fragment"
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
new file mode 100644
index 00000000000..773e9da576f
--- /dev/null
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2020 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.fragment.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.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+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.android.tools.lint.detector.api.isKotlin
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UPostfixExpression
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.getContainingUClass
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
+import java.util.Locale
+
+/**
+ * Androidx added new "require____()" versions of common "get___()" APIs, such as
+ * getContext/getActivity/getArguments/etc. Rather than wrap these in something like
+ * requireNotNull() or null-checking with `!!` in Kotlin, using these APIs will allow the
+ * underlying component to try to tell you _why_ it was null, and thus yield a better error
+ * message.
+ */
+@Suppress("UnstableApiUsage")
+class UseRequireInsteadOfGet : Detector(), SourceCodeScanner {
+
+ companion object {
+ val ISSUE: Issue = Issue.create(
+ "UseRequireInsteadOfGet",
+ "Use the 'require_____()' API rather than 'get____()' API for more " +
+ "descriptive error messages when it's null.",
+ """
+ AndroidX added new "require____()" versions of common "get___()" APIs, such as \
+ getContext/getActivity/getArguments/etc. Rather than wrap these in something like \
+ requireNotNull(), using these APIs will allow the underlying component to try \
+ to tell you _why_ it was null, and thus yield a better error message.
+ """,
+ Category.CORRECTNESS,
+ 6,
+ Severity.ERROR,
+ Implementation(UseRequireInsteadOfGet::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+
+ private const val FRAGMENT_FQCN = "androidx.fragment.app.Fragment"
+ internal val REQUIRABLE_METHODS = setOf(
+ "getArguments",
+ "getContext",
+ "getActivity",
+ "getFragmentManager",
+ "getHost",
+ "getParentFragment",
+ "getView"
+ )
+ // Convert 'getArguments' to 'arguments'
+ internal val REQUIRABLE_REFERENCES = REQUIRABLE_METHODS.map {
+ it.removePrefix("get").decapitalize(Locale.US)
+ }
+ internal val KNOWN_NULLCHECKS = setOf(
+ "checkNotNull",
+ "requireNonNull"
+ )
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ super.visitMethodCall(context, node, method)
+ }
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>>? {
+ return listOf(UCallExpression::class.java, USimpleNameReferenceExpression::class.java)
+ }
+
+ override fun createUastHandler(context: JavaContext): UElementHandler? {
+ val isKotlin = isKotlin(context.psiFile)
+ return object : UElementHandler() {
+
+ /** This covers Kotlin accessor syntax expressions like "fragment.arguments" */
+ override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) {
+ val parent = node.uastParent
+ if (parent is UQualifiedReferenceExpression) {
+ checkReferenceExpression(parent, node.identifier) {
+ parent.receiver.getExpressionType()
+ ?.let { context.evaluator.findClass(it.canonicalText) }
+ }
+ } else {
+ // It's a member of the enclosing class
+ checkReferenceExpression(node, node.identifier) {
+ node.getContainingUClass()
+ }
+ }
+ }
+
+ private fun checkReferenceExpression(
+ node: UExpression,
+ identifier: String,
+ resolveEnclosingClass: () -> PsiClass?
+ ) {
+ if (identifier in REQUIRABLE_REFERENCES) {
+ val enclosingClass = resolveEnclosingClass() ?: return
+ if (context.evaluator.extendsClass(enclosingClass, FRAGMENT_FQCN, false)) {
+ checkForIssue(node, identifier)
+ }
+ }
+ }
+
+ /** This covers function/method calls like "fragment.getArguments()" */
+ override fun visitCallExpression(node: UCallExpression) {
+ val targetMethod = node.resolve() ?: return
+ val containingClass = targetMethod.containingClass ?: return
+ if (targetMethod.name in REQUIRABLE_METHODS &&
+ context.evaluator.extendsClass(containingClass, FRAGMENT_FQCN, false)) {
+ checkForIssue(node, targetMethod.name, "${targetMethod.name}()")
+ }
+ }
+
+ /** Called only when we know we're looking at a whitelisted method call type. */
+ private fun checkForIssue(
+ node: UExpression,
+ targetMethodName: String,
+ targetExpression: String = targetMethodName
+ ) {
+ // Note we go up potentially two parents - the first one may just be the qualified reference expression
+ val nearestNonQualifiedReferenceParent =
+ node.nearestNonQualifiedReferenceParent ?: return
+ if (isKotlin && nearestNonQualifiedReferenceParent.isNullCheckBlock()) {
+ // We're a double-bang expression (!!)
+ val parentSourceToReplace =
+ nearestNonQualifiedReferenceParent.asSourceString()
+ val correctMethod = correctMethod(
+ parentSourceToReplace,
+ "$targetExpression!!",
+ targetMethodName
+ )
+ report(nearestNonQualifiedReferenceParent, parentSourceToReplace, correctMethod)
+ } else if (nearestNonQualifiedReferenceParent is UCallExpression) {
+ // See if we're in a "requireNotNull(...)" or similar expression
+ val enclosingMethodCall =
+ nearestNonQualifiedReferenceParent.resolve() ?: return
+
+ if (enclosingMethodCall.name in KNOWN_NULLCHECKS) {
+ // Only match for single (specified) parameter. If existing code had a
+ // custom failure message, we don't want to overwrite it.
+ val singleParameterSpecified =
+ isSingleParameterSpecified(
+ enclosingMethodCall,
+ nearestNonQualifiedReferenceParent
+ )
+
+ if (singleParameterSpecified) {
+ // Grab the source of this argument as it's represented.
+ val source = nearestNonQualifiedReferenceParent.valueArguments[0]
+ .asSourceString()
+ val parentToReplace =
+ nearestNonQualifiedReferenceParent.fullyQualifiedNearestParent()
+ .asSourceString()
+ val correctMethod =
+ correctMethod(source, targetExpression, targetMethodName)
+ report(
+ nearestNonQualifiedReferenceParent,
+ parentToReplace,
+ correctMethod
+ )
+ }
+ }
+ }
+ }
+
+ private fun isSingleParameterSpecified(
+ enclosingMethodCall: PsiMethod,
+ nearestNonQualifiedRefParent: UCallExpression
+ ) = enclosingMethodCall.parameterList.parametersCount == 1 ||
+ (isKotlin &&
+ nearestNonQualifiedRefParent is KotlinUFunctionCallExpression &&
+ nearestNonQualifiedRefParent.getArgumentForParameter(1) == null)
+
+ private fun correctMethod(
+ source: String,
+ targetExpression: String,
+ targetMethodName: String
+ ): String {
+ return source.replace(
+ targetExpression,
+ "require${targetMethodName.removePrefix("get").capitalize(Locale.US)}()"
+ )
+ }
+
+ private fun report(node: UElement, targetExpression: String, correctMethod: String) {
+ context.report(
+ ISSUE,
+ context.getLocation(node),
+ "Use $correctMethod instead of $targetExpression",
+ LintFix.create()
+ .replace()
+ .name("Replace with $correctMethod")
+ .text(targetExpression)
+ .with(correctMethod)
+ .autoFix()
+ .build()
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Copy of the currently experimental Kotlin stdlib version. Can be removed once the stdlib version
+ * comes out of experimental.
+ */
+internal fun String.decapitalize(locale: Locale): String {
+ return if (isNotEmpty() && !this[0].isLowerCase()) {
+ substring(0, 1).toLowerCase(locale) + substring(1)
+ } else {
+ this
+ }
+}
+
+/**
+ * Copy of the currently experimental Kotlin stdlib version. Can be removed once the stdlib version
+ * comes out of experimental.
+ */
+internal fun String.capitalize(locale: Locale): String {
+ if (isNotEmpty()) {
+ val firstChar = this[0]
+ if (firstChar.isLowerCase()) {
+ return buildString {
+ val titleChar = firstChar.toTitleCase()
+ if (titleChar != firstChar.toUpperCase()) {
+ append(titleChar)
+ } else {
+ append(this@capitalize.substring(0, 1).toUpperCase(locale))
+ }
+ append(this@capitalize.substring(1))
+ }
+ }
+ }
+ return this
+}
+
+internal fun UElement.isNullCheckBlock(): Boolean {
+ return this is UPostfixExpression && operator.text == "!!"
+}
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/BackPressedDispatcherCallbackDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/BackPressedDispatcherCallbackDetectorTest.kt
new file mode 100644
index 00000000000..81676865d63
--- /dev/null
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/BackPressedDispatcherCallbackDetectorTest.kt
@@ -0,0 +1,290 @@
+/*
+ * 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.fragment.lint
+
+import androidx.fragment.lint.stubs.BACK_CALLBACK_STUBS
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintResult
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+import java.util.Properties
+
+@RunWith(JUnit4::class)
+class BackPressedDispatcherCallbackDetectorTest : LintDetectorTest() {
+
+ override fun getDetector(): Detector = UnsafeFragmentLifecycleObserverDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(UnsafeFragmentLifecycleObserverDetector.BACK_PRESSED_ISSUE)
+
+ private lateinit var sdkDir: File
+
+ @Before
+ fun setup() {
+ val stream = BackPressedDispatcherCallbackDetectorTest::class.java.classLoader
+ .getResourceAsStream("sdk.prop")
+ val properties = Properties()
+ properties.load(stream)
+ sdkDir = File(properties["sdk.dir"] as String)
+ }
+
+ private fun check(vararg files: TestFile): TestLintResult {
+ return lint().files(*files, *BACK_CALLBACK_STUBS)
+ .sdkHome(sdkDir)
+ .run()
+ }
+
+ @Test
+ fun pass() {
+ check(
+ kotlin("""
+package com.example
+
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+import com.example.test.Foo
+
+class TestFragment : Fragment {
+
+ override fun onCreateView() {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(getViewLifecycleOwner(), OnBackPressedCallback {})
+ }
+
+ override fun onViewCreated() {
+ test()
+ val foo = Foo()
+ foo.addCallback(this)
+ foo.callback(this)
+ }
+
+ private fun test() {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(getViewLifecycleOwner(), OnBackPressedCallback {})
+ test()
+ }
+}
+ """),
+ kotlin("""
+package com.example.test
+
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.LifecycleOwner
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+
+class Foo {
+ fun addCallback(fragment: Fragment) {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(LifecycleOwner(), OnBackPressedCallback {})
+ }
+
+ fun callback(fragment: Fragment) {}
+}
+ """))
+ .expectClean()
+ }
+
+ @Test
+ fun inMethodFails() {
+ check(
+ kotlin("""
+package com.example
+
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+
+class TestFragment : Fragment {
+
+ override fun onCreateView() {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(this, OnBackPressedCallback {})
+ }
+}
+ """))
+ .expect("""
+src/com/example/TestFragment.kt:12: Error: Use viewLifecycleOwner as the LifecycleOwner. [FragmentBackPressedCallback]
+ dispatcher.addCallback(this, OnBackPressedCallback {})
+ ~~~~
+1 errors, 0 warnings
+ """)
+ .checkFix(null, kotlin("""
+package com.example
+
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+
+class TestFragment : Fragment {
+
+ override fun onCreateView() {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(viewLifecycleOwner, OnBackPressedCallback {})
+ }
+}
+ """))
+ }
+
+ @Test
+ fun helperMethodFails() {
+ check(
+ kotlin("""
+package com.example
+
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+
+class TestFragment : Fragment {
+
+ override fun onCreateView() {
+ test()
+ }
+
+ private fun test() {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(this, OnBackPressedCallback {})
+ }
+}
+ """))
+ .expect("""
+src/com/example/TestFragment.kt:16: Error: Use viewLifecycleOwner as the LifecycleOwner. [FragmentBackPressedCallback]
+ dispatcher.addCallback(this, OnBackPressedCallback {})
+ ~~~~
+1 errors, 0 warnings
+ """)
+ .checkFix(null, kotlin("""
+package com.example
+
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+
+class TestFragment : Fragment {
+
+ override fun onCreateView() {
+ test()
+ }
+
+ private fun test() {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(viewLifecycleOwner, OnBackPressedCallback {})
+ }
+}
+ """))
+ }
+
+ @Test
+ fun externalCallFails() {
+ check(
+ kotlin("""
+package com.example
+
+import androidx.fragment.app.Fragment
+import com.example.test.Foo
+
+class TestFragment : Fragment {
+
+ override fun onCreateView() {
+ test()
+ }
+
+ private fun test() {
+ val foo = Foo()
+ foo.addCallback(this)
+ }
+}
+ """),
+ kotlin("""
+package com.example.test
+
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+
+class Foo {
+ fun addCallback(fragment: Fragment) {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(fragment, OnBackPressedCallback {})
+ }
+}
+ """))
+ .expect("""
+src/com/example/test/Foo.kt:11: Error: Unsafe call to addCallback with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentBackPressedCallback]
+ dispatcher.addCallback(fragment, OnBackPressedCallback {})
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """)
+ }
+
+ @Test
+ fun externalHelperMethodFails() {
+ check(
+ kotlin("""
+package com.example
+
+import androidx.fragment.app.Fragment
+import com.example.test.Foo
+
+class TestFragment : Fragment {
+
+ override fun onCreateView() {
+ test()
+ }
+
+ private fun test() {
+ val foo = Foo()
+ foo.addCallback(this)
+ }
+}
+ """),
+ kotlin("""
+package com.example.test
+
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedCallback
+
+class Foo {
+ private lateinit val fragment: Fragment
+
+ fun addCallback(fragment: Fragment) {
+ this.fragment = fragment
+ callback()
+ }
+
+ private fun callback() {
+ val dispatcher = OnBackPressedDispatcher()
+ dispatcher.addCallback(fragment, OnBackPressedCallback {})
+ }
+}
+ """))
+ .expect("""
+src/com/example/test/Foo.kt:18: Error: Unsafe call to addCallback with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentBackPressedCallback]
+ dispatcher.addCallback(fragment, OnBackPressedCallback {})
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """)
+ }
+}
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt
index 5bee5d202c5..791faeb9ad4 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt
@@ -15,8 +15,9 @@
*/
package androidx.fragment.lint
-import androidx.fragment.lint.stubs.STUBS
+import androidx.fragment.lint.stubs.LIVEDATA_STUBS
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintResult
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Before
@@ -28,24 +29,32 @@ import java.util.Properties
@RunWith(JUnit4::class)
class FragmentLiveDataObserveDetectorTest : LintDetectorTest() {
- override fun getDetector(): Detector = FragmentLiveDataObserverDetector()
+
+ override fun getDetector(): Detector = UnsafeFragmentLifecycleObserverDetector()
override fun getIssues(): MutableList<Issue> =
- mutableListOf(FragmentLiveDataObserverDetector.ISSUE)
+ mutableListOf(UnsafeFragmentLifecycleObserverDetector.LIVEDATA_ISSUE)
- private var sdkDir: File? = null
+ private lateinit var sdkDir: File
@Before
fun setup() {
- val stream = FragmentTagDetectorTest::class.java.classLoader.getResourceAsStream("sdk.prop")
+ val stream = FragmentLiveDataObserveDetectorTest::class.java.classLoader
+ .getResourceAsStream("sdk.prop")
val properties = Properties()
properties.load(stream)
sdkDir = File(properties["sdk.dir"] as String)
}
+ private fun check(vararg files: TestFile): TestLintResult {
+ return lint().files(*files, *LIVEDATA_STUBS)
+ .sdkHome(sdkDir)
+ .run()
+ }
+
@Test
fun pass() {
- lint().files(
+ check(
kotlin("""
package com.example
@@ -90,15 +99,13 @@ class Foo {
fun observe(fragment: Fragment) {}
}
- """), *STUBS)
- .sdkHome(sdkDir!!)
- .run()
+ """))
.expectClean()
}
@Test
fun javaLintFixTest() {
- lint().files(
+ check(
java("""
package com.example;
@@ -113,9 +120,7 @@ class TestFragment extends Fragment {
liveData.observe(this, new Observer<String>() {});
}
}
- """), *STUBS)
- .sdkHome(sdkDir!!)
- .run()
+ """))
.expect("""
src/com/example/TestFragment.java:12: Error: Use getViewLifecycleOwner() as the LifecycleOwner. [FragmentLiveDataObserve]
liveData.observe(this, new Observer<String>() {});
@@ -140,8 +145,8 @@ class TestFragment extends Fragment {
}
@Test
- fun observeInMethodFails() {
- lint().files(
+ fun inMethodFails() {
+ check(
kotlin("""
package com.example
@@ -155,9 +160,7 @@ class TestFragment : Fragment {
liveData.observe(this, Observer<String> {})
}
}
- """), *STUBS)
- .sdkHome(sdkDir!!)
- .run()
+ """))
.expect("""
src/com/example/TestFragment.kt:11: Error: Use viewLifecycleOwner as the LifecycleOwner. [FragmentLiveDataObserve]
liveData.observe(this, Observer<String> {})
@@ -182,7 +185,7 @@ class TestFragment : Fragment {
@Test
fun helperMethodFails() {
- lint().files(
+ check(
kotlin("""
package com.example
@@ -200,9 +203,7 @@ class TestFragment : Fragment {
liveData.observe(this, Observer<String> {})
}
}
- """), *STUBS)
- .sdkHome(sdkDir!!)
- .run()
+ """))
.expect("""
src/com/example/TestFragment.kt:15: Error: Use viewLifecycleOwner as the LifecycleOwner. [FragmentLiveDataObserve]
liveData.observe(this, Observer<String> {})
@@ -231,7 +232,7 @@ class TestFragment : Fragment {
@Test
fun externalCallFails() {
- lint().files(
+ check(
kotlin("""
package com.example
@@ -263,11 +264,9 @@ class Foo {
liveData.observe(fragment, Observer<String> {})
}
}
- """), *STUBS)
- .sdkHome(sdkDir!!)
- .run()
+ """))
.expect("""
-src/com/example/test/Foo.kt:10: Error: Unsafe call to observe with Fragment instance from TestFragment.onCreateView. [FragmentLiveDataObserve]
+src/com/example/test/Foo.kt:10: Error: Unsafe call to observe with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentLiveDataObserve]
liveData.observe(fragment, Observer<String> {})
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
@@ -276,7 +275,7 @@ src/com/example/test/Foo.kt:10: Error: Unsafe call to observe with Fragment inst
@Test
fun externalHelperMethodFails() {
- lint().files(
+ check(
kotlin("""
package com.example
@@ -315,11 +314,9 @@ class Foo {
liveData.observe(fragment, Observer<String> {})
}
}
- """), *STUBS)
- .sdkHome(sdkDir!!)
- .run()
+ """))
.expect("""
-src/com/example/test/Foo.kt:17: Error: Unsafe call to observe with Fragment instance from TestFragment.onCreateView. [FragmentLiveDataObserve]
+src/com/example/test/Foo.kt:17: Error: Unsafe call to observe with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentLiveDataObserve]
liveData.observe(fragment, Observer<String> {})
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentTagDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentTagDetectorTest.kt
index 86c769d0af4..7c8ae62432a 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentTagDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentTagDetectorTest.kt
@@ -33,7 +33,7 @@ class FragmentTagDetectorTest : LintDetectorTest() {
override fun getIssues(): MutableList<Issue> = mutableListOf(FragmentTagDetector.ISSUE)
- private var sdkDir: File? = null
+ private lateinit var sdkDir: File
@Before
fun setup() {
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt
new file mode 100644
index 00000000000..4ac1e5f5e83
--- /dev/null
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt
@@ -0,0 +1,956 @@
+/*
+ * Copyright 2020 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.fragment.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+class UseRequireInsteadOfGetTest {
+
+ private val fragmentStub = java(
+ """
+ package androidx.fragment.app;
+
+ public class Fragment {
+ public void getArguments() {
+
+ }
+ public void getContext() {
+
+ }
+ public void getActivity() {
+
+ }
+ public void getFragmentManager() {
+
+ }
+ public void getHost() {
+
+ }
+ public void getParentFragment() {
+
+ }
+ public void getView() {
+
+ }
+ }
+ """
+ ).indented()
+
+ private val preconditionsStub = java(
+ """
+ package util;
+
+ public final class Preconditions {
+ public static <T> T checkNotNull(T value) {
+
+ }
+
+ public static <T> T checkNotNull(T value, String message) {
+
+ }
+ }
+ """
+ ).indented()
+
+ private fun useRequireLint(): TestLintTask {
+ return lint()
+ .detector(UseRequireInsteadOfGet())
+ .issues(UseRequireInsteadOfGet.ISSUE)
+ }
+
+ @Test
+ fun `simple java checks where the fragment is a variable`() {
+ useRequireLint()
+ .files(
+ fragmentStub,
+ preconditionsStub,
+ java(
+ """
+ package foo;
+
+ import androidx.fragment.app.Fragment;
+ import static util.Preconditions.checkNotNull;
+
+ class Test {
+ void test() {
+ Fragment fragment = new Fragment();
+
+ checkNotNull(fragment.getArguments());
+ checkNotNull(fragment.getFragmentManager());
+ checkNotNull(fragment.getContext());
+ checkNotNull(fragment.getActivity());
+ checkNotNull(fragment.getHost());
+ checkNotNull(fragment.getParentFragment());
+ checkNotNull(fragment.getView());
+
+ // These are redundant. Java-only really
+ checkNotNull(fragment.requireArguments());
+ checkNotNull(fragment.requireFragmentManager());
+ checkNotNull(fragment.requireContext());
+ checkNotNull(fragment.requireActivity());
+ checkNotNull(fragment.requireHost());
+ checkNotNull(fragment.requireParentFragment());
+ checkNotNull(fragment.requireView());
+
+ // These don't have errors
+ fragment.requireArguments();
+ fragment.requireFragmentManager();
+ fragment.requireContext();
+ fragment.requireActivity();
+ fragment.requireHost();
+ fragment.requireParentFragment();
+ fragment.requireView();
+
+ // These are ignored because they have custom error messages
+ checkNotNull(fragment.getArguments(), "getArguments");
+ checkNotNull(fragment.getFragmentManager(), "getFragmentManager");
+ checkNotNull(fragment.getContext(), "getContext");
+ checkNotNull(fragment.getActivity(), "getActivity");
+ checkNotNull(fragment.getHost(), "getHost");
+ checkNotNull(fragment.getParentFragment(), "getParentFragment");
+ checkNotNull(fragment.getView(), "getView");
+ }
+ }
+ """
+ ).indented()
+ )
+ .allowCompilationErrors(false)
+ .run()
+ .expect(
+ """
+ src/foo/Test.java:10: Error: Use fragment.requireArguments() instead of checkNotNull(fragment.getArguments()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getArguments());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.java:11: Error: Use fragment.requireFragmentManager() instead of checkNotNull(fragment.getFragmentManager()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getFragmentManager());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.java:12: Error: Use fragment.requireContext() instead of checkNotNull(fragment.getContext()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getContext());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.java:13: Error: Use fragment.requireActivity() instead of checkNotNull(fragment.getActivity()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getActivity());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.java:14: Error: Use fragment.requireHost() instead of checkNotNull(fragment.getHost()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getHost());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.java:15: Error: Use fragment.requireParentFragment() instead of checkNotNull(fragment.getParentFragment()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getParentFragment());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.java:16: Error: Use fragment.requireView() instead of checkNotNull(fragment.getView()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getView());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 7 errors, 0 warnings
+ """.trimIndent()
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/foo/Test.java line 10: Replace with fragment.requireArguments():
+ @@ -10 +10
+ - checkNotNull(fragment.getArguments());
+ + fragment.requireArguments();
+ Fix for src/foo/Test.java line 11: Replace with fragment.requireFragmentManager():
+ @@ -11 +11
+ - checkNotNull(fragment.getFragmentManager());
+ + fragment.requireFragmentManager();
+ Fix for src/foo/Test.java line 12: Replace with fragment.requireContext():
+ @@ -12 +12
+ - checkNotNull(fragment.getContext());
+ + fragment.requireContext();
+ Fix for src/foo/Test.java line 13: Replace with fragment.requireActivity():
+ @@ -13 +13
+ - checkNotNull(fragment.getActivity());
+ + fragment.requireActivity();
+ Fix for src/foo/Test.java line 14: Replace with fragment.requireHost():
+ @@ -14 +14
+ - checkNotNull(fragment.getHost());
+ + fragment.requireHost();
+ Fix for src/foo/Test.java line 15: Replace with fragment.requireParentFragment():
+ @@ -15 +15
+ - checkNotNull(fragment.getParentFragment());
+ + fragment.requireParentFragment();
+ Fix for src/foo/Test.java line 16: Replace with fragment.requireView():
+ @@ -16 +16
+ - checkNotNull(fragment.getView());
+ + fragment.requireView();
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun `simple java checks where the code is in a fragment`() {
+ useRequireLint()
+ .files(
+ fragmentStub,
+ preconditionsStub,
+ java(
+ """
+ package foo;
+
+ import androidx.fragment.app.Fragment;
+ import static util.Preconditions.checkNotNull;
+
+ class TestFragment extends Fragment {
+ void test() {
+ checkNotNull(getArguments());
+ checkNotNull(getFragmentManager());
+ checkNotNull(getContext());
+ checkNotNull(getActivity());
+ checkNotNull(getHost());
+ checkNotNull(getParentFragment());
+ checkNotNull(getView());
+
+ // These are redundant. Java-only really
+ checkNotNull(requireArguments());
+ checkNotNull(requireFragmentManager());
+ checkNotNull(requireContext());
+ checkNotNull(requireActivity());
+ checkNotNull(requireHost());
+ checkNotNull(requireParentFragment());
+ checkNotNull(requireView());
+
+ // These don't have errors
+ requireArguments();
+ requireFragmentManager();
+ requireContext();
+ requireActivity();
+ requireHost();
+ requireParentFragment();
+ requireView();
+
+ // These are ignored because they have custom error messages
+ checkNotNull(fragment.getArguments(), "getArguments");
+ checkNotNull(fragment.getFragmentManager(), "getFragmentManager");
+ checkNotNull(fragment.getContext(), "getContext");
+ checkNotNull(fragment.getActivity(), "getActivity");
+ checkNotNull(fragment.getHost(), "getHost");
+ checkNotNull(fragment.getParentFragment(), "getParentFragment");
+ checkNotNull(fragment.getView(), "getView");
+ }
+ }
+ """
+ ).indented()
+ )
+ .allowCompilationErrors(false)
+ .run()
+ .expect(
+ """
+ src/foo/TestFragment.java:8: Error: Use requireArguments() instead of checkNotNull(getArguments()) [UseRequireInsteadOfGet]
+ checkNotNull(getArguments());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/TestFragment.java:9: Error: Use requireFragmentManager() instead of checkNotNull(getFragmentManager()) [UseRequireInsteadOfGet]
+ checkNotNull(getFragmentManager());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/TestFragment.java:10: Error: Use requireContext() instead of checkNotNull(getContext()) [UseRequireInsteadOfGet]
+ checkNotNull(getContext());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/TestFragment.java:11: Error: Use requireActivity() instead of checkNotNull(getActivity()) [UseRequireInsteadOfGet]
+ checkNotNull(getActivity());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/TestFragment.java:12: Error: Use requireHost() instead of checkNotNull(getHost()) [UseRequireInsteadOfGet]
+ checkNotNull(getHost());
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/TestFragment.java:13: Error: Use requireParentFragment() instead of checkNotNull(getParentFragment()) [UseRequireInsteadOfGet]
+ checkNotNull(getParentFragment());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/TestFragment.java:14: Error: Use requireView() instead of checkNotNull(getView()) [UseRequireInsteadOfGet]
+ checkNotNull(getView());
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ 7 errors, 0 warnings
+ """.trimIndent()
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/foo/TestFragment.java line 8: Replace with requireArguments():
+ @@ -8 +8
+ - checkNotNull(getArguments());
+ + requireArguments();
+ Fix for src/foo/TestFragment.java line 9: Replace with requireFragmentManager():
+ @@ -9 +9
+ - checkNotNull(getFragmentManager());
+ + requireFragmentManager();
+ Fix for src/foo/TestFragment.java line 10: Replace with requireContext():
+ @@ -10 +10
+ - checkNotNull(getContext());
+ + requireContext();
+ Fix for src/foo/TestFragment.java line 11: Replace with requireActivity():
+ @@ -11 +11
+ - checkNotNull(getActivity());
+ + requireActivity();
+ Fix for src/foo/TestFragment.java line 12: Replace with requireHost():
+ @@ -12 +12
+ - checkNotNull(getHost());
+ + requireHost();
+ Fix for src/foo/TestFragment.java line 13: Replace with requireParentFragment():
+ @@ -13 +13
+ - checkNotNull(getParentFragment());
+ + requireParentFragment();
+ Fix for src/foo/TestFragment.java line 14: Replace with requireView():
+ @@ -14 +14
+ - checkNotNull(getView());
+ + requireView();
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun `qualified checkNotNulls should remove the qualifier`() {
+ useRequireLint()
+ .files(
+ fragmentStub,
+ preconditionsStub,
+ java(
+ """
+ package foo;
+
+ import androidx.fragment.app.Fragment;
+ import util.Preconditions;
+
+ class TestFragment extends Fragment {
+ void test() {
+ Preconditions.checkNotNull(getArguments());
+ }
+ }
+ """
+ ).indented()
+ )
+ .allowCompilationErrors(false)
+ .run()
+ .expect(
+ """
+ src/foo/TestFragment.java:8: Error: Use requireArguments() instead of Preconditions.checkNotNull(getArguments()) [UseRequireInsteadOfGet]
+ Preconditions.checkNotNull(getArguments());
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/foo/TestFragment.java line 8: Replace with requireArguments():
+ @@ -8 +8
+ - Preconditions.checkNotNull(getArguments());
+ + requireArguments();
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun `simple kotlin checks where the fragment is a variable`() {
+ // Note we don't import a preconditions stub here because we use kotlin's built-in
+ useRequireLint()
+ .files(
+ fragmentStub,
+ kotlin(
+ """
+ package foo
+
+ import androidx.fragment.app.Fragment
+
+ class Test {
+ fun test() {
+ val fragment = Fragment()
+
+ checkNotNull(fragment.getArguments())
+ checkNotNull(fragment.getFragmentManager())
+ checkNotNull(fragment.getContext())
+ checkNotNull(fragment.getActivity())
+ checkNotNull(fragment.getHost())
+ checkNotNull(fragment.getParentFragment())
+ checkNotNull(fragment.getView())
+
+ checkNotNull(fragment.arguments)
+ checkNotNull(fragment.fragmentManager)
+ checkNotNull(fragment.context)
+ checkNotNull(fragment.activity)
+ checkNotNull(fragment.host)
+ checkNotNull(fragment.parentFragment)
+ checkNotNull(fragment.view)
+
+ // !! nullchecks
+ fragment.getArguments()!!
+ fragment.getFragmentManager()!!
+ fragment.getContext()!!
+ fragment.getActivity()!!
+ fragment.getHost()!!
+ fragment.getParentFragment()!!
+ fragment.getView()!!
+ fragment.arguments!!
+ fragment.fragmentManager!!
+ fragment.context!!
+ fragment.activity!!
+ fragment.host!!
+ fragment.parentFragment!!
+ fragment.view!!
+
+ // These don't have errors
+ fragment.requireArguments()
+ fragment.requireFragmentManager()
+ fragment.requireContext()
+ fragment.requireActivity()
+ fragment.requireHost()
+ fragment.requireParentFragment()
+ fragment.requireView()
+
+ // These are ignored because they have custom error messages
+ checkNotNull(fragment.getArguments()) { "getArguments" }
+ checkNotNull(fragment.getFragmentManager()) { "getFragmentManager" }
+ checkNotNull(fragment.getContext()) { "getContext" }
+ checkNotNull(fragment.getActivity()) { "getActivity" }
+ checkNotNull(fragment.getHost()) { "getHost" }
+ checkNotNull(fragment.getParentFragment()) { "getParentFragment" }
+ checkNotNull(fragment.getView()) { "getView" }
+ requireNonNull(fragment.getArguments()) { "getArguments" }
+ requireNonNull(fragment.getFragmentManager()) { "getFragmentManager" }
+ requireNonNull(fragment.getContext()) { "getContext" }
+ requireNonNull(fragment.getActivity()) { "getActivity" }
+ requireNonNull(fragment.getHost()) { "getHost" }
+ requireNonNull(fragment.getParentFragment()) { "getParentFragment" }
+ requireNonNull(fragment.getView()) { "getView" }
+ checkNotNull(fragment.arguments) { "getArguments" }
+ checkNotNull(fragment.fragmentManager) { "getFragmentManager" }
+ checkNotNull(fragment.context) { "getContext" }
+ checkNotNull(fragment.activity) { "getActivity" }
+ checkNotNull(fragment.host) { "getHost" }
+ checkNotNull(fragment.parentFragment) { "getParentFragment" }
+ checkNotNull(fragment.view) { "getView" }
+ requireNonNull(fragment.arguments) { "getArguments" }
+ requireNonNull(fragment.fragmentManager) { "getFragmentManager" }
+ requireNonNull(fragment.context) { "getContext" }
+ requireNonNull(fragment.activity) { "getActivity" }
+ requireNonNull(fragment.host) { "getHost" }
+ requireNonNull(fragment.parentFragment) { "getParentFragment" }
+ requireNonNull(fragment.view) { "getView" }
+ }
+ }
+ """
+ ).indented()
+ )
+ .allowCompilationErrors(false)
+ .run()
+ .expect(
+ """
+ src/foo/Test.kt:9: Error: Use fragment.requireArguments() instead of checkNotNull(fragment.getArguments()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getArguments())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:10: Error: Use fragment.requireFragmentManager() instead of checkNotNull(fragment.getFragmentManager()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getFragmentManager())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:11: Error: Use fragment.requireContext() instead of checkNotNull(fragment.getContext()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getContext())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:12: Error: Use fragment.requireActivity() instead of checkNotNull(fragment.getActivity()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getActivity())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:13: Error: Use fragment.requireHost() instead of checkNotNull(fragment.getHost()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getHost())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:14: Error: Use fragment.requireParentFragment() instead of checkNotNull(fragment.getParentFragment()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getParentFragment())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:15: Error: Use fragment.requireView() instead of checkNotNull(fragment.getView()) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.getView())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:17: Error: Use fragment.requireArguments() instead of checkNotNull(fragment.arguments) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.arguments)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:18: Error: Use fragment.requireFragmentManager() instead of checkNotNull(fragment.fragmentManager) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.fragmentManager)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:19: Error: Use fragment.requireContext() instead of checkNotNull(fragment.context) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.context)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:20: Error: Use fragment.requireActivity() instead of checkNotNull(fragment.activity) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.activity)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:21: Error: Use fragment.requireHost() instead of checkNotNull(fragment.host) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.host)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:22: Error: Use fragment.requireParentFragment() instead of checkNotNull(fragment.parentFragment) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.parentFragment)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:23: Error: Use fragment.requireView() instead of checkNotNull(fragment.view) [UseRequireInsteadOfGet]
+ checkNotNull(fragment.view)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:26: Error: Use fragment.requireArguments() instead of fragment.getArguments()!! [UseRequireInsteadOfGet]
+ fragment.getArguments()!!
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:27: Error: Use fragment.requireFragmentManager() instead of fragment.getFragmentManager()!! [UseRequireInsteadOfGet]
+ fragment.getFragmentManager()!!
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:28: Error: Use fragment.requireContext() instead of fragment.getContext()!! [UseRequireInsteadOfGet]
+ fragment.getContext()!!
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:29: Error: Use fragment.requireActivity() instead of fragment.getActivity()!! [UseRequireInsteadOfGet]
+ fragment.getActivity()!!
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:30: Error: Use fragment.requireHost() instead of fragment.getHost()!! [UseRequireInsteadOfGet]
+ fragment.getHost()!!
+ ~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:31: Error: Use fragment.requireParentFragment() instead of fragment.getParentFragment()!! [UseRequireInsteadOfGet]
+ fragment.getParentFragment()!!
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:32: Error: Use fragment.requireView() instead of fragment.getView()!! [UseRequireInsteadOfGet]
+ fragment.getView()!!
+ ~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:33: Error: Use fragment.requireArguments() instead of fragment.arguments!! [UseRequireInsteadOfGet]
+ fragment.arguments!!
+ ~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:34: Error: Use fragment.requireFragmentManager() instead of fragment.fragmentManager!! [UseRequireInsteadOfGet]
+ fragment.fragmentManager!!
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:35: Error: Use fragment.requireContext() instead of fragment.context!! [UseRequireInsteadOfGet]
+ fragment.context!!
+ ~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:36: Error: Use fragment.requireActivity() instead of fragment.activity!! [UseRequireInsteadOfGet]
+ fragment.activity!!
+ ~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:37: Error: Use fragment.requireHost() instead of fragment.host!! [UseRequireInsteadOfGet]
+ fragment.host!!
+ ~~~~~~~~~~~~~~~
+ src/foo/Test.kt:38: Error: Use fragment.requireParentFragment() instead of fragment.parentFragment!! [UseRequireInsteadOfGet]
+ fragment.parentFragment!!
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:39: Error: Use fragment.requireView() instead of fragment.view!! [UseRequireInsteadOfGet]
+ fragment.view!!
+ ~~~~~~~~~~~~~~~
+ 28 errors, 0 warnings
+ """.trimIndent()
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/foo/Test.kt line 9: Replace with fragment.requireArguments():
+ @@ -9 +9
+ - checkNotNull(fragment.getArguments())
+ + fragment.requireArguments()
+ Fix for src/foo/Test.kt line 10: Replace with fragment.requireFragmentManager():
+ @@ -10 +10
+ - checkNotNull(fragment.getFragmentManager())
+ + fragment.requireFragmentManager()
+ Fix for src/foo/Test.kt line 11: Replace with fragment.requireContext():
+ @@ -11 +11
+ - checkNotNull(fragment.getContext())
+ + fragment.requireContext()
+ Fix for src/foo/Test.kt line 12: Replace with fragment.requireActivity():
+ @@ -12 +12
+ - checkNotNull(fragment.getActivity())
+ + fragment.requireActivity()
+ Fix for src/foo/Test.kt line 13: Replace with fragment.requireHost():
+ @@ -13 +13
+ - checkNotNull(fragment.getHost())
+ + fragment.requireHost()
+ Fix for src/foo/Test.kt line 14: Replace with fragment.requireParentFragment():
+ @@ -14 +14
+ - checkNotNull(fragment.getParentFragment())
+ + fragment.requireParentFragment()
+ Fix for src/foo/Test.kt line 15: Replace with fragment.requireView():
+ @@ -15 +15
+ - checkNotNull(fragment.getView())
+ + fragment.requireView()
+ Fix for src/foo/Test.kt line 17: Replace with fragment.requireArguments():
+ @@ -17 +17
+ - checkNotNull(fragment.arguments)
+ + fragment.requireArguments()
+ Fix for src/foo/Test.kt line 18: Replace with fragment.requireFragmentManager():
+ @@ -18 +18
+ - checkNotNull(fragment.fragmentManager)
+ + fragment.requireFragmentManager()
+ Fix for src/foo/Test.kt line 19: Replace with fragment.requireContext():
+ @@ -19 +19
+ - checkNotNull(fragment.context)
+ + fragment.requireContext()
+ Fix for src/foo/Test.kt line 20: Replace with fragment.requireActivity():
+ @@ -20 +20
+ - checkNotNull(fragment.activity)
+ + fragment.requireActivity()
+ Fix for src/foo/Test.kt line 21: Replace with fragment.requireHost():
+ @@ -21 +21
+ - checkNotNull(fragment.host)
+ + fragment.requireHost()
+ Fix for src/foo/Test.kt line 22: Replace with fragment.requireParentFragment():
+ @@ -22 +22
+ - checkNotNull(fragment.parentFragment)
+ + fragment.requireParentFragment()
+ Fix for src/foo/Test.kt line 23: Replace with fragment.requireView():
+ @@ -23 +23
+ - checkNotNull(fragment.view)
+ + fragment.requireView()
+ Fix for src/foo/Test.kt line 26: Replace with fragment.requireArguments():
+ @@ -26 +26
+ - fragment.getArguments()!!
+ + fragment.requireArguments()
+ Fix for src/foo/Test.kt line 27: Replace with fragment.requireFragmentManager():
+ @@ -27 +27
+ - fragment.getFragmentManager()!!
+ + fragment.requireFragmentManager()
+ Fix for src/foo/Test.kt line 28: Replace with fragment.requireContext():
+ @@ -28 +28
+ - fragment.getContext()!!
+ + fragment.requireContext()
+ Fix for src/foo/Test.kt line 29: Replace with fragment.requireActivity():
+ @@ -29 +29
+ - fragment.getActivity()!!
+ + fragment.requireActivity()
+ Fix for src/foo/Test.kt line 30: Replace with fragment.requireHost():
+ @@ -30 +30
+ - fragment.getHost()!!
+ + fragment.requireHost()
+ Fix for src/foo/Test.kt line 31: Replace with fragment.requireParentFragment():
+ @@ -31 +31
+ - fragment.getParentFragment()!!
+ + fragment.requireParentFragment()
+ Fix for src/foo/Test.kt line 32: Replace with fragment.requireView():
+ @@ -32 +32
+ - fragment.getView()!!
+ + fragment.requireView()
+ Fix for src/foo/Test.kt line 33: Replace with fragment.requireArguments():
+ @@ -33 +33
+ - fragment.arguments!!
+ + fragment.requireArguments()
+ Fix for src/foo/Test.kt line 34: Replace with fragment.requireFragmentManager():
+ @@ -34 +34
+ - fragment.fragmentManager!!
+ + fragment.requireFragmentManager()
+ Fix for src/foo/Test.kt line 35: Replace with fragment.requireContext():
+ @@ -35 +35
+ - fragment.context!!
+ + fragment.requireContext()
+ Fix for src/foo/Test.kt line 36: Replace with fragment.requireActivity():
+ @@ -36 +36
+ - fragment.activity!!
+ + fragment.requireActivity()
+ Fix for src/foo/Test.kt line 37: Replace with fragment.requireHost():
+ @@ -37 +37
+ - fragment.host!!
+ + fragment.requireHost()
+ Fix for src/foo/Test.kt line 38: Replace with fragment.requireParentFragment():
+ @@ -38 +38
+ - fragment.parentFragment!!
+ + fragment.requireParentFragment()
+ Fix for src/foo/Test.kt line 39: Replace with fragment.requireView():
+ @@ -39 +39
+ - fragment.view!!
+ + fragment.requireView()
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun `simple kotlin checks where the code is in a fragment`() {
+ // Note we don't import a preconditions stub here because we use kotlin's built-in
+ useRequireLint()
+ .files(
+ fragmentStub,
+ kotlin(
+ """
+ package foo
+
+ import androidx.fragment.app.Fragment
+
+ class Test : Fragment() {
+ fun test() {
+ checkNotNull(getArguments())
+ checkNotNull(getFragmentManager())
+ checkNotNull(getContext())
+ checkNotNull(getActivity())
+ checkNotNull(getHost())
+ checkNotNull(getParentFragment())
+ checkNotNull(getView())
+
+ checkNotNull(arguments)
+ checkNotNull(fragmentManager)
+ checkNotNull(context)
+ checkNotNull(activity)
+ checkNotNull(host)
+ checkNotNull(parentFragment)
+ checkNotNull(view)
+
+ // !! nullchecks
+ getArguments()!!
+ getFragmentManager()!!
+ getContext()!!
+ getActivity()!!
+ getHost()!!
+ getParentFragment()!!
+ getView()!!
+ arguments!!
+ fragmentManager!!
+ context!!
+ activity!!
+ host!!
+ parentFragment!!
+ view!!
+
+ // These don't have errors
+ requireArguments()
+ requireFragmentManager()
+ requireContext()
+ requireActivity()
+ requireHost()
+ requireParentFragment()
+ requireView()
+
+ // These are ignored because they have custom error messages
+ checkNotNull(getArguments()) { "getArguments" }
+ checkNotNull(getFragmentManager()) { "getFragmentManager" }
+ checkNotNull(getContext()) { "getContext" }
+ checkNotNull(getActivity()) { "getActivity" }
+ checkNotNull(getHost()) { "getHost" }
+ checkNotNull(getParentFragment()) { "getParentFragment" }
+ checkNotNull(getView()) { "getView" }
+ requireNonNull(getArguments()) { "getArguments" }
+ requireNonNull(getFragmentManager()) { "getFragmentManager" }
+ requireNonNull(getContext()) { "getContext" }
+ requireNonNull(getActivity()) { "getActivity" }
+ requireNonNull(getHost()) { "getHost" }
+ requireNonNull(getParentFragment()) { "getParentFragment" }
+ requireNonNull(getView()) { "getView" }
+ checkNotNull(arguments) { "getArguments" }
+ checkNotNull(fragmentManager) { "getFragmentManager" }
+ checkNotNull(context) { "getContext" }
+ checkNotNull(activity) { "getActivity" }
+ checkNotNull(host) { "getHost" }
+ checkNotNull(parentFragment) { "getParentFragment" }
+ checkNotNull(view) { "getView" }
+ requireNonNull(arguments) { "getArguments" }
+ requireNonNull(fragmentManager) { "getFragmentManager" }
+ requireNonNull(context) { "getContext" }
+ requireNonNull(activity) { "getActivity" }
+ requireNonNull(host) { "getHost" }
+ requireNonNull(parentFragment) { "getParentFragment" }
+ requireNonNull(view) { "getView" }
+ }
+ }
+ """
+ ).indented()
+ )
+ .allowCompilationErrors(false)
+ .run()
+ .expect(
+ """
+ src/foo/Test.kt:7: Error: Use requireArguments() instead of checkNotNull(getArguments()) [UseRequireInsteadOfGet]
+ checkNotNull(getArguments())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:8: Error: Use requireFragmentManager() instead of checkNotNull(getFragmentManager()) [UseRequireInsteadOfGet]
+ checkNotNull(getFragmentManager())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:9: Error: Use requireContext() instead of checkNotNull(getContext()) [UseRequireInsteadOfGet]
+ checkNotNull(getContext())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:10: Error: Use requireActivity() instead of checkNotNull(getActivity()) [UseRequireInsteadOfGet]
+ checkNotNull(getActivity())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:11: Error: Use requireHost() instead of checkNotNull(getHost()) [UseRequireInsteadOfGet]
+ checkNotNull(getHost())
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:12: Error: Use requireParentFragment() instead of checkNotNull(getParentFragment()) [UseRequireInsteadOfGet]
+ checkNotNull(getParentFragment())
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:13: Error: Use requireView() instead of checkNotNull(getView()) [UseRequireInsteadOfGet]
+ checkNotNull(getView())
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:15: Error: Use requireArguments() instead of checkNotNull(arguments) [UseRequireInsteadOfGet]
+ checkNotNull(arguments)
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:16: Error: Use requireFragmentManager() instead of checkNotNull(fragmentManager) [UseRequireInsteadOfGet]
+ checkNotNull(fragmentManager)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:17: Error: Use requireContext() instead of checkNotNull(context) [UseRequireInsteadOfGet]
+ checkNotNull(context)
+ ~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:18: Error: Use requireActivity() instead of checkNotNull(activity) [UseRequireInsteadOfGet]
+ checkNotNull(activity)
+ ~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:19: Error: Use requireHost() instead of checkNotNull(host) [UseRequireInsteadOfGet]
+ checkNotNull(host)
+ ~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:20: Error: Use requireParentFragment() instead of checkNotNull(parentFragment) [UseRequireInsteadOfGet]
+ checkNotNull(parentFragment)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:21: Error: Use requireView() instead of checkNotNull(view) [UseRequireInsteadOfGet]
+ checkNotNull(view)
+ ~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:24: Error: Use requireArguments() instead of getArguments()!! [UseRequireInsteadOfGet]
+ getArguments()!!
+ ~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:25: Error: Use requireFragmentManager() instead of getFragmentManager()!! [UseRequireInsteadOfGet]
+ getFragmentManager()!!
+ ~~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:26: Error: Use requireContext() instead of getContext()!! [UseRequireInsteadOfGet]
+ getContext()!!
+ ~~~~~~~~~~~~~~
+ src/foo/Test.kt:27: Error: Use requireActivity() instead of getActivity()!! [UseRequireInsteadOfGet]
+ getActivity()!!
+ ~~~~~~~~~~~~~~~
+ src/foo/Test.kt:28: Error: Use requireHost() instead of getHost()!! [UseRequireInsteadOfGet]
+ getHost()!!
+ ~~~~~~~~~~~
+ src/foo/Test.kt:29: Error: Use requireParentFragment() instead of getParentFragment()!! [UseRequireInsteadOfGet]
+ getParentFragment()!!
+ ~~~~~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:30: Error: Use requireView() instead of getView()!! [UseRequireInsteadOfGet]
+ getView()!!
+ ~~~~~~~~~~~
+ src/foo/Test.kt:31: Error: Use requireArguments() instead of arguments!! [UseRequireInsteadOfGet]
+ arguments!!
+ ~~~~~~~~~~~
+ src/foo/Test.kt:32: Error: Use requireFragmentManager() instead of fragmentManager!! [UseRequireInsteadOfGet]
+ fragmentManager!!
+ ~~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:33: Error: Use requireContext() instead of context!! [UseRequireInsteadOfGet]
+ context!!
+ ~~~~~~~~~
+ src/foo/Test.kt:34: Error: Use requireActivity() instead of activity!! [UseRequireInsteadOfGet]
+ activity!!
+ ~~~~~~~~~~
+ src/foo/Test.kt:35: Error: Use requireHost() instead of host!! [UseRequireInsteadOfGet]
+ host!!
+ ~~~~~~
+ src/foo/Test.kt:36: Error: Use requireParentFragment() instead of parentFragment!! [UseRequireInsteadOfGet]
+ parentFragment!!
+ ~~~~~~~~~~~~~~~~
+ src/foo/Test.kt:37: Error: Use requireView() instead of view!! [UseRequireInsteadOfGet]
+ view!!
+ ~~~~~~
+ 28 errors, 0 warnings
+ """.trimIndent()
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/foo/Test.kt line 7: Replace with requireArguments():
+ @@ -7 +7
+ - checkNotNull(getArguments())
+ + requireArguments()
+ Fix for src/foo/Test.kt line 8: Replace with requireFragmentManager():
+ @@ -8 +8
+ - checkNotNull(getFragmentManager())
+ + requireFragmentManager()
+ Fix for src/foo/Test.kt line 9: Replace with requireContext():
+ @@ -9 +9
+ - checkNotNull(getContext())
+ + requireContext()
+ Fix for src/foo/Test.kt line 10: Replace with requireActivity():
+ @@ -10 +10
+ - checkNotNull(getActivity())
+ + requireActivity()
+ Fix for src/foo/Test.kt line 11: Replace with requireHost():
+ @@ -11 +11
+ - checkNotNull(getHost())
+ + requireHost()
+ Fix for src/foo/Test.kt line 12: Replace with requireParentFragment():
+ @@ -12 +12
+ - checkNotNull(getParentFragment())
+ + requireParentFragment()
+ Fix for src/foo/Test.kt line 13: Replace with requireView():
+ @@ -13 +13
+ - checkNotNull(getView())
+ + requireView()
+ Fix for src/foo/Test.kt line 15: Replace with requireArguments():
+ @@ -15 +15
+ - checkNotNull(arguments)
+ + requireArguments()
+ Fix for src/foo/Test.kt line 16: Replace with requireFragmentManager():
+ @@ -16 +16
+ - checkNotNull(fragmentManager)
+ + requireFragmentManager()
+ Fix for src/foo/Test.kt line 17: Replace with requireContext():
+ @@ -17 +17
+ - checkNotNull(context)
+ + requireContext()
+ Fix for src/foo/Test.kt line 18: Replace with requireActivity():
+ @@ -18 +18
+ - checkNotNull(activity)
+ + requireActivity()
+ Fix for src/foo/Test.kt line 19: Replace with requireHost():
+ @@ -19 +19
+ - checkNotNull(host)
+ + requireHost()
+ Fix for src/foo/Test.kt line 20: Replace with requireParentFragment():
+ @@ -20 +20
+ - checkNotNull(parentFragment)
+ + requireParentFragment()
+ Fix for src/foo/Test.kt line 21: Replace with requireView():
+ @@ -21 +21
+ - checkNotNull(view)
+ + requireView()
+ Fix for src/foo/Test.kt line 24: Replace with requireArguments():
+ @@ -24 +24
+ - getArguments()!!
+ + requireArguments()
+ Fix for src/foo/Test.kt line 25: Replace with requireFragmentManager():
+ @@ -25 +25
+ - getFragmentManager()!!
+ + requireFragmentManager()
+ Fix for src/foo/Test.kt line 26: Replace with requireContext():
+ @@ -26 +26
+ - getContext()!!
+ + requireContext()
+ Fix for src/foo/Test.kt line 27: Replace with requireActivity():
+ @@ -27 +27
+ - getActivity()!!
+ + requireActivity()
+ Fix for src/foo/Test.kt line 28: Replace with requireHost():
+ @@ -28 +28
+ - getHost()!!
+ + requireHost()
+ Fix for src/foo/Test.kt line 29: Replace with requireParentFragment():
+ @@ -29 +29
+ - getParentFragment()!!
+ + requireParentFragment()
+ Fix for src/foo/Test.kt line 30: Replace with requireView():
+ @@ -30 +30
+ - getView()!!
+ + requireView()
+ Fix for src/foo/Test.kt line 31: Replace with requireArguments():
+ @@ -31 +31
+ - arguments!!
+ + requireArguments()
+ Fix for src/foo/Test.kt line 32: Replace with requireFragmentManager():
+ @@ -32 +32
+ - fragmentManager!!
+ + requireFragmentManager()
+ Fix for src/foo/Test.kt line 33: Replace with requireContext():
+ @@ -33 +33
+ - context!!
+ + requireContext()
+ Fix for src/foo/Test.kt line 34: Replace with requireActivity():
+ @@ -34 +34
+ - activity!!
+ + requireActivity()
+ Fix for src/foo/Test.kt line 35: Replace with requireHost():
+ @@ -35 +35
+ - host!!
+ + requireHost()
+ Fix for src/foo/Test.kt line 36: Replace with requireParentFragment():
+ @@ -36 +36
+ - parentFragment!!
+ + requireParentFragment()
+ Fix for src/foo/Test.kt line 37: Replace with requireView():
+ @@ -37 +37
+ - view!!
+ + requireView()
+ """.trimIndent()
+ )
+ }
+}
+/* ktlint-enable max-line-length */
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt
index 221f03c317a..4a51de9857a 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt
@@ -18,6 +18,22 @@ package androidx.fragment.lint.stubs
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+private val BACK_PRESSED_CALLBACK = java("""
+ package androidx.activity;
+
+ public abstract class OnBackPressedCallback {}
+""")
+
+private val BACK_PRESSED_DISPATCHER = java("""
+ package androidx.activity;
+
+ import androidx.lifecycle.LifecycleOwner;
+
+ public final class OnBackPressedDispatcher {
+ public void addCallback(LifecycleOwner owner, OnBackPressedCallback callback) {}
+ }
+""")
+
private val FRAGMENT = java("""
package androidx.fragment.app;
@@ -58,10 +74,19 @@ private val OBSERVER = java("""
public interface Observer<T> {}
""")
-internal val STUBS = arrayOf(
+// stubs for testing calls to LiveData.observe calls
+internal val LIVEDATA_STUBS = arrayOf(
FRAGMENT,
+ LIFECYCLE_OWNER,
LIVEDATA,
MUTABLE_LIVEDATA,
- OBSERVER,
+ OBSERVER
+)
+
+// stubs for testing calls to OnBackPressedDispatcher.addCallback calls
+internal val BACK_CALLBACK_STUBS = arrayOf(
+ BACK_PRESSED_CALLBACK,
+ BACK_PRESSED_DISPATCHER,
+ FRAGMENT,
LIFECYCLE_OWNER
)
diff --git a/fragment/fragment-testing-lint/OWNERS b/fragment/fragment-testing-lint/OWNERS
new file mode 100644
index 00000000000..80569a3428e
--- /dev/null
+++ b/fragment/fragment-testing-lint/OWNERS
@@ -0,0 +1 @@
+fraschilla@google.com
diff --git a/fragment/fragment-testing-lint/build.gradle b/fragment/fragment-testing-lint/build.gradle
new file mode 100644
index 00000000000..fe4ebcc71c0
--- /dev/null
+++ b/fragment/fragment-testing-lint/build.gradle
@@ -0,0 +1,72 @@
+/*
+ * 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.AndroidXExtension
+import androidx.build.CompilationTarget
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SdkHelperKt
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+ext.generatedResources = "$buildDir/generated/sdkResourcesForTest"
+
+sourceSets {
+ test.resources.srcDirs += generatedResources
+}
+
+task generateSdkResource() {
+ outputs.dir(generatedResources)
+ doLast {
+ new File(generatedResources, "sdk.prop").withWriter('UTF-8') { writer ->
+ writer.write("sdk.dir=${SdkHelperKt.getSdkPath(project.rootDir)}")
+ }
+ }
+}
+
+tasks["compileTestJava"].dependsOn generateSdkResource
+
+dependencies {
+ // compileOnly because we use lintChecks and it doesn't allow other types of deps
+ // this ugly hack exists because of b/63873667
+ if (rootProject.hasProperty("android.injected.invoked.from.ide")) {
+ compileOnly LINT_API_LATEST
+ } else {
+ compileOnly LINT_API_MIN
+ }
+ compileOnly KOTLIN_STDLIB
+
+ testImplementation KOTLIN_STDLIB
+ testImplementation LINT_CORE
+ testImplementation LINT_TESTS
+}
+
+androidx {
+ name = "Android Fragment-Testing Lint Checks"
+ toolingProject = true
+ publish = Publish.NONE
+ mavenVersion = LibraryVersions.FRAGMENT
+ mavenGroup = LibraryGroups.FRAGMENT
+ inceptionYear = "2019"
+ description = "Lint Checks for the Fragment Testing module"
+ url = AndroidXExtension.ARCHITECTURE_URL
+ compilationTarget = CompilationTarget.HOST
+}
diff --git a/fragment/fragment-testing-lint/src/main/AndroidManifest.xml b/fragment/fragment-testing-lint/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..7aa73d97575
--- /dev/null
+++ b/fragment/fragment-testing-lint/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest package="androidx.fragment.testing.lint"/>
diff --git a/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/FragmentTestingIssueRegistry.kt b/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/FragmentTestingIssueRegistry.kt
new file mode 100644
index 00000000000..baa182b8528
--- /dev/null
+++ b/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/FragmentTestingIssueRegistry.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.fragment.testing.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.detector.api.CURRENT_API
+
+class FragmentTestingIssueRegistry : IssueRegistry() {
+ override val api = 6
+ override val minApi = CURRENT_API
+ override val issues get() = listOf(GradleConfigurationDetector.ISSUE)
+}
diff --git a/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.kt b/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.kt
new file mode 100644
index 00000000000..4ad3dec84da
--- /dev/null
+++ b/fragment/fragment-testing-lint/src/main/java/androidx/fragment/testing/lint/GradleConfigurationDetector.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.fragment.testing.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.GradleContext
+import com.android.tools.lint.detector.api.GradleScanner
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+
+/**
+ * Lint check for ensuring that the Fragment Testing library is included using the correct
+ * debugImplementation configuration.
+ */
+class GradleConfigurationDetector : Detector(), GradleScanner {
+ companion object {
+ val ISSUE = Issue.create(
+ id = "FragmentGradleConfiguration",
+ briefDescription = "Include the fragment-testing library using the " +
+ "debugImplementation configuration.",
+ explanation = """The fragment-testing library contains a FragmentScenario class that \
+ creates an Activity that must exist in the runtime APK. To include the \
+ fragment-testing library in the runtime APK it must be added using the \
+ debugImplementation configuration.""",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ GradleConfigurationDetector::class.java, Scope.GRADLE_SCOPE
+ ),
+ androidSpecific = true
+ ).addMoreInfo("https://d.android.com/training/basics/fragments/testing#configure")
+ }
+
+ override fun checkDslPropertyAssignment(
+ context: GradleContext,
+ property: String,
+ value: String,
+ parent: String,
+ parentParent: String?,
+ valueCookie: Any,
+ statementCookie: Any
+ ) {
+ // Remove enclosing quotes and check starting string to ensure only instances that
+ // result in the fragment-testing library being imported are checked.
+ // Non-string values cannot be resolved so invalid imports via functions, variables, etc.
+ // will not be detected.
+ val library = getStringLiteralValue(value)
+ if (library.startsWith("androidx.fragment:fragment-testing") &&
+ property != "debugImplementation") {
+ context.report(ISSUE, statementCookie, context.getLocation(statementCookie),
+ "Replace with debugImplementation.",
+ fix().replace()
+ .text(property)
+ .with("debugImplementation")
+ .build())
+ }
+ }
+
+ /**
+ * Extracts the string value from the DSL value by removing surrounding quotes.
+ *
+ * Returns an empty string if [value] is not a string literal.
+ */
+ private fun getStringLiteralValue(value: String): String {
+ if (value.length > 2 && (value.startsWith("'") && value.endsWith("'") ||
+ value.startsWith("\"") && value.endsWith("\""))) {
+ return value.substring(1, value.length - 1)
+ }
+ return ""
+ }
+}
diff --git a/fragment/fragment-testing-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/fragment/fragment-testing-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
new file mode 100644
index 00000000000..a850f4192f9
--- /dev/null
+++ b/fragment/fragment-testing-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
@@ -0,0 +1 @@
+androidx.fragment.testing.lint.FragmentTestingIssueRegistry
diff --git a/fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/ApiLintVersionsTest.kt b/fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/ApiLintVersionsTest.kt
new file mode 100644
index 00000000000..bfeb80369d3
--- /dev/null
+++ b/fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/ApiLintVersionsTest.kt
@@ -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.
+ */
+
+package androidx.fragment.lint
+
+import androidx.fragment.testing.lint.FragmentTestingIssueRegistry
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ApiLintVersionsTest {
+
+ @Test
+ fun versionsCheck() {
+ val registry = FragmentTestingIssueRegistry()
+ // we hardcode version registry.api to the version that is used to run tests
+ assertThat(registry.api).isEqualTo(CURRENT_API)
+ // Intentionally fails in IDE, because we use different API version in
+ // studio and command line
+ assertThat(registry.minApi).isEqualTo(3)
+ }
+}
diff --git a/fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/GradleConfigurationDetectorTest.kt b/fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/GradleConfigurationDetectorTest.kt
new file mode 100644
index 00000000000..cb6ab0ae6de
--- /dev/null
+++ b/fragment/fragment-testing-lint/src/test/java/androidx/fragment/testing/lint/GradleConfigurationDetectorTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.fragment.testing.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+import java.util.Properties
+
+@RunWith(JUnit4::class)
+class GradleConfigurationDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = GradleConfigurationDetector()
+
+ override fun getIssues(): MutableList<Issue> = mutableListOf(GradleConfigurationDetector.ISSUE)
+
+ private lateinit var sdkDir: File
+
+ @Before
+ fun setup() {
+ val stream = GradleConfigurationDetectorTest::class.java.classLoader
+ .getResourceAsStream("sdk.prop")
+ val properties = Properties()
+ properties.load(stream)
+ sdkDir = File(properties["sdk.dir"] as String)
+ }
+
+ @Test
+ fun expectPass() {
+ lint().files(
+ gradle("build.gradle", """
+ dependencies {
+ debugImplementation("androidx.fragment:fragment-testing:1.2.0-beta02")
+ }
+ """).indented())
+ .sdkHome(sdkDir)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun expectFail() {
+ lint().files(
+ gradle("build.gradle", """
+ dependencies {
+ androidTestImplementation("androidx.fragment:fragment-testing:1.2.0-beta02")
+ }
+ """).indented())
+ .sdkHome(sdkDir)
+ .run()
+ .expect("""
+ build.gradle:2: Error: Replace with debugImplementation. [FragmentGradleConfiguration]
+ androidTestImplementation("androidx.fragment:fragment-testing:1.2.0-beta02")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent())
+ .checkFix(null, gradle("""
+ dependencies {
+ debugImplementation("androidx.fragment:fragment-testing:1.2.0-beta02")
+ }
+ """).indented())
+ }
+}
diff --git a/fragment/fragment-testing/build.gradle b/fragment/fragment-testing/build.gradle
index 76c5a6f0570..b6c4d219641 100644
--- a/fragment/fragment-testing/build.gradle
+++ b/fragment/fragment-testing/build.gradle
@@ -46,6 +46,8 @@ dependencies {
androidTestImplementation(TRUTH)
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+
+ lintPublish(project(':fragment:fragment-testing-lint'))
}
androidx {
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt
index 5dbf21b824d..b2594848afa 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt
@@ -33,7 +33,7 @@ import java.util.Properties
@RunWith(JUnit4::class)
class LifecycleWhenChecksTest {
- private var sdkDir: File? = null
+ private lateinit var sdkDir: File
@Before
fun setup() {
@@ -46,7 +46,7 @@ class LifecycleWhenChecksTest {
private fun check(body: String): TestLintResult {
return TestLintTask.lint()
.files(VIEW_STUB, LIFECYCLE_STUB, COROUTINES_STUB, kt(template(body)))
- .sdkHome(sdkDir!!)
+ .sdkHome(sdkDir)
.issues(ISSUE)
.run()
}
@@ -465,4 +465,4 @@ class LifecycleWhenChecksTest {
)
)
}
-} \ No newline at end of file
+}
diff --git a/settings.gradle b/settings.gradle
index 1c8572b2cee..a3630866c43 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -119,6 +119,7 @@ includeProject(":fragment:fragment", "fragment/fragment")
includeProject(":fragment:fragment-ktx", "fragment/fragment-ktx")
includeProject(":fragment:fragment-lint", "fragment/fragment-lint")
includeProject(":fragment:fragment-testing", "fragment/fragment-testing")
+includeProject(":fragment:fragment-testing-lint", "fragment/fragment-testing-lint")
includeProject(":fragment:fragment-truth", "fragment/fragment-truth")
includeProject(":fakeannotations", "fakeannotations")
includeProject(":gridlayout", "gridlayout")