blob: b852d0574f103b37bd679c4c3f199196a20a39b8 [file] [log] [blame]
/*
* Copyright 2021 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.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiType
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
/**
* Lint check for detecting calls to the suspend `repeatOnLifecycle` APIs using `lifecycleOwner`
* instead of `viewLifecycleOwner` in [androidx.fragment.app.Fragment] classes but not in
* [androidx.fragment.app.DialogFragment] classes.
*
* DialogFragments are allowed to use `lifecycleOwner` since they don't always have a `view`
* attached to them.
*/
class UnsafeRepeatOnLifecycleDetector : Detector(), SourceCodeScanner {
companion object {
val ISSUE = Issue.create(
id = "UnsafeRepeatOnLifecycleDetector",
briefDescription = "RepeatOnLifecycle should be used with viewLifecycleOwner in " +
"Fragments.",
explanation = """The repeatOnLifecycle APIs should be used with the viewLifecycleOwner \
in Fragments as opposed to lifecycleOwner.""",
category = Category.CORRECTNESS,
severity = Severity.ERROR,
implementation = Implementation(
UnsafeRepeatOnLifecycleDetector::class.java, Scope.JAVA_FILE_SCOPE
),
androidSpecific = true
)
}
override fun getApplicableMethodNames() = listOf("repeatOnLifecycle")
private val lifecycleMethods = setOf(
"onCreateView", "onViewCreated", "onActivityCreated",
"onViewStateRestored"
)
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
// Check that repeatOnLifecycle is called in a Fragment
if (!hasFragmentAsAncestorType(node.getParentOfType())) return
// Check that repeatOnLifecycle is called in the proper Lifecycle function
if (!isCalledInViewLifecycleFunction(node.getParentOfType())) return
// Look at the entire launch scope
var launchScope = node.getParentOfType<KotlinUFunctionCallExpression>()?.receiver
while (
launchScope != null && !containsViewLifecycleOwnerCall(launchScope.sourcePsi?.text)
) {
launchScope = launchScope.getParentOfType()
}
// Report issue if there is no viewLifecycleOwner in the launch scope
if (!containsViewLifecycleOwnerCall(launchScope?.sourcePsi?.text)) {
context.report(
ISSUE,
context.getLocation(node),
"The repeatOnLifecycle API should be used with viewLifecycleOwner"
)
}
}
private fun containsViewLifecycleOwnerCall(sourceText: String?): Boolean {
if (sourceText == null) return false
return sourceText.contains(VIEW_LIFECYCLE_KOTLIN_PROP, ignoreCase = true) ||
sourceText.contains(VIEW_LIFECYCLE_FUN, ignoreCase = true)
}
private fun isCalledInViewLifecycleFunction(uMethod: UMethod?): Boolean {
if (uMethod == null) return false
return lifecycleMethods.contains(uMethod.name)
}
/**
* Check if `uClass` has FRAGMENT as a super type but not DIALOG_FRAGMENT
*/
@Suppress("UNCHECKED_CAST")
private fun hasFragmentAsAncestorType(uClass: UClass?): Boolean {
if (uClass == null) return false
return hasFragmentAsSuperType(uClass.superTypes as Array<PsiType>)
}
private fun hasFragmentAsSuperType(superTypes: Array<PsiType>): Boolean {
for (superType in superTypes) {
if (superType.canonicalText == DIALOG_FRAGMENT_CLASS) return false
if (superType.canonicalText == FRAGMENT_CLASS) return true
if (hasFragmentAsSuperType(superType.superTypes)) return true
}
return false
}
}
private const val VIEW_LIFECYCLE_KOTLIN_PROP = "viewLifecycleOwner"
private const val VIEW_LIFECYCLE_FUN = "getViewLifecycleOwner"
private const val FRAGMENT_CLASS = "androidx.fragment.app.Fragment"
private const val DIALOG_FRAGMENT_CLASS = "androidx.fragment.app.DialogFragment"