blob: 192caf6a7ebe8f28b435023e872656281e26477f [file] [log] [blame]
/*
* Copyright (C) 2015 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.client.api.TYPE_STRING
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ConstantEvaluator
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.android.tools.lint.detector.api.UastLintUtils
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiVariable
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UClassInitializer
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UField
import org.jetbrains.uast.UIfExpression
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.USimpleNameReferenceExpression
import org.jetbrains.uast.evaluateString
import org.jetbrains.uast.tryResolveNamed
import java.util.Locale
/**
* Detector for finding inefficiencies and errors in logging calls.
*/
class LogDetector : Detector(), SourceCodeScanner {
companion object Issues {
private val IMPLEMENTATION = Implementation(
LogDetector::class.java, Scope.JAVA_FILE_SCOPE
)
/** Log call missing surrounding if */
@JvmField
val CONDITIONAL = Issue.create(
id = "LogConditional",
briefDescription = "Unconditional Logging Calls",
explanation = """
The `BuildConfig` class provides a constant, `DEBUG`, which indicates \
whether the code is being built in release mode or in debug mode. In release mode, you typically \
want to strip out all the logging calls. Since the compiler will automatically remove all code \
which is inside a `if (false)` check, surrounding your logging calls with a check for \
`BuildConfig.DEBUG` is a good idea.
If you **really** intend for the logging to be present in release mode, you can suppress this \
warning with a `@SuppressLint` annotation for the intentional logging calls.""",
category = Category.PERFORMANCE,
priority = 5,
severity = Severity.WARNING,
androidSpecific = true,
implementation = IMPLEMENTATION
).setEnabledByDefault(false)
/** Mismatched tags between isLogging and log calls within it */
@JvmField
val WRONG_TAG = Issue.create(
id = "LogTagMismatch",
briefDescription = "Mismatched Log Tags",
explanation = """
When guarding a `Log.v(tag, ...)` call with `Log.isLoggable(tag)`, the tag passed to both calls \
should be the same. Similarly, the level passed in to `Log.isLoggable` should typically match \
the type of `Log` call, e.g. if checking level `Log.DEBUG`, the corresponding `Log` call should \
be `Log.d`, not `Log.i`.""",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION
)
/** Log tag is too long */
@JvmField
val LONG_TAG = Issue.create(
id = "LongLogTag",
briefDescription = "Too Long Log Tags",
explanation = """
Log tags are only allowed to be at most 23 tag characters long.""",
category = Category.CORRECTNESS,
priority = 5,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION
)
private const val IS_LOGGABLE = "isLoggable"
private const val PRINTLN = "println"
const val LOG_CLS = "android.util.Log"
}
override fun getApplicableMethodNames(): List<String>? =
listOf(
"d",
"e",
"i",
"v",
"w",
PRINTLN,
IS_LOGGABLE
)
override fun visitMethodCall(
context: JavaContext,
node: UCallExpression,
method: PsiMethod
) {
val evaluator = context.evaluator
if (!evaluator.isMemberInClass(method, LOG_CLS)) {
return
}
val name = method.name
val withinConditional =
IS_LOGGABLE == name || checkWithinConditional(context, node.uastParent, node)
// See if it's surrounded by an if statement (and it's one of the non-error, spammy
// log methods (info, verbose, etc))
if (("i" == name || "d" == name || "v" == name || PRINTLN == name) &&
!withinConditional &&
performsWork(node) &&
context.isEnabled(CONDITIONAL)
) {
val message = String.format(
"The log call Log.%1\$s(...) should be " +
"conditional: surround with `if (Log.isLoggable(...))` or " +
"`if (BuildConfig.DEBUG) { ... }`",
node.methodName
)
val location = context.getLocation(node)
context.report(CONDITIONAL, node, location, message)
}
// Check tag length
if (context.isEnabled(LONG_TAG)) {
val tagArgumentIndex = if (PRINTLN == name) 1 else 0
val parameterList = method.parameterList
val argumentList = node.valueArguments
if (evaluator.parameterHasType(method, tagArgumentIndex, TYPE_STRING) &&
parameterList.parametersCount == argumentList.size
) {
val argument = argumentList[tagArgumentIndex]
val tag = ConstantEvaluator.evaluateString(context, argument, true)
if (tag != null && tag.length > 23 && context.mainProject.minSdk <= 23) {
val message =
"The logging tag can be at most 23 characters, was ${tag.length} ($tag)"
context.report(LONG_TAG, node, context.getLocation(argument), message)
}
}
}
}
private fun getTagForMethod(method: String): String? =
when (method) {
"d" -> "DEBUG"
"e" -> "ERROR"
"i" -> "INFO"
"v" -> "VERBOSE"
"w" -> "WARN"
else -> null
}
/** Returns true if the given logging call performs "work" to compute the message */
private fun performsWork(node: UCallExpression): Boolean {
val referenceName = node.methodName ?: return false
val messageArgumentIndex = if (PRINTLN == referenceName) 2 else 1
val arguments = node.valueArguments
if (arguments.size > messageArgumentIndex) {
val argument = arguments[messageArgumentIndex]
return performsWork(argument)
}
return false
}
private fun performsWork(argument: UExpression): Boolean {
if (argument is ULiteralExpression) {
return false
}
if (argument is UPolyadicExpression) {
val string = argument.evaluateString()
if (string != null) { // does it resolve to a constant?
return false
}
} else if (argument is UBinaryExpression) {
// Not currently a polyadic expr in UAST: repeat check done for polyadic
val string = argument.evaluateString()
if (string != null) { // does it resolve to a constant?
return false
}
} else if (argument is USimpleNameReferenceExpression) {
// Just a simple local variable/field reference
return false
} else if (argument is UParenthesizedExpression) {
return performsWork(argument.expression)
} else if (argument is UQualifiedReferenceExpression) {
val string = argument.evaluateString()
if (string != null) {
return false
}
val resolved = argument.resolve()
if (resolved is PsiVariable) {
// Just a reference to a property/field, parameter or variable
return false
}
}
// Method invocations etc
return true
}
private fun checkWithinConditional(
context: JavaContext,
start: UElement?,
logCall: UCallExpression
): Boolean {
var curr = start
while (curr != null) {
if (curr is UIfExpression) {
var condition = curr.condition
if (condition is UQualifiedReferenceExpression) {
condition = getLastInQualifiedChain(condition)
}
if (condition is UCallExpression) {
if (IS_LOGGABLE == condition.methodName) {
checkTagConsistent(context, logCall, condition)
}
}
return true
} else if (curr is UCallExpression ||
curr is UMethod ||
curr is UClassInitializer ||
curr is UField ||
curr is UClass
) { // static block
break
}
curr = curr.uastParent
}
return false
}
/** Checks that the tag passed to Log.s and Log.isLoggable match */
private fun checkTagConsistent(
context: JavaContext,
logCall: UCallExpression,
isLoggableCall: UCallExpression
) {
val isLoggableArguments = isLoggableCall.valueArguments
val logArguments = logCall.valueArguments
if (isLoggableArguments.isEmpty() || logArguments.isEmpty()) {
return
}
val isLoggableTag = isLoggableArguments[0]
var logTag: UExpression? = logArguments[0]
val logCallName = logCall.methodName ?: return
val isPrintln = PRINTLN == logCallName
if (isPrintln && logArguments.size > 1) {
logTag = logArguments[1]
}
if (logTag != null) {
if (!areLiteralsEqual(isLoggableTag, logTag) && !UastLintUtils.areIdentifiersEqual(
isLoggableTag,
logTag
)
) {
val resolved1 = isLoggableTag.tryResolveNamed()
val resolved2 = logTag.tryResolveNamed()
if ((resolved1 == null || resolved2 == null || resolved1 != resolved2) && context.isEnabled(
WRONG_TAG
)
) {
val location = context.getLocation(logTag)
val alternate = context.getLocation(isLoggableTag)
alternate.message = "Conflicting tag"
location.secondary = alternate
val isLoggableDescription = if (resolved1 != null)
resolved1.name
else isLoggableTag.asRenderString()
val logCallDescription = if (resolved2 != null)
resolved2.name
else logTag.asRenderString()
val message = String.format(
"Mismatched tags: the `%1\$s()` and `isLoggable()` calls typically should pass the same tag: `%2\$s` versus `%3\$s`",
logCallName,
isLoggableDescription,
logCallDescription
)
context.report(WRONG_TAG, isLoggableCall, location, message)
}
}
}
// Check log level versus the actual log call type (e.g. flag
// if (Log.isLoggable(TAG, Log.DEBUG) Log.info(TAG, "something")
if (logCallName.length != 1 || isLoggableArguments.size < 2) { // e.g. println
return
}
val isLoggableLevel = isLoggableArguments[1]
val resolved = isLoggableLevel.tryResolveNamed() ?: return
if (resolved is PsiVariable) {
val containingClass = PsiTreeUtil.getParentOfType(resolved, PsiClass::class.java)
if (containingClass == null ||
"android.util.Log" != containingClass.qualifiedName ||
resolved.getName() == null ||
resolved.getName() == getTagForMethod(logCallName)
) {
return
}
val expectedCall = resolved.getName()!!.substring(0, 1)
.toLowerCase(Locale.getDefault())
val message = String.format(
"Mismatched logging levels: when checking `isLoggable` level `%1\$s`, the " +
"corresponding log call should be `Log.%2\$s`, not `Log.%3\$s`",
resolved.getName(), expectedCall, logCallName
)
val location = context.getCallLocation(logCall, false, false)
val alternate = context.getLocation(isLoggableLevel)
alternate.message = "Conflicting tag"
location.secondary = alternate
context.report(WRONG_TAG, isLoggableCall, location, message)
}
}
private fun getLastInQualifiedChain(node: UQualifiedReferenceExpression): UExpression {
var last = node.selector
while (last is UQualifiedReferenceExpression) {
last = last.selector
}
return last
}
private fun areLiteralsEqual(first: UExpression, second: UExpression): Boolean {
if (first !is ULiteralExpression) {
return false
}
if (second !is ULiteralExpression) {
return false
}
val firstValue = first.value
val secondValue = second.value
if (firstValue == null) {
return secondValue == null
}
return firstValue == secondValue
}
}