/*
* 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.*;
import com.intellij.psi.*;
import org.jetbrains.uast.*;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.*;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
/**
* Detector for finding inefficiencies and errors in logging calls.
*/
public class LogDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LogDetector.class, Scope.JAVA_FILE_SCOPE);
/** Log call missing surrounding if */
public static final Issue CONDITIONAL = Issue.create(
"LogConditional", //$NON-NLS-1$
"Unconditional Logging Calls",
"The BuildConfig class (available in Tools 17) 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.\n" +
"\n" +
"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.PERFORMANCE,
5,
Severity.WARNING,
IMPLEMENTATION).setEnabledByDefault(false);
/** Mismatched tags between isLogging and log calls within it */
public static final Issue WRONG_TAG = Issue.create(
"LogTagMismatch", //$NON-NLS-1$
"Mismatched Log Tags",
"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.CORRECTNESS,
5,
Severity.ERROR,
IMPLEMENTATION);
/** Log tag is too long */
public static final Issue LONG_TAG = Issue.create(
"LongLogTag", //$NON-NLS-1$
"Too Long Log Tags",
"Log tags are only allowed to be at most 23 tag characters long.",
Category.CORRECTNESS,
5,
Severity.ERROR,
IMPLEMENTATION);
@SuppressWarnings("SpellCheckingInspection")
private static final String IS_LOGGABLE = "isLoggable"; //$NON-NLS-1$
public static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$
private static final String PRINTLN = "println"; //$NON-NLS-1$
private static final Map<String, String> TAG_PAIRS;
static {
Map<String, String> pairs = new HashMap<String, String>();
pairs.put("d", "DEBUG");
pairs.put("e", "ERROR");
pairs.put("i", "INFO");
pairs.put("v", "VERBOSE");
pairs.put("w", "WARN");
TAG_PAIRS = Collections.unmodifiableMap(pairs);
}
// ---- Implements Detector.UastScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
"d", //$NON-NLS-1$
"e", //$NON-NLS-1$
"i", //$NON-NLS-1$
"v", //$NON-NLS-1$
"w", //$NON-NLS-1$
PRINTLN,
IS_LOGGABLE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
if (!JavaEvaluator.isMemberInClass(method, LOG_CLS)) {
return;
}
String name = method.getName();
boolean withinConditional = IS_LOGGABLE.equals(name) ||
checkWithinConditional(context, node.getUastParent(), 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".equals(name) || "d".equals(name) || "v".equals(name) || PRINTLN.equals(name))
&& !withinConditional
&& performsWork(node)
&& context.isEnabled(CONDITIONAL)) {
String message = String.format("The log call Log.%1$s(...) should be " +
"conditional: surround with `if (Log.isLoggable(...))` or " +
"`if (BuildConfig.DEBUG) { ... }`",
name);
context.report(CONDITIONAL, node, context.getUastLocation(node), message);
}
// Check tag length
if (context.isEnabled(LONG_TAG)) {
int tagArgumentIndex = PRINTLN.equals(name) ? 1 : 0;
PsiParameterList parameterList = method.getParameterList();
List<UExpression> argumentList = node.getValueArguments();
if (evaluator.parameterHasType(method, tagArgumentIndex, TYPE_STRING)
&& parameterList.getParametersCount() == argumentList.size()) {
UExpression argument = argumentList.get(tagArgumentIndex);
String tag = ConstantEvaluator.evaluateString(context, argument, true);
if (tag != null && tag.length() > 23) {
String message = String.format(
"The logging tag can be at most 23 characters, was %1$d (%2$s)",
tag.length(), tag);
context.report(LONG_TAG, node, context.getUastLocation(node), message);
}
}
}
}
/** Returns true if the given logging call performs "work" to compute the message */
private static boolean performsWork(@NonNull UCallExpression node) {
String referenceName = node.getMethodName();
if (referenceName == null) {
return false;
}
int messageArgumentIndex = PRINTLN.equals(referenceName) ? 2 : 1;
List<UExpression> arguments = node.getValueArguments();
if (arguments.size() > messageArgumentIndex) {
UExpression argument = arguments.get(messageArgumentIndex);
if (argument == null) {
return false;
}
if (argument instanceof ULiteralExpression) {
return false;
}
if (argument instanceof UBinaryExpression) {
String string = UastUtils.evaluateString(argument);
//noinspection VariableNotUsedInsideIf
if (string != null) { // does it resolve to a constant?
return false;
}
} else if (argument instanceof USimpleNameReferenceExpression) {
// Just a simple local variable/field reference
return false;
} else if (argument instanceof UQualifiedReferenceExpression) {
String string = UastUtils.evaluateString(argument);
//noinspection VariableNotUsedInsideIf
if (string != null) {
return false;
}
PsiElement resolved = ((UQualifiedReferenceExpression) argument).resolve();
if (resolved instanceof PsiVariable) {
// Just a reference to a property/field, parameter or variable
return false;
}
}
// Method invocations etc
return true;
}
return false;
}
private static boolean checkWithinConditional(
@NonNull JavaContext context,
@Nullable UElement curr,
@NonNull UCallExpression logCall) {
while (curr != null) {
if (curr instanceof UIfExpression) {
UExpression condition = ((UIfExpression) curr).getCondition();
if (condition instanceof UQualifiedReferenceExpression) {
condition = getLastInQualifiedChain((UQualifiedReferenceExpression) condition);
}
if (condition instanceof UCallExpression) {
UCallExpression call = (UCallExpression) condition;
if (IS_LOGGABLE.equals(call.getMethodName())) {
checkTagConsistent(context, logCall, call);
}
}
return true;
} else if (curr instanceof UCallExpression
|| curr instanceof UMethod
|| curr instanceof UClassInitializer
|| curr instanceof UField
|| curr instanceof UClass) { // static block
break;
}
curr = curr.getUastParent();
}
return false;
}
/** Checks that the tag passed to Log.s and Log.isLoggable match */
private static void checkTagConsistent(JavaContext context, UCallExpression logCall,
UCallExpression isLoggableCall) {
List<UExpression> isLoggableArguments = isLoggableCall.getValueArguments();
List<UExpression> logArguments = logCall.getValueArguments();
if (isLoggableArguments.isEmpty() || logArguments.isEmpty()) {
return;
}
UExpression isLoggableTag = isLoggableArguments.get(0);
UExpression logTag = logArguments.get(0);
String logCallName = logCall.getMethodName();
if (logCallName == null) {
return;
}
boolean isPrintln = PRINTLN.equals(logCallName);
if (isPrintln && logArguments.size() > 1) {
logTag = logArguments.get(1);
}
if (logTag != null) {
if (!areLiteralsEqual(isLoggableTag, logTag) &&
!UastLintUtils.areIdentifiersEqual(isLoggableTag, logTag)) {
PsiNamedElement resolved1 = UastUtils.tryResolveNamed(isLoggableTag);
PsiNamedElement resolved2 = UastUtils.tryResolveNamed(logTag);
if ((resolved1 == null || resolved2 == null || !resolved1.equals(resolved2))
&& context.isEnabled(WRONG_TAG)) {
Location location = context.getUastLocation(logTag);
Location alternate = context.getUastLocation(isLoggableTag);
alternate.setMessage("Conflicting tag");
location.setSecondary(alternate);
String isLoggableDescription = resolved1 != null
? resolved1.getName()
: isLoggableTag.asRenderString();
String logCallDescription = resolved2 != null
? resolved2.getName()
: logTag.asRenderString();
String 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;
}
UExpression isLoggableLevel = isLoggableArguments.get(1);
if (isLoggableLevel == null) {
return;
}
PsiNamedElement resolved = UastUtils.tryResolveNamed(isLoggableLevel);
if (resolved == null) {
return;
}
if (resolved instanceof PsiVariable) {
PsiClass containingClass = UastUtils.getContainingClass(resolved);
if (containingClass == null
|| !"android.util.Log".equals(containingClass.getQualifiedName())
|| resolved.getName() == null
|| resolved.getName().equals(TAG_PAIRS.get(logCallName))) {
return;
}
String expectedCall = resolved.getName().substring(0, 1)
.toLowerCase(Locale.getDefault());
String 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);
Location location = context.getUastLocation(logCall.getMethodIdentifier());
Location alternate = context.getUastLocation(isLoggableLevel);
alternate.setMessage("Conflicting tag");
location.setSecondary(alternate);
context.report(WRONG_TAG, isLoggableCall, location, message);
}
}
@NonNull
private static UExpression getLastInQualifiedChain(@NonNull UQualifiedReferenceExpression node) {
UExpression last = node.getSelector();
while (last instanceof UQualifiedReferenceExpression) {
last = ((UQualifiedReferenceExpression) last).getSelector();
}
return last;
}
private static boolean areLiteralsEqual(UExpression first, UExpression second) {
if (!(first instanceof ULiteralExpression)) {
return false;
}
if (!(second instanceof ULiteralExpression)) {
return false;
}
Object firstValue = ((ULiteralExpression) first).getValue();
Object secondValue = ((ULiteralExpression) second).getValue();
if (firstValue == null) {
return secondValue == null;
}
return firstValue.equals(secondValue);
}
}