com.google.errorprone.bugpatterns.LockNotBeforeTry.java Source code

Java tutorial

Introduction

Here is the source code for com.google.errorprone.bugpatterns.LockNotBeforeTry.java

Source

/*
 * Copyright 2018 The Error Prone Authors.
 *
 * 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.google.errorprone.bugpatterns;

import static com.google.errorprone.BugPattern.ProvidesFix.REQUIRES_HUMAN_ATTENTION;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.FRAGILE_CODE;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.util.ASTHelpers.getReceiver;

import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.method.MethodMatchers.MethodClassMatcher;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TryTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;

/**
 * Suggests that calls to {@code Lock.lock} must be immediately followed by a {@code try-finally}
 * that calls {@code Lock.unlock}.
 *
 * @author ghm@google.com (Graeme Morgan)
 */
@BugPattern(name = "LockNotBeforeTry", summary = "Calls to Lock#lock should be immediately followed by a try block which releases the lock.", severity = WARNING, tags = FRAGILE_CODE, providesFix = REQUIRES_HUMAN_ATTENTION)
public final class LockNotBeforeTry extends BugChecker implements MethodInvocationTreeMatcher {

    private static final MethodClassMatcher LOCK_METHOD = instanceMethod()
            .onDescendantOf("java.util.concurrent.locks.Lock");
    private static final Matcher<ExpressionTree> LOCK = LOCK_METHOD.named("lock");
    private static final Matcher<ExpressionTree> UNLOCK = LOCK_METHOD.named("unlock");

    @Override
    public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
        if (!LOCK.matches(tree, state)) {
            return NO_MATCH;
        }
        Tree parent = state.getPath().getParentPath().getLeaf();
        if (!(parent instanceof StatementTree)) {
            return NO_MATCH;
        }
        Tree enclosing = state.getPath().getParentPath().getParentPath().getLeaf();
        if (!(enclosing instanceof BlockTree)) {
            return NO_MATCH;
        }
        BlockTree block = (BlockTree) enclosing;
        int index = block.getStatements().indexOf(parent);
        if (index + 1 < block.getStatements().size()) {
            StatementTree nextStatement = block.getStatements().get(index + 1);
            if (nextStatement instanceof TryTree) {
                return NO_MATCH;
            }
        }
        return describe(tree, state.getPath().getParentPath(), state);
    }

    private Description describe(MethodInvocationTree lockInvocation, TreePath statementPath, VisitorState state) {
        Tree lockStatement = statementPath.getLeaf();
        ExpressionTree lockee = getReceiver(lockInvocation);
        if (lockee == null) {
            return NO_MATCH;
        }
        TryTree enclosingTry = state.findEnclosing(TryTree.class);
        if (enclosingTry != null && releases(enclosingTry, lockee, state)) {
            SuggestedFix fix = SuggestedFix.builder().replace(lockStatement, "")
                    .prefixWith(enclosingTry, state.getSourceForNode(lockStatement)).build();
            return buildDescription(lockInvocation).addFix(fix)
                    .setMessage(String.format(
                            "Prefer obtaining the lock for %s outside the try block. That way, if #lock"
                                    + " throws, the lock is not erroneously released.",
                            state.getSourceForNode(getReceiver(lockInvocation))))
                    .build();
        }
        Tree enclosing = state.getPath().getParentPath().getParentPath().getLeaf();
        if (!(enclosing instanceof BlockTree)) {
            return NO_MATCH;
        }
        BlockTree block = (BlockTree) enclosing;
        int index = block.getStatements().indexOf(lockStatement);
        // Scan through the enclosing statements
        for (StatementTree statement : Iterables.skip(block.getStatements(), index + 1)) {
            // ... for a try/finally which releases this lock.
            if (statement instanceof TryTree && releases((TryTree) statement, lockee, state)) {
                SuggestedFix fix = SuggestedFix.builder().replace(lockStatement, "")
                        .prefixWith(statement, state.getSourceForNode(lockStatement)).build();
                return buildDescription(lockInvocation).addFix(fix)
                        .setMessage("Prefer locking *immediately* before the try block which releases the lock to"
                                + " avoid the possibility of any intermediate statements throwing.")
                        .build();
            }
            // ... or an unlock at the same level.
            if (statement instanceof ExpressionStatementTree) {
                ExpressionTree expression = ((ExpressionStatementTree) statement).getExpression();
                if (acquires(expression, lockee, state)) {
                    return buildDescription(lockInvocation)
                            .setMessage(String.format("Did you forget to release the lock on %s?",
                                    state.getSourceForNode(getReceiver(lockInvocation))))
                            .build();
                }
                if (releases(expression, lockee, state)) {
                    SuggestedFix fix = SuggestedFix.builder().postfixWith(lockStatement, "try {")
                            .prefixWith(statement, "} finally {").postfixWith(statement, "}").build();
                    return buildDescription(lockInvocation).addFix(fix)
                            .setMessage(String.format("Prefer releasing the lock on %s inside a finally block.",
                                    state.getSourceForNode(getReceiver(lockInvocation))))
                            .build();
                }
            }
        }
        return NO_MATCH;
    }

    private static boolean releases(TryTree tryTree, ExpressionTree lockee, VisitorState state) {
        if (tryTree.getFinallyBlock() == null) {
            return false;
        }
        // False if a different lock was released, true if 'lockee' was released, null otherwise.
        Boolean released = new TreeScanner<Boolean, Void>() {
            @Override
            public Boolean reduce(Boolean r1, Boolean r2) {
                return r1 == null ? r2 : (r2 == null ? null : r1 && r2);
            }

            @Override
            public Boolean visitMethodInvocation(MethodInvocationTree node, Void unused) {
                if (UNLOCK.matches(node, state)) {
                    return releases(node, lockee, state);
                }
                return super.visitMethodInvocation(node, null);
            }
        }.scan(tryTree.getFinallyBlock(), null);
        return released == null ? false : released;
    }

    private static boolean releases(ExpressionTree node, ExpressionTree lockee, VisitorState state) {
        if (!UNLOCK.matches(node, state)) {
            return false;
        }
        ExpressionTree receiver = getReceiver(node);
        return receiver != null && UNLOCK.matches(node, state)
                && state.getSourceForNode(receiver).equals(state.getSourceForNode(lockee));
    }

    private static boolean acquires(ExpressionTree node, ExpressionTree lockee, VisitorState state) {
        if (!LOCK.matches(node, state)) {
            return false;
        }
        ExpressionTree receiver = getReceiver(node);
        return receiver != null && LOCK.matches(node, state)
                && state.getSourceForNode(receiver).equals(state.getSourceForNode(lockee));
    }
}