/* * 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.util; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.Iterables.getLast; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static java.util.Objects.requireNonNull; import com.sun.source.tree.AssertTree; import com.sun.source.tree.BlockTree; import com.sun.source.tree.BreakTree; import com.sun.source.tree.CaseTree; import com.sun.source.tree.CatchTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ContinueTree; import com.sun.source.tree.DoWhileLoopTree; import com.sun.source.tree.EmptyStatementTree; import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ForLoopTree; import com.sun.source.tree.IfTree; import com.sun.source.tree.LabeledStatementTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.ReturnTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.SwitchTree; import com.sun.source.tree.SynchronizedTree; import com.sun.source.tree.ThrowTree; import com.sun.source.tree.Tree; import com.sun.source.tree.TryTree; import com.sun.source.tree.VariableTree; import com.sun.source.tree.WhileLoopTree; import com.sun.source.util.SimpleTreeVisitor; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.tree.JCTree; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; /** An implementation of JLS 14.21 reachability. */ public class Reachability { /** * Returns true if the given statement can complete normally, as defined by JLS 14.21. * * <p>An exception is made for {@code System.exit}, which cannot complete normally in practice. */ public static boolean canCompleteNormally(StatementTree statement) { return statement.accept(new CanCompleteNormallyVisitor(), null); } /** * Returns true if the given case tree can complete normally, as defined by JLS 14.21. * * <p>An exception is made for {@code System.exit}, which cannot complete normally in practice. */ public static boolean canCompleteNormally(CaseTree caseTree) { List<? extends StatementTree> statements = caseTree.getStatements(); if (statements.isEmpty()) { return true; } // We only care whether the last statement completes; javac would have already // reported an error if that statement wasn't reachable, and the answer is // independent of any preceding statements. // TODO(cushon): This isn't really making an exception for System.exit in the prior statements. return canCompleteNormally(getLast(statements)); } private static class CanCompleteNormallyVisitor extends SimpleTreeVisitor<Boolean, Void> { /** Trees that are the target of a reachable break statement. */ private final Set<Tree> breaks = new HashSet<>(); /** Trees that are the target of a reachable continue statement. */ private final Set<Tree> continues = new HashSet<>(); boolean scan(List<? extends StatementTree> trees) { boolean completes = true; for (StatementTree tree : trees) { completes = scan(tree); } return completes; } private boolean scan(Tree tree) { return tree.accept(this, null); } /* A break statement cannot complete normally. */ @Override public Boolean visitBreak(BreakTree tree, Void unused) { breaks.add(skipLabel(requireNonNull(((JCTree.JCBreak) tree).target))); return false; } /* A continue statement cannot complete normally. */ @Override public Boolean visitContinue(ContinueTree tree, Void unused) { continues.add(skipLabel(requireNonNull(((JCTree.JCContinue) tree).target))); return false; } private Tree skipLabel(JCTree tree) { return tree.hasTag(JCTree.Tag.LABELLED) ? ((JCTree.JCLabeledStatement) tree).body : tree; } @Override public Boolean visitBlock(BlockTree tree, Void unused) { return scan(tree.getStatements()); } /* A local class declaration statement can complete normally iff it is reachable. */ @Override public Boolean visitClass(ClassTree tree, Void unused) { return true; } /* A local variable declaration statement can complete normally iff it is reachable. */ @Override public Boolean visitVariable(VariableTree tree, Void unused) { return true; } /* An empty statement can complete normally iff it is reachable. */ @Override public Boolean visitEmptyStatement(EmptyStatementTree tree, Void unused) { return true; } @Override public Boolean visitLabeledStatement(LabeledStatementTree tree, Void unused) { // break/continue targets have already been resolved by javac, so // there's nothing to do here return scan(tree.getStatement()); } /* An expression statement can complete normally iff it is reachable. */ @Override public Boolean visitExpressionStatement(ExpressionStatementTree tree, Void unused) { if (isSystemExit(tree.getExpression())) { // The spec doesn't have any special handling for {@code System.exit}, but in practice it // cannot complete normally return false; } return true; } private boolean isSystemExit(ExpressionTree expression) { if (!(expression instanceof MethodInvocationTree)) { return false; } MethodSymbol sym = getSymbol((MethodInvocationTree) expression); if (sym == null) { return false; } return sym.owner.getQualifiedName().contentEquals("java.lang.System") && sym.getSimpleName().contentEquals("exit"); } /* * An if-then statement can complete normally iff it is reachable. * * The then-statement is reachable iff the if-then statement is reachable. * An if-then-else statement can complete normally iff the then-statement * can complete normally or the else-statement can complete normally. * * The then-statement is reachable iff the if-then-else statement is * reachable. * * The else-statement is reachable iff the if-then-else statement is * reachable. */ @Override public Boolean visitIf(IfTree tree, Void unused) { boolean thenCompletes = scan(tree.getThenStatement()); boolean elseCompletes = tree.getElseStatement() == null || scan(tree.getElseStatement()); return thenCompletes || elseCompletes; } /* An assert statement can complete normally iff it is reachable. */ @Override public Boolean visitAssert(AssertTree tree, Void unused) { return true; } /* * A switch statement can complete normally iff at least one of the * following is true: * * 1) The switch block is empty or contains only switch labels. * 2) The last statement in the switch block can complete normally. * 3) There is at least one switch label after the last switch block * statement group. * 4) The switch block does not contain a default label. * 5) There is a reachable break statement that exits the switch statement. * * A switch block is reachable iff its switch statement is reachable. * * A statement in a switch block is reachable iff its switch statement is * reachable and at least one of the following is true: * * - It bears a case or default label. * * - There is a statement preceding it in the switch block and that * preceding statement can complete normally. */ @Override public Boolean visitSwitch(SwitchTree tree, Void unused) { // (1) if (tree.getCases().stream().allMatch(c -> c.getStatements().isEmpty())) { return true; } // (2) boolean lastCompletes = true; for (CaseTree c : tree.getCases()) { lastCompletes = scan(c.getStatements()); } if (lastCompletes) { return true; } // (3) if (getLast(tree.getCases()).getStatements().isEmpty()) { return true; } // (4) if (tree.getCases().stream().noneMatch(c -> c.getExpression() == null)) { return true; } // (5) if (breaks.contains(tree)) { return true; } return false; } /* * A while statement can complete normally iff at least one of the * following is true: * * 1) The while statement is reachable and the condition expression is not * a constant expression (§15.28) with value true. * 2) There is a reachable break statement that exits the while statement. * * The contained statement is reachable iff the while statement is * reachable and the condition expression is not a constant expression * whose value is false. */ @Override public Boolean visitWhileLoop(WhileLoopTree tree, Void unused) { Boolean condValue = ASTHelpers.constValue(tree.getCondition(), Boolean.class); if (!Objects.equals(condValue, false)) { scan(tree.getStatement()); } // (1) if (!Objects.equals(condValue, true)) { return true; } // (2) if (breaks.contains(tree)) { return true; } return false; } /* * A do statement can complete normally iff at least one of the following * is true: * * 1) The contained statement can complete normally and the condition * expression is not a constant expression (§15.28) with value true. * * 2) The do statement contains a reachable continue statement with no * label, and the do statement is the innermost while, do, or for * statement that contains that continue statement, and the continue * statement continues that do statement, and the condition expression * is not a constant expression with value true. * * 3) The do statement contains a reachable continue statement with a * label L, and the do statement has label L, and the continue * statement continues that do statement, and the condition expression * is not a constant expression with value true. * * 4) There is a reachable break statement that exits the do statement. * * The contained statement is reachable iff the do statement is reachable. */ @Override public Boolean visitDoWhileLoop(DoWhileLoopTree that, Void unused) { boolean completes = scan(that.getStatement()); boolean conditionIsAlwaysTrue = firstNonNull(ASTHelpers.constValue(that.getCondition(), Boolean.class), false); // (1) if (completes && !conditionIsAlwaysTrue) { return true; } // (2) or (3) if (continues.contains(that) && !conditionIsAlwaysTrue) { return true; } // (4) if (breaks.contains(that)) { return true; } return false; } /* * A basic for statement can complete normally iff at least one of the * following is true: * * 1) The for statement is reachable, there is a condition expression, and * the condition expression is not a constant expression (§15.28) with * value true. * * 2) There is a reachable break statement that exits the for statement. * * The contained statement is reachable iff the for statement is reachable * and the condition expression is not a constant expression whose value is * false. */ @Override public Boolean visitForLoop(ForLoopTree that, Void unused) { Boolean condValue = ASTHelpers.constValue(that.getCondition(), Boolean.class); if (!Objects.equals(condValue, false)) { scan(that.getStatement()); } // (1) if (that.getCondition() != null && !Objects.equals(condValue, true)) { return true; } // (2) if (breaks.contains(that)) { return true; } return false; } /* An enhanced for statement can complete normally iff it is reachable. */ @Override public Boolean visitEnhancedForLoop(EnhancedForLoopTree that, Void unused) { scan(that.getStatement()); return true; } /* A return statement cannot complete normally. */ @Override public Boolean visitReturn(ReturnTree tree, Void unused) { return false; } /* A throw statement cannot complete normally. */ @Override public Boolean visitThrow(ThrowTree tree, Void unused) { return false; } /* * A synchronized statement can complete normally iff the contained * statement can complete normally. * * The contained statement is reachable iff the synchronized statement * is reachable. */ @Override public Boolean visitSynchronized(SynchronizedTree tree, Void unused) { return scan(tree.getBlock()); } /* * A try statement can complete normally iff both of the following are true: * * 1) The try block can complete normally or any catch block can complete * normally. * * 2) If the try statement has a finally block, then the finally block can * complete normally. */ @Override public Boolean visitTry(TryTree that, Void unused) { boolean completes = scan(that.getBlock()); // assume all catch blocks are reachable; javac has already rejected unreachable // checked exception handlers for (CatchTree catchTree : that.getCatches()) { completes |= scan(catchTree.getBlock()); } if (that.getFinallyBlock() != null && !scan(that.getFinallyBlock())) { completes = false; } return completes; } } }