All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.LogDetector Maven / Gradle / Ivy

/*
 * 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.ResolvedField;
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.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.BinaryOperator;
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 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 iterator = node.astArguments().iterator();
                if (tagArgumentIndex == 1) {
                    iterator.next();
                }
                Node argument = iterator.next();
                String tag = findLiteralValue(context, argument);
                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 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 = findLiteralValue(context, argument);
                //noinspection VariableNotUsedInsideIf
                if (string != null) { // does it resolve to a constant?
                    return false;
                }
            } else if (argument instanceof Select) {
                String string = findLiteralValue(context, argument);
                //noinspection VariableNotUsedInsideIf
                if (string != null) {
                    return false;
                }
            }

            // Method invocations etc
            return true;
        }

        return false;
    }

    @Nullable
    private static String findLiteralValue(@NonNull JavaContext context, @NonNull Node argument) {
        if (argument instanceof StringLiteral) {
            return ((StringLiteral)argument).astValue();
        } else if (argument instanceof BinaryExpression) {
            BinaryExpression expression = (BinaryExpression) argument;
            if (expression.astOperator() == BinaryOperator.PLUS) {
                String left = findLiteralValue(context, expression.astLeft());
                String right = findLiteralValue(context, expression.astRight());
                if (left != null && right != null) {
                    return left + right;
                }
            }
        } else {
            ResolvedNode resolved = context.resolve(argument);
            if (resolved instanceof ResolvedField) {
                ResolvedField field = (ResolvedField) resolved;
                Object value = field.getValue();
                if (value instanceof String) {
                    return (String)value;
                }
            }
        }

        return null;
    }

    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 isLogIterator = call.astArguments().iterator();
        Iterator 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);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy