[go: nahoru, domu]

blob: 6bf839144a3d17ba61189ca2e1c1d2b9b79e3f61 [file] [log] [blame]
/*
* 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.ui.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.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.impl.source.PsiClassReferenceType
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.kotlin.KotlinUBlockExpression
import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
import org.jetbrains.uast.kotlin.KotlinUImplicitReturnExpression
/**
* Lint [Detector] to ensure that we are not creating extra lambdas just to emit already captured
* lambdas inside Compose code. For example:
* ```
* val lambda = @Composable {}
* Foo {
* lambda()
* }
* ```
*
* Can just be inlined to:
* ```
* Foo(lambda)
* ```
*
* This helps avoid object allocation but more importantly helps us avoid extra code generation
* around composable lambdas.
*/
class UnnecessaryLambdaCreationDetector : Detector(), SourceCodeScanner {
override fun createUastHandler(context: JavaContext) = UnnecessaryLambdaCreationHandler(context)
override fun getApplicableUastTypes() = listOf(ULambdaExpression::class.java)
/**
* This handler visits every lambda expression and reports an issue if the following criteria
* (in order) hold true:
*
* 1. There is only one expression inside the lambda.
* 2. The expression is a function call
* 3. The lambda is being invoked as part of a function call, and not as a property assignment
* such as val foo = @Composable {}
* 4. The receiver type of the function call is `Function0` (i.e, we are invoking something
* that matches `() -> Unit` - this both avoids non-lambda invocations but also makes sure
* that we don't warn for lambdas that have parameters, such as @Composable() (Int) -> Unit
* - this cannot be inlined.)
* 5. The outer function call that contains this lambda is not a call to a `ComponentNode`
* (because these are technically constructor invocations that we just intercept calls to
* there is no way to avoid using a trailing lambda for this)
* 6. The lambda is not being passed as a parameter, for example `Foo { lambda -> lambda() }`
*/
class UnnecessaryLambdaCreationHandler(private val context: JavaContext) : UElementHandler() {
override fun visitLambdaExpression(node: ULambdaExpression) {
val expressions = (node.body as? KotlinUBlockExpression)?.expressions ?: return
if (expressions.size != 1) return
val expression = when (val expr = expressions.first()) {
is KotlinUFunctionCallExpression -> expr
is KotlinUImplicitReturnExpression ->
expr.returnExpression as? KotlinUFunctionCallExpression
else -> null
} ?: return
// We want to make sure this lambda is being invoked in the context of a function call,
// and not as a property assignment.
val parentExpression = node.uastParent!!.sourcePsi as? KtCallExpression ?: return
// If the expression has no receiver, it is not a lambda invocation
val receiverType = expression.receiverType as? PsiClassReferenceType ?: return
// Ignore function types with multiple parameters such as Function1, Function2 etc.
if (receiverType.reference.referenceName != function0SimpleName) return
if (parentExpression.isComponentNodeInvocation()) return
val lambdaName = expression.methodIdentifier!!.name
if (node.valueParameters.any { it.name == lambdaName }) return
context.report(
ISSUE,
node,
context.getNameLocation(expression),
"Creating an unnecessary lambda to emit a captured lambda"
)
}
}
companion object {
private fun KtCallExpression.isComponentNodeInvocation() =
referenceExpression()!!.text.endsWith("Node")
private val function0SimpleName = Function0::class.simpleName!!
private const val explanation =
"Creating this extra lambda instead of just passing the already captured lambda means" +
" that during code generation the Compose compiler will insert code around " +
"this lambda to track invalidations. This adds some extra runtime cost so you" +
" should instead just directly pass the lambda as a parameter to the function."
val ISSUE = Issue.create(
"UnnecessaryLambdaCreation",
"Creating an unnecessary lambda to emit a captured lambda",
explanation,
Category.PERFORMANCE, 5, Severity.ERROR,
Implementation(
UnnecessaryLambdaCreationDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
}
}