| /* |
| * 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 static com.android.tools.lint.client.api.JavaParser.TYPE_STRING; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedMethod; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedNode; |
| 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.Location; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import lombok.ast.AstVisitor; |
| import lombok.ast.BinaryExpression; |
| import lombok.ast.ClassDeclaration; |
| import lombok.ast.Expression; |
| import lombok.ast.If; |
| import lombok.ast.MethodInvocation; |
| import lombok.ast.Node; |
| import lombok.ast.Select; |
| import lombok.ast.StringLiteral; |
| import lombok.ast.VariableReference; |
| |
| /** |
| * Detector for finding inefficiencies and errors in logging calls. |
| */ |
| public class LogDetector extends Detector implements Detector.JavaScanner { |
| 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$ |
| private static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$ |
| private static final String PRINTLN = "println"; //$NON-NLS-1$ |
| |
| // ---- Implements Detector.JavaScanner ---- |
| |
| @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 AstVisitor visitor, @NonNull MethodInvocation node) { |
| ResolvedNode resolved = context.resolve(node); |
| if (!(resolved instanceof ResolvedMethod)) { |
| return; |
| } |
| |
| ResolvedMethod method = (ResolvedMethod) resolved; |
| if (!method.getContainingClass().matches(LOG_CLS)) { |
| return; |
| } |
| |
| String name = node.astName().astValue(); |
| boolean withinConditional = IS_LOGGABLE.equals(name) || |
| checkWithinConditional(context, node.getParent(), 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(context, 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) { ... }`", |
| node.astName().toString()); |
| context.report(CONDITIONAL, node, context.getLocation(node), message); |
| } |
| |
| // Check tag length |
| if (context.isEnabled(LONG_TAG)) { |
| int tagArgumentIndex = PRINTLN.equals(name) ? 1 : 0; |
| if (method.getArgumentCount() > tagArgumentIndex |
| && method.getArgumentType(tagArgumentIndex).matchesSignature(TYPE_STRING) |
| && node.astArguments().size() == method.getArgumentCount()) { |
| Iterator<Expression> iterator = node.astArguments().iterator(); |
| if (tagArgumentIndex == 1) { |
| iterator.next(); |
| } |
| Node argument = iterator.next(); |
| 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.getLocation(node), message); |
| } |
| } |
| } |
| } |
| |
| /** Returns true if the given logging call performs "work" to compute the message */ |
| private static boolean performsWork( |
| @NonNull JavaContext context, |
| @NonNull MethodInvocation node) { |
| int messageArgumentIndex = PRINTLN.equals(node.astName().astValue()) ? 2 : 1; |
| if (node.astArguments().size() >= messageArgumentIndex) { |
| Iterator<Expression> iterator = node.astArguments().iterator(); |
| Node argument = null; |
| for (int i = 0; i <= messageArgumentIndex; i++) { |
| argument = iterator.next(); |
| } |
| if (argument == null) { |
| return false; |
| } |
| if (argument instanceof StringLiteral || argument instanceof VariableReference) { |
| return false; |
| } |
| if (argument instanceof BinaryExpression) { |
| String string = ConstantEvaluator.evaluateString(context, argument, false); |
| //noinspection VariableNotUsedInsideIf |
| if (string != null) { // does it resolve to a constant? |
| return false; |
| } |
| } else if (argument instanceof Select) { |
| String string = ConstantEvaluator.evaluateString(context, argument, false); |
| //noinspection VariableNotUsedInsideIf |
| if (string != null) { |
| return false; |
| } |
| } |
| |
| // Method invocations etc |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private static boolean checkWithinConditional( |
| @NonNull JavaContext context, |
| @Nullable Node curr, |
| @NonNull MethodInvocation logCall) { |
| while (curr != null) { |
| if (curr instanceof If) { |
| If ifNode = (If) curr; |
| if (ifNode.astCondition() instanceof MethodInvocation) { |
| MethodInvocation call = (MethodInvocation) ifNode.astCondition(); |
| if (IS_LOGGABLE.equals(call.astName().astValue())) { |
| checkTagConsistent(context, logCall, call); |
| } |
| } |
| |
| return true; |
| } else if (curr instanceof MethodInvocation |
| || curr instanceof ClassDeclaration) { // static block |
| break; |
| } |
| curr = curr.getParent(); |
| } |
| return false; |
| } |
| |
| /** Checks that the tag passed to Log.s and Log.isLoggable match */ |
| private static void checkTagConsistent(JavaContext context, MethodInvocation logCall, |
| MethodInvocation call) { |
| Iterator<Expression> isLogIterator = call.astArguments().iterator(); |
| Iterator<Expression> logIterator = logCall.astArguments().iterator(); |
| if (!isLogIterator.hasNext() || !logIterator.hasNext()) { |
| return; |
| } |
| Expression isLoggableTag = isLogIterator.next(); |
| Expression logTag = logIterator.next(); |
| |
| //String callName = logCall.astName().astValue(); |
| String logCallName = logCall.astName().astValue(); |
| boolean isPrintln = PRINTLN.equals(logCallName); |
| if (isPrintln) { |
| if (!logIterator.hasNext()) { |
| return; |
| } |
| logTag = logIterator.next(); |
| } |
| |
| if (logTag != null) { |
| if (!isLoggableTag.toString().equals(logTag.toString())) { |
| ResolvedNode resolved1 = context.resolve(isLoggableTag); |
| ResolvedNode resolved2 = context.resolve(logTag); |
| if ((resolved1 == null || resolved2 == null || !resolved1.equals(resolved2)) |
| && context.isEnabled(WRONG_TAG)) { |
| Location location = context.getLocation(logTag); |
| Location alternate = context.getLocation(isLoggableTag); |
| alternate.setMessage("Conflicting tag"); |
| location.setSecondary(alternate); |
| String isLoggableDescription = resolved1 != null ? resolved1 |
| .getName() |
| : isLoggableTag.toString(); |
| String logCallDescription = resolved2 != null ? resolved2.getName() |
| : logTag.toString(); |
| 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, call, 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 || !isLogIterator.hasNext()) { // e.g. println |
| return; |
| } |
| Expression isLoggableLevel = isLogIterator.next(); |
| if (isLoggableLevel == null) { |
| return; |
| } |
| String levelString = isLoggableLevel.toString(); |
| if (isLoggableLevel instanceof Select) { |
| levelString = ((Select)isLoggableLevel).astIdentifier().astValue(); |
| } |
| if (levelString.isEmpty()) { |
| return; |
| } |
| char levelChar = Character.toLowerCase(levelString.charAt(0)); |
| if (logCallName.charAt(0) == levelChar || !context.isEnabled(WRONG_TAG)) { |
| return; |
| } |
| switch (levelChar) { |
| case 'd': |
| case 'e': |
| case 'i': |
| case 'v': |
| case 'w': |
| break; |
| default: |
| // Some other char; e.g. user passed in a literal value or some |
| // local constant or variable alias |
| return; |
| } |
| String expectedCall = String.valueOf(levelChar); |
| 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`", |
| levelString, expectedCall, logCallName); |
| Location location = context.getLocation(logCall.astName()); |
| Location alternate = context.getLocation(isLoggableLevel); |
| alternate.setMessage("Conflicting tag"); |
| location.setSecondary(alternate); |
| context.report(WRONG_TAG, call, location, message); |
| } |
| } |