blob: ad1a44649c3dfa35612eaab1d79591906cd3d038 [file]
/*
* Copyright 2024 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.navigation.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.PsiClass
import com.intellij.psi.PsiClassOwner
import com.intellij.psi.PsiCompiledElement
import com.intellij.psi.PsiMethod
import com.intellij.psi.impl.source.PsiClassReferenceType
import org.jetbrains.kotlin.psi.KtClassLiteralExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getParameterForArgument
import org.jetbrains.uast.toUElement
/**
* Checks for missing annotations on type-safe route declarations
*
* Retrieves route classes/objects by tracing KClasses passed as route during NavDestination
* creation
*/
abstract class BaseTypeSafeDestinationMissingAnnotationDetector(
private val methodNames: List<String>,
private val constructorNames: List<String>,
) : Detector(), SourceCodeScanner {
// Issue instantiation requires specific Detector class which is the child class
abstract fun getMissingSerializableIssue(): Issue
abstract fun getMissingKeepIssue(): Issue
final override fun getApplicableMethodNames(): List<String> = methodNames
final override fun getApplicableConstructorTypes(): List<String> = constructorNames
final override fun visitMethodCall(
context: JavaContext,
node: UCallExpression,
method: PsiMethod,
) {
val packageName = (method.containingFile as? PsiClassOwner)?.packageName
if (
packageName != NAVIGATION_PACKAGE_NAME && packageName != NAVIGATION_COMPOSE_PACKAGE_NAME
)
return
val receiver = node.receiver?.getExpressionType()?.canonicalText
// Get the destination Type, but we only want to lint routes from safe args apis.
// Such routes come in two possible forms:
// 1. route as regular parameter specifically for NavigatorProvider.navigation
// 2. reified type parameter for all other destination builder apis
val kClazzType =
when {
// route as parameter
node.methodName == "navigation" &&
receiver == "androidx.navigation.NavigatorProvider" -> node.getRouteKClassType()
// route as reified Type
node.typeArguments.isNotEmpty() ->
(node.typeArguments.first() as? PsiClassReferenceType)?.resolve()
else -> null
} ?: return
checkMissingSerializableAnnotation(kClazzType, context)
// filter for Enums in Class fields
val enums = kClazzType.getEnumFields()
if (enums.isNotEmpty()) checkMissingKeepAnnotation(enums, context)
}
final override fun visitConstructor(
context: JavaContext,
node: UCallExpression,
constructor: PsiMethod,
) {
val kClazzType = node.getRouteKClassType() ?: return
checkMissingSerializableAnnotation(kClazzType, context)
// filter for Enums in Class fields
val enums = kClazzType.getEnumFields()
if (enums.isNotEmpty()) checkMissingKeepAnnotation(enums, context)
}
// resolves and returns the actual type of KClass<*>
private fun UCallExpression.getRouteKClassType(): PsiClass? {
val routeNode =
valueArguments.find {
getParameterForArgument(it)?.name == "route" &&
it.sourcePsi is KtClassLiteralExpression
} ?: return null
return routeNode.getKClassType()
}
// check that the Type is annotated with @Serializable
private fun checkMissingSerializableAnnotation(kClazz: PsiClass, context: JavaContext) {
if (
kClazz !is PsiCompiledElement &&
!kClazz.isInterface &&
!kClazz.hasAnnotation("kotlinx.serialization.Serializable")
) {
val uElement = kClazz.toUElement() ?: return
context.report(
getMissingSerializableIssue(),
uElement,
context.getNameLocation(uElement),
"""To use this class or object as a type-safe destination, annotate it with @Serializable""",
)
}
}
private fun PsiClass.getEnumFields(): List<PsiClass> {
return fields.mapNotNull {
val resolved = (it.type as? PsiClassReferenceType)?.resolve()
resolved?.takeIf { resolved.isEnum }
}
}
private fun checkMissingKeepAnnotation(fields: List<PsiClass>, context: JavaContext) {
fields.onEach {
if (!it.hasAnnotation("androidx.annotation.Keep")) {
val uElement = it.toUElement() ?: return
context.report(
getMissingKeepIssue(),
uElement,
context.getNameLocation(uElement),
"""
|To prevent this Enum's serializer from being obfuscated in minified builds, annotate it with @androidx.annotation.Keep
"""
.trimMargin(),
)
}
}
}
}
val NAVIGATION_PACKAGE_NAME = "androidx.navigation"
val NAVIGATION_COMPOSE_PACKAGE_NAME = "androidx.navigation.compose"
fun createMissingSerializableAnnotationIssue(
detectorClass: Class<out BaseTypeSafeDestinationMissingAnnotationDetector>
) =
Issue.create(
id = "MissingSerializableAnnotation",
briefDescription =
"Type-safe NavDestinations must be annotated with " +
"@kotlinx.serialization.Serializable.",
explanation =
"The destination needs to be annotated with @Serializable " +
"in order for Navigation library to convert the class or object declaration " +
"into a NavDestination.",
category = Category.CORRECTNESS,
severity = Severity.ERROR,
implementation = Implementation(detectorClass, Scope.JAVA_FILE_SCOPE),
)
fun createMissingKeepAnnotationIssue(
detectorClass: Class<out BaseTypeSafeDestinationMissingAnnotationDetector>
) =
Issue.create(
id = "MissingKeepAnnotation",
briefDescription =
"In minified builds, Enum classes used as type-safe " +
"Navigation arguments should be annotated with @androidx.annotation.Keep ",
explanation =
"Type-safe nav arguments such as Enum types can get " +
"incorrectly obfuscated in minified builds when not referenced directly",
category = Category.CORRECTNESS,
severity = Severity.WARNING,
implementation = Implementation(detectorClass, Scope.JAVA_FILE_SCOPE),
)