[go: nahoru, domu]

blob: 624f26a87a6c00f1fb616eaf05c7bdcec8ed70aa [file] [log] [blame]
/*
* 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.compose.compiler.plugins.kotlin
import androidx.compose.compiler.plugins.kotlin.analysis.ComposeWritableSlices
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.container.StorageComponentContainer
import org.jetbrains.kotlin.container.useInstance
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.PropertyGetterDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.jvm.isJvm
import org.jetbrains.kotlin.psi.KtAnnotatedExpression
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtFunctionLiteral
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.psi.KtPsiUtil
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getValueArgumentForExpression
import org.jetbrains.kotlin.resolve.calls.checkers.AdditionalTypeChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext
import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.VariableAsFunctionResolvedCall
import org.jetbrains.kotlin.resolve.inline.InlineUtil.canBeInlineArgument
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInline
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlineParameter
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlinedArgument
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.lowerIfFlexible
import org.jetbrains.kotlin.types.typeUtil.builtIns
import org.jetbrains.kotlin.types.upperIfFlexible
import org.jetbrains.kotlin.util.OperatorNameConventions
open class ComposableCallChecker : CallChecker, AdditionalTypeChecker,
StorageComponentContainerContributor {
override fun registerModuleComponents(
container: StorageComponentContainer,
platform: TargetPlatform,
moduleDescriptor: ModuleDescriptor
) {
if (!platform.isJvm()) return
container.useInstance(this)
}
override fun check(
resolvedCall: ResolvedCall<*>,
reportOn: PsiElement,
context: CallCheckerContext
) {
if (!resolvedCall.isComposableInvocation()) return
val bindingContext = context.trace.bindingContext
var node: PsiElement? = reportOn
loop@while (node != null) {
when (node) {
is KtFunctionLiteral -> {
// keep going, as this is a "KtFunction", but we actually want the
// KtLambdaExpression
}
is KtLambdaExpression -> {
val descriptor = bindingContext[BindingContext.FUNCTION, node.functionLiteral]
if (descriptor == null) {
illegalCall(context, reportOn)
return
}
val composable = descriptor.isComposableCallable(bindingContext)
if (composable) return
val arg = getArgumentDescriptor(node.functionLiteral, bindingContext)
if (arg?.type?.composablePreventCaptureContract() == true) {
context.trace.record(
ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE,
descriptor,
false
)
context.trace.report(
ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION.on(
reportOn,
arg,
arg.containingDeclaration
)
)
return
}
// TODO(lmr): in future, we should check for CALLS_IN_PLACE contract
val inlined = arg != null &&
canBeInlineArgument(node.functionLiteral) &&
isInline(arg.containingDeclaration) &&
isInlineParameter(arg)
if (!inlined) {
illegalCall(context, reportOn)
return
} else {
// since the function is inlined, we continue going up the PSI tree
// until we find a composable context. We also mark this lambda
context.trace.record(
ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE,
descriptor,
true
)
}
}
is KtFunction -> {
val descriptor = bindingContext[BindingContext.FUNCTION, node]
if (descriptor == null) {
illegalCall(context, reportOn)
return
}
val composable = descriptor.isComposableCallable(bindingContext)
if (!composable) {
illegalCall(context, reportOn, node.nameIdentifier ?: node)
}
return
}
is KtProperty -> {
// NOTE: since we're explicitly going down a different branch for
// KtPropertyAccessor, the ONLY time we make it into this branch is when the
// call was done in the initializer of the property/variable.
val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, node]
if (
descriptor !is LocalVariableDescriptor &&
node.annotationEntries.hasComposableAnnotation(bindingContext)
) {
// composables shouldn't have initializers in the first place
illegalCall(context, reportOn)
return
}
}
is KtPropertyAccessor -> {
val property = node.property
if (!property.annotationEntries.hasComposableAnnotation(bindingContext)) {
illegalCall(context, reportOn, property.nameIdentifier ?: property)
}
return
}
is KtFile -> {
// if we've made it this far, the call was made in a non-composable context.
illegalCall(context, reportOn)
return
}
is KtClass -> {
// composable calls are never allowed in the initializers of a class
illegalCall(context, reportOn)
return
}
}
node = node.parent as? KtElement
}
}
private fun illegalCall(
context: CallCheckerContext,
callEl: PsiElement,
functionEl: PsiElement? = null
) {
context.trace.report(ComposeErrors.COMPOSABLE_INVOCATION.on(callEl))
if (functionEl != null) {
context.trace.report(ComposeErrors.COMPOSABLE_EXPECTED.on(functionEl))
}
}
override fun checkType(
expression: KtExpression,
expressionType: KotlinType,
expressionTypeWithSmartCast: KotlinType,
c: ResolutionContext<*>
) {
val bindingContext = c.trace.bindingContext
val expectedType = c.expectedType
if (expectedType === TypeUtils.NO_EXPECTED_TYPE) return
if (expectedType === TypeUtils.UNIT_EXPECTED_TYPE) return
val expectedComposable = expectedType.hasComposableAnnotation()
if (expression is KtLambdaExpression) {
val descriptor = bindingContext[BindingContext.FUNCTION, expression.functionLiteral]
?: return
val isComposable = descriptor.isComposableCallable(bindingContext)
if (expectedComposable != isComposable) {
val isInlineable = isInlinedArgument(
expression.functionLiteral,
c.trace.bindingContext,
true
)
if (isInlineable) return
val reportOn =
if (expression.parent is KtAnnotatedExpression)
expression.parent as KtExpression
else expression
c.trace.report(
Errors.TYPE_MISMATCH.on(
reportOn,
expectedType,
expressionTypeWithSmartCast
)
)
}
return
} else {
val nullableAnyType = expectedType.builtIns.nullableAnyType
val anyType = expectedType.builtIns.anyType
if (anyType == expectedType.lowerIfFlexible() &&
nullableAnyType == expectedType.upperIfFlexible()) return
val nullableNothingType = expectedType.builtIns.nullableNothingType
// Handle assigning null to a nullable composable type
if (expectedType.isMarkedNullable &&
expressionTypeWithSmartCast == nullableNothingType) return
val isComposable = expressionType.hasComposableAnnotation()
if (expectedComposable != isComposable) {
val reportOn =
if (expression.parent is KtAnnotatedExpression)
expression.parent as KtExpression
else expression
c.trace.report(
Errors.TYPE_MISMATCH.on(
reportOn,
expectedType,
expressionTypeWithSmartCast
)
)
}
return
}
}
}
fun ResolvedCall<*>.isComposableInvocation(): Boolean {
if (this is VariableAsFunctionResolvedCall) {
if (variableCall.candidateDescriptor.type.hasComposableAnnotation())
return true
if (functionCall.resultingDescriptor.hasComposableAnnotation()) return true
return false
}
val candidateDescriptor = candidateDescriptor
if (candidateDescriptor is FunctionDescriptor) {
if (candidateDescriptor.isOperator &&
candidateDescriptor.name == OperatorNameConventions.INVOKE) {
if (dispatchReceiver?.type?.hasComposableAnnotation() == true) {
return true
}
}
}
return when (candidateDescriptor) {
is ValueParameterDescriptor -> false
is LocalVariableDescriptor -> false
is PropertyDescriptor -> candidateDescriptor.hasComposableAnnotation()
is PropertyGetterDescriptor ->
candidateDescriptor.correspondingProperty.hasComposableAnnotation()
else -> candidateDescriptor.hasComposableAnnotation()
}
}
internal fun CallableDescriptor.isMarkedAsComposable(): Boolean {
return when (this) {
is PropertyGetterDescriptor -> correspondingProperty.hasComposableAnnotation()
is ValueParameterDescriptor -> type.hasComposableAnnotation()
is LocalVariableDescriptor -> type.hasComposableAnnotation()
is PropertyDescriptor -> hasComposableAnnotation()
else -> hasComposableAnnotation()
}
}
// if you called this, it would need to be a composable call (composer, changed, etc.)
fun CallableDescriptor.isComposableCallable(bindingContext: BindingContext): Boolean {
// if it's marked as composable then we're done
if (isMarkedAsComposable()) return true
if (
this is FunctionDescriptor &&
bindingContext[ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR, this] == true
) {
// even though it's not marked, it is inferred as so by the type system (by being passed
// into a parameter marked as composable or a variable typed as one. This isn't much
// different than being marked explicitly.
return true
}
val functionLiteral = findPsi() as? KtFunctionLiteral
// if it isn't a function literal then we are out of things to try.
?: return false
if (functionLiteral.annotationEntries.hasComposableAnnotation(bindingContext)) {
// in this case the function literal itself is being annotated as composable but the
// annotation isn't in the descriptor itself
return true
}
val lambdaExpr = functionLiteral.parent as? KtLambdaExpression
if (
lambdaExpr != null &&
bindingContext[ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL, lambdaExpr] == true
) {
// this lambda was marked as inferred to be composable
return true
}
// TODO(lmr): i'm not sure that this is actually needed at this point, since this should have
// been covered by the TypeResolutionInterceptorExtension
val arg = getArgumentDescriptor(functionLiteral, bindingContext) ?: return false
return arg.type.hasComposableAnnotation()
}
// the body of this function can have composable calls in it, even if it itself is not
// composable (it might capture a composer from the parent)
fun FunctionDescriptor.allowsComposableCalls(bindingContext: BindingContext): Boolean {
// if it's callable as a composable, then the answer is yes.
if (isComposableCallable(bindingContext)) return true
// otherwise, this is only true if it is a lambda which can be capable of composer
// capture
return bindingContext[
ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE,
this
] == true
}
private fun getArgumentDescriptor(
argument: KtFunction,
bindingContext: BindingContext
): ValueParameterDescriptor? {
val call = KtPsiUtil.getParentCallIfPresent(argument) ?: return null
val resolvedCall = call.getResolvedCall(bindingContext) ?: return null
val valueArgument = resolvedCall.call.getValueArgumentForExpression(argument) ?: return null
val mapping = resolvedCall.getArgumentMapping(valueArgument) as? ArgumentMatch ?: return null
return mapping.valueParameter
}
fun List<KtAnnotationEntry>.hasComposableAnnotation(bindingContext: BindingContext): Boolean {
for (entry in this) {
val descriptor = bindingContext.get(BindingContext.ANNOTATION, entry) ?: continue
if (descriptor.isComposableAnnotation) return true
}
return false
}