Java tutorial
/* * Copyright 2017 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.android; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.matchers.Matchers.instanceMethod; import static com.google.errorprone.util.ASTHelpers.constValue; import static com.google.errorprone.util.ASTHelpers.findEnclosingNode; import static com.google.errorprone.util.ASTHelpers.getReceiver; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.getType; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableMultimap.Builder; import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; import com.google.errorprone.BugPattern.Category; import com.google.errorprone.BugPattern.ProvidesFix; import com.google.errorprone.BugPattern.SeverityLevel; import com.google.errorprone.BugPattern.StandardTags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; 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; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.CatchTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IfTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.LambdaExpressionTree.BodyKind; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TryTree; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.UnionClassType; import com.sun.tools.javac.code.Types; /** @author epmjohnston@google.com */ @BugPattern(name = "WakelockReleasedDangerously", tags = StandardTags.FRAGILE_CODE, summary = "A wakelock acquired with a timeout may be released by the system before calling" + " `release`, even after checking `isHeld()`. If so, it will throw a RuntimeException." + " Please wrap in a try/catch block.", severity = SeverityLevel.WARNING, category = Category.ANDROID, providesFix = ProvidesFix.REQUIRES_HUMAN_ATTENTION) public class WakelockReleasedDangerously extends BugChecker implements MethodInvocationTreeMatcher { private static final String WAKELOCK_CLASS_NAME = "android.os.PowerManager.WakeLock"; private static final Matcher<ExpressionTree> RELEASE = MethodMatchers.instanceMethod() .onExactClass(WAKELOCK_CLASS_NAME).named("release"); @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { // Match on calls to any override of WakeLock.release(). if (!RELEASE.matches(tree, state)) { return NO_MATCH; } // Ok if surrounded in try/catch block that catches RuntimeException. TryTree enclosingTry = findEnclosingNode(state.getPath(), TryTree.class); if (enclosingTry != null && tryCatchesException(enclosingTry, state.getSymtab().runtimeExceptionType, state)) { return NO_MATCH; } // Ok if WakeLock not in danger of unexpected exception. // Also, can't perform analysis if WakeLock symbol not found. Symbol wakelockSymbol = getSymbol(getReceiver(tree)); if (wakelockSymbol == null || !wakelockMayThrow(wakelockSymbol, state)) { return NO_MATCH; } Tree releaseStatement = state.getPath().getParentPath().getLeaf(); return describeMatch(releaseStatement, getFix(releaseStatement, wakelockSymbol, state)); } private SuggestedFix getFix(Tree releaseStatement, Symbol wakelockSymbol, VisitorState state) { // Wrap the release call line in a try/catch(RuntimeException) block. String before = "\ntry {\n"; String after = "\n} catch (RuntimeException unused) {\n" + "// Ignore: already released by timeout.\n" + "// TODO: Log this exception.\n" + "}\n"; // Lambda expressions are special. If the release call is in a one-expression lambda, // only wrap body (not args) and convert to block lambda. if (releaseStatement.getKind() == Kind.LAMBDA_EXPRESSION) { LambdaExpressionTree enclosingLambda = (LambdaExpressionTree) releaseStatement; if (enclosingLambda.getBodyKind() == BodyKind.EXPRESSION) { releaseStatement = enclosingLambda.getBody(); before = "{" + before; after = ";" + after + "}"; } } // Remove `if (wakelock.isHeld())` check. // TODO(epmjohnston): can avoid this if no isHeld check in class (check call map). IfTree enclosingIfHeld = findEnclosingNode(state.getPath(), IfTree.class); if (enclosingIfHeld != null) { ExpressionTree condition = ASTHelpers.stripParentheses(enclosingIfHeld.getCondition()); if (enclosingIfHeld.getElseStatement() == null && instanceMethod().onExactClass(WAKELOCK_CLASS_NAME).named("isHeld").matches(condition, state) && wakelockSymbol.equals(getSymbol(getReceiver(condition)))) { String ifBody = state.getSourceForNode(enclosingIfHeld.getThenStatement()).trim(); // Remove leading and trailing `{}` ifBody = ifBody.startsWith("{") ? ifBody.substring(1) : ifBody; ifBody = ifBody.endsWith("}") ? ifBody.substring(0, ifBody.length() - 1) : ifBody; ifBody = ifBody.trim(); String releaseStatementSource = state.getSourceForNode(releaseStatement); return SuggestedFix.replace(enclosingIfHeld, ifBody.replace(releaseStatementSource, before + releaseStatementSource + after)); } } return SuggestedFix.builder().prefixWith(releaseStatement, before).postfixWith(releaseStatement, after) .build(); } /** Return whether the given try-tree will catch the given exception type. */ private boolean tryCatchesException(TryTree tryTree, Type exceptionToCatch, VisitorState state) { Types types = state.getTypes(); return tryTree.getCatches().stream().anyMatch((CatchTree catchClause) -> { Type catchesException = getType(catchClause.getParameter().getType()); // Examine all alternative types of a union type. if (catchesException != null && catchesException.isUnion()) { return Streams.stream(((UnionClassType) catchesException).getAlternativeTypes()) .anyMatch(caught -> types.isSuperType(caught, exceptionToCatch)); } // Simple type, just check superclass. return types.isSuperType(catchesException, exceptionToCatch); }); } /** * Whether the given WakeLock may throw an unexpected RuntimeException when released. * * <p>Returns true if: 1) the given WakeLock was acquired with timeout, and 2) the given WakeLock * is reference counted. */ private boolean wakelockMayThrow(Symbol wakelockSymbol, VisitorState state) { ClassTree enclosingClass = getTopLevelClass(state); ImmutableMultimap<String, MethodInvocationTree> map = methodCallsForSymbol(wakelockSymbol, enclosingClass); // Was acquired with timeout. return map.get("acquire").stream().anyMatch(m -> m.getArguments().size() == 1) // Is reference counted, i.e., referenceCounted not explicitly set to false. && map.get("setReferenceCounted").stream() .noneMatch(m -> Boolean.FALSE.equals(constValue(m.getArguments().get(0), Boolean.class))); } private ClassTree getTopLevelClass(VisitorState state) { return (ClassTree) Streams .findLast(Streams.stream(state.getPath().iterator()).filter((Tree t) -> t.getKind() == Kind.CLASS)) .orElseThrow(() -> new IllegalArgumentException("No enclosing class found")); } /** * Finds all method invocations on the given symbol, and constructs a map of the called method's * name, to the {@link MethodInvocationTree} in which it was called. */ private ImmutableMultimap<String, MethodInvocationTree> methodCallsForSymbol(Symbol sym, ClassTree classTree) { Builder<String, MethodInvocationTree> methodMap = ImmutableMultimap.builder(); // Populate map builder with names of method called : the tree in which it is called. classTree.accept(new TreeScanner<Void, Void>() { @Override public Void visitMethodInvocation(MethodInvocationTree callTree, Void unused) { if (sym.equals(getSymbol(getReceiver(callTree)))) { MethodSymbol methodSymbol = getSymbol(callTree); if (methodSymbol != null) { methodMap.put(methodSymbol.getSimpleName().toString(), callTree); } } return super.visitMethodInvocation(callTree, unused); } }, null); return methodMap.build(); } }