blob: edaf9577686c175b1b81e1c004459f536269adc9 [file] [log] [blame]
/*
* Copyright (C) 2014 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 com.android.tools.lint.checks
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.PsiCompiledElement
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiVariable
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UBinaryExpressionWithType
import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.UInstanceExpression
import org.jetbrains.uast.ULiteralExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UParenthesizedExpression
import org.jetbrains.uast.UPolyadicExpression
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UReturnExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.UUnaryExpression
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.kotlin.KotlinUTypeCheckExpression
import org.jetbrains.uast.toUElement
/** Looks for assertion usages. */
class AssertDetector : Detector(), SourceCodeScanner {
companion object Issues {
/** In Kotlin arguments to assertions are always evaluated. */
@JvmField
val EXPENSIVE = Issue.create(
id = "ExpensiveAssertion",
briefDescription = "Expensive Assertions",
explanation = """
In Kotlin, assertions are not handled the same way as from the Java programming \
language. In particular, they're just implemented as a library call, and inside \
the library call the error is only thrown if assertions are enabled.
This means that the arguments to the `assert` call will **always** \
be evaluated. If you're doing any computation in the expression being \
asserted, that computation will unconditionally be performed whether or not \
assertions are turned on. This typically turns into wasted work in release \
builds.
This check looks for cases where the assertion condition is nontrivial, e.g. \
it is performing method calls or doing more work than simple comparisons \
on local variables or fields.
You can work around this by writing your own inline assert method instead:
```
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
inline fun assert(condition: () -> Boolean) {
if (_Assertions.ENABLED && !condition()) {
throw AssertionError()
}
}
```
In Android, because assertions are not enforced at runtime, instead use this:
```kotlin
inline fun assert(condition: () -> Boolean) {
if (BuildConfig.DEBUG && !condition()) {
throw AssertionError()
}
}
```
""",
category = Category.PERFORMANCE,
priority = 6,
severity = Severity.WARNING,
implementation = Implementation(
AssertDetector::class.java,
Scope.JAVA_FILE_SCOPE
),
enabledByDefault = false
)
}
override fun getApplicableMethodNames(): List<String> =
// Kotlin assertions -- regular method call
listOf("assert")
override fun visitMethodCall(
context: JavaContext,
node: UCallExpression,
method: PsiMethod
) {
// Make sure it's Kotlin, and that the assert() call being called is the stdlib one
if (!isKotlin(node.sourcePsi)) {
return
}
val containingClass = method.containingClass?.qualifiedName ?: return
if (containingClass != "kotlin.PreconditionsKt" &&
containingClass != "kotlin.PreconditionsKt__AssertionsJVMKt"
) {
return
}
checkKotlinAssertion(context, node)
}
private fun checkKotlinAssertion(
context: JavaContext,
assertion: UCallExpression
) {
val condition = assertion.valueArguments.firstOrNull() ?: return
if (context.isEnabled(EXPENSIVE) && warnAboutWork(assertion, condition)) {
val location = context.getLocation(condition)
var message =
"Kotlin assertion arguments are always evaluated, even when assertions are off"
val fix: LintFix?
val cls = assertion.getParentOfType(UClass::class.java, true)
if (cls?.sourcePsi != null) { // sourcePsi == null: top level functions
fix = createKotlinAssertionStatusFix(context, assertion)
message += ". Consider surrounding assertion with `if (javaClass.desiredAssertionStatus()) { assert(...) }`"
} else {
fix = null
}
context.report(EXPENSIVE, assertion, location, message, fix)
}
}
private fun createKotlinAssertionStatusFix(
context: JavaContext,
assertCall: UCallExpression
): LintFix {
return fix().name("Surround with desiredAssertionStatus() check")
.replace()
.range(context.getLocation(assertCall))
.pattern("(.*)")
.with("if (javaClass.desiredAssertionStatus()) { \\k<1> }")
.reformat(true)
.build()
}
/**
* Returns true if the given assert call is performing computation
* in its condition without explicitly checking for whether
* assertions are enabled.
*/
private fun warnAboutWork(assertCall: UCallExpression, condition: UExpression): Boolean {
return isExpensive(condition, 0) && !isWithinAssertionStatusCheck(assertCall)
}
/**
* Returns true if the given logging call performs "work" to compute
* the message.
*/
private fun isExpensive(argument: UExpression, depth: Int): Boolean {
if (depth == 4) {
return true
}
if (argument is ULiteralExpression || argument is UInstanceExpression) {
return false
}
if (argument is UBinaryExpressionWithType) {
return if (argument is KotlinUTypeCheckExpression) {
false
} else {
isExpensive(argument.operand, depth + 1)
}
}
if (argument is UPolyadicExpression) {
for (value in argument.operands) {
if (isExpensive((value), depth + 1)) {
return true
}
}
return false
} else if (argument is UParenthesizedExpression) {
return isExpensive(argument.expression, depth + 1)
} else if (argument is UBinaryExpression) {
return isExpensive(
argument.leftOperand,
depth + 1
) || isExpensive(argument.rightOperand, depth + 1)
} else if (argument is UUnaryExpression) {
return isExpensive(argument.operand, depth + 1)
} else if (argument is USimpleNameReferenceExpression) {
// Just a simple local variable/field reference
return false
} else if (argument is UQualifiedReferenceExpression) {
if (argument.selector is UCallExpression) {
return isExpensive(argument.selector, depth + 1)
}
val value = argument.evaluate()
if (value != null) { // constant inlined by compiler
return false
}
val resolved = argument.resolve()
if (resolved is PsiVariable) {
// Just a reference to a property/field, parameter or variable
return false
}
} else if (argument is UCallExpression) {
val method = argument.resolve()
if (method != null && method !is PsiCompiledElement) {
val body = method.toUElement(UMethod::class.java)?.uastBody
?: return true
if (body is UBlockExpression) {
val expressions = body.expressions
if (expressions.size == 1 && expressions[0] is UReturnExpression) {
val retExp = (expressions[0] as UReturnExpression)
.returnExpression
return retExp == null || isExpensive(retExp, depth + 1)
}
} else {
// Expression body
return isExpensive(body, depth + 1)
}
}
}
// Method invocations etc
return true
}
private fun isWithinAssertionStatusCheck(node: UExpression): Boolean {
var curr = node
while (true) {
val ifStatement = curr.getParentOfType<UIfExpression>(true) ?: break
// This is inefficient, but works around current resolve bug on javaClass access
if (ifStatement.condition.sourcePsi?.text?.contains("desiredAssertionStatus") == true) {
return true
}
curr = ifStatement
}
return false
}
}