/* * Copyright 2017 Google Inc. All Rights Reserved. * * 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.matchers.Description.NO_MATCH; import static com.google.errorprone.matchers.Matchers.hasAnnotation; import static com.google.errorprone.matchers.Matchers.instanceMethod; import static com.google.errorprone.matchers.Matchers.toType; import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; import static com.google.errorprone.util.ASTHelpers.getReceiver; import static com.google.errorprone.util.ASTHelpers.getSymbol; import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.MustBeClosed; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.sun.source.tree.BlockTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; 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; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import java.util.Objects; import javax.annotation.Nullable; import javax.lang.model.element.ElementKind; /** * An abstract check for resources that must be closed; used by {@link FilesLinesLeak} and {@link * MustBeClosedChecker}. */ public abstract class AbstractMustBeClosedChecker extends BugChecker { protected static final Matcher<Tree> HAS_MUST_BE_CLOSED_ANNOTATION = hasAnnotation(MustBeClosed.class.getCanonicalName()); private static final Matcher<ExpressionTree> CLOSE_METHOD = instanceMethod().onDescendantOf("java.lang.AutoCloseable").named("close"); private static final Matcher<Tree> MOCKITO_MATCHER = toType( MethodInvocationTree.class, staticMethod().onClass("org.mockito.Mockito").named("when")); /** * Check that constructors and methods annotated with {@link MustBeClosed} occur within the * resource variable initializer of a try-with-resources statement. */ protected Description matchNewClassOrMethodInvocation(Tree tree, VisitorState state) { Description description = checkClosed(tree, state); if (description == NO_MATCH) { return NO_MATCH; } if (AbstractReturnValueIgnored.expectedExceptionTest(tree, state) || MOCKITO_MATCHER.matches(state.getPath().getParentPath().getLeaf(), state)) { return NO_MATCH; } return description; } private Description checkClosed(Tree tree, VisitorState state) { MethodTree callerMethodTree = enclosingMethod(state); if (state.getPath().getParentPath().getLeaf().getKind() == Tree.Kind.RETURN && callerMethodTree != null) { // The invocation occurs within a return statement of a method, instead of a lambda // expression or anonymous class. if (HAS_MUST_BE_CLOSED_ANNOTATION.matches(callerMethodTree, state)) { // Ignore invocations of annotated methods and constructors that occur in the return // statement of an annotated caller method, since invocations of the caller are enforced. return NO_MATCH; } // The caller method is not annotated, so the closing of the returned resource is not // enforced. Suggest fixing this by annotating the caller method. return buildDescription(tree) .addFix( SuggestedFix.builder() .prefixWith(callerMethodTree, "@MustBeClosed\n") .addImport(MustBeClosed.class.getCanonicalName()) .build()) .build(); } if (!inTWR(state)) { // The constructor or method invocation does not occur within the resource variable // initializer of a try-with-resources statement. Description.Builder description = buildDescription(tree); addFix(description, tree, state); return description.build(); } return NO_MATCH; } /** * Returns the enclosing method of the given visitor state. Returns null if the state is within a * lambda expression or anonymous class. */ @Nullable private static MethodTree enclosingMethod(VisitorState state) { for (Tree node : state.getPath().getParentPath()) { switch (node.getKind()) { case LAMBDA_EXPRESSION: case NEW_CLASS: return null; case METHOD: return (MethodTree) node; default: break; } } return null; } /** * Returns whether an invocation occurs within the resource variable initializer of a * try-with-resources statement. */ // TODO(cushon): This method has been copied from FilesLinesLeak. Move it to a shared class. private boolean inTWR(VisitorState state) { TreePath path = state.getPath().getParentPath(); while (path.getLeaf().getKind() == Tree.Kind.CONDITIONAL_EXPRESSION) { path = path.getParentPath(); } Symbol sym = getSymbol(path.getLeaf()); if (!(sym instanceof VarSymbol)) { return false; } VarSymbol var = (VarSymbol) sym; return var.getKind() == ElementKind.RESOURCE_VARIABLE || tryFinallyClose(var, path, state); } private boolean tryFinallyClose(VarSymbol var, TreePath path, VisitorState state) { if ((var.flags() & (Flags.FINAL | Flags.EFFECTIVELY_FINAL)) == 0) { return false; } Tree parent = path.getParentPath().getLeaf(); if (parent.getKind() != Tree.Kind.BLOCK) { return false; } BlockTree block = (BlockTree) parent; int idx = block.getStatements().indexOf(path.getLeaf()); if (idx == -1 || idx == block.getStatements().size() - 1) { return false; } StatementTree next = block.getStatements().get(idx + 1); if (!(next instanceof TryTree)) { return false; } TryTree tryTree = (TryTree) next; if (tryTree.getFinallyBlock() == null) { return false; } boolean[] closed = {false}; tryTree .getFinallyBlock() .accept( new TreeScanner<Void, Void>() { @Override public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { if (CLOSE_METHOD.matches(tree, state) && Objects.equals(getSymbol(getReceiver(tree)), var)) { closed[0] = true; } return null; } }, null); return closed[0]; } protected void addFix(Description.Builder description, Tree tree, VisitorState state) {} }