diff options
author | android-build-prod (mdb) <android-build-team-robot@google.com> | 2020-02-11 06:18:09 +0000 |
---|---|---|
committer | android-build-prod (mdb) <android-build-team-robot@google.com> | 2020-02-11 06:18:09 +0000 |
commit | 75b75ee76ce1f0e85b90fd5b329c1234eea69abe (patch) | |
tree | 6877d8e518d1064c02553f1c964d0b1ad32dc092 | |
parent | f8ef1988158a6de6316aac7d8c8dd7db00407b91 (diff) | |
parent | 7e016f08973140eda32b174c420934f7c77fc5fa (diff) | |
download | support-75b75ee76ce1f0e85b90fd5b329c1234eea69abe.tar.gz |
Merge cherrypicks of [1231549, 1231550, 1232111, 1232112, 1232113, 1232114, 1232115, 1232116, 1232117] into androidx-fragment-release
Change-Id: I16720f080170c7cc31568e7e9780ee1d55683145
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") |