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

org.checkerframework.dataflow.util.PurityChecker Maven / Gradle / Ivy

Go to download

dataflow-shaded is a dataflow framework based on the javac compiler. It differs from the org.checkerframework:dataflow artifact in two ways. First, the packages in this artifact have been renamed to org.checkerframework.shaded.*. Second, unlike the dataflow artifact, this artifact contains the dependencies it requires.

There is a newer version: 3.47.0
Show newest version
package org.checkerframework.dataflow.util;

import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.UnaryTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import org.checkerframework.dataflow.qual.Deterministic;
import org.checkerframework.dataflow.qual.Pure;
import org.checkerframework.dataflow.qual.SideEffectFree;
import org.checkerframework.javacutil.AnnotationProvider;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreePathUtil;
import org.checkerframework.javacutil.TreeUtils;
import org.plumelib.util.IPair;

/**
 * A visitor that determines the purity (as defined by {@link
 * org.checkerframework.dataflow.qual.SideEffectFree}, {@link
 * org.checkerframework.dataflow.qual.Deterministic}, and {@link
 * org.checkerframework.dataflow.qual.Pure}) of a statement or expression. The entry point is method
 * {@link #checkPurity}.
 *
 * @see SideEffectFree
 * @see Deterministic
 * @see Pure
 */
public class PurityChecker {

  /**
   * Compute whether the given statement is side-effect-free, deterministic, or both. Returns a
   * result that can be queried.
   *
   * @param statement the statement to check
   * @param annoProvider the annotation provider
   * @param assumeSideEffectFree true if all methods should be assumed to be @SideEffectFree
   * @param assumeDeterministic true if all methods should be assumed to be @Deterministic
   * @param assumePureGetters true if all getter methods should be assumed to be @Pure
   * @return information about whether the given statement is side-effect-free, deterministic, or
   *     both
   */
  public static PurityResult checkPurity(
      TreePath statement,
      AnnotationProvider annoProvider,
      boolean assumeSideEffectFree,
      boolean assumeDeterministic,
      boolean assumePureGetters) {
    PurityCheckerHelper helper =
        new PurityCheckerHelper(
            annoProvider, assumeSideEffectFree, assumeDeterministic, assumePureGetters);
    helper.scan(statement, null);
    return helper.purityResult;
  }

  /**
   * Result of the {@link PurityChecker}. Can be queried regarding whether a given tree was
   * side-effect-free, deterministic, or both; also gives reasons if the answer is "no".
   */
  public static class PurityResult {

    /** Reasons that the referenced method is not side-effect-free. */
    protected final List> notSEFreeReasons = new ArrayList<>(1);

    /** Reasons that the referenced method is not deterministic. */
    protected final List> notDetReasons = new ArrayList<>(1);

    /** Reasons that the referenced method is not side-effect-free and deterministic. */
    protected final List> notBothReasons = new ArrayList<>(1);

    /**
     * Contains all the varieties of purity that the expression has. Starts out with all varieties,
     * and elements are removed from it as violations are found.
     */
    protected EnumSet kinds = EnumSet.allOf(Pure.Kind.class);

    /**
     * Return the kinds of purity that the method has.
     *
     * @return the kinds of purity that the method has
     */
    public EnumSet getKinds() {
      return kinds;
    }

    /**
     * Is the method pure w.r.t. a given set of kinds?
     *
     * @param otherKinds the varieties of purity to check
     * @return true if the method is pure with respect to all the given kinds
     */
    public boolean isPure(EnumSet otherKinds) {
      return kinds.containsAll(otherKinds);
    }

    /**
     * Get the reasons why the method is not side-effect-free.
     *
     * @return the reasons why the method is not side-effect-free
     */
    public List> getNotSEFreeReasons() {
      return notSEFreeReasons;
    }

    /**
     * Add a reason why the method is not side-effect-free.
     *
     * @param t a tree
     * @param msgId why the tree is not side-effect-free
     */
    public void addNotSEFreeReason(Tree t, String msgId) {
      notSEFreeReasons.add(IPair.of(t, msgId));
      kinds.remove(Pure.Kind.SIDE_EFFECT_FREE);
    }

    /**
     * Get the reasons why the method is not deterministic.
     *
     * @return the reasons why the method is not deterministic
     */
    public List> getNotDetReasons() {
      return notDetReasons;
    }

    /**
     * Add a reason why the method is not deterministic.
     *
     * @param t a tree
     * @param msgId why the tree is not deterministic
     */
    public void addNotDetReason(Tree t, String msgId) {
      notDetReasons.add(IPair.of(t, msgId));
      kinds.remove(Pure.Kind.DETERMINISTIC);
    }

    /**
     * Get the reasons why the method is not both side-effect-free and deterministic.
     *
     * @return the reasons why the method is not both side-effect-free and deterministic
     */
    public List> getNotBothReasons() {
      return notBothReasons;
    }

    /**
     * Add a reason why the method is not both side-effect-free and deterministic.
     *
     * @param t tree
     * @param msgId why the tree is not deterministic and side-effect-free
     */
    public void addNotBothReason(Tree t, String msgId) {
      notBothReasons.add(IPair.of(t, msgId));
      kinds.remove(Pure.Kind.DETERMINISTIC);
      kinds.remove(Pure.Kind.SIDE_EFFECT_FREE);
    }

    @Override
    public String toString() {
      return String.join(
          System.lineSeparator(),
          "PurityResult{",
          "  notSEF: " + notSEFreeReasons,
          "  notDet: " + notDetReasons,
          "  notBoth: " + notBothReasons,
          "}");
    }
  }

  // TODO: It would be possible to improve efficiency by visiting fewer nodes.  This would require
  // overriding more visit* methods.  I'm not sure whether such an optimization would be worth it.

  /**
   * Helper class to keep {@link PurityChecker}'s interface clean.
   *
   * 

The scanner is run on a single statement, not on a class or method. */ protected static class PurityCheckerHelper extends TreePathScanner { /** The purity result. */ PurityResult purityResult = new PurityResult(); /** The annotation provider (typically an AnnotatedTypeFactory). */ protected final AnnotationProvider annoProvider; /** * True if all methods should be assumed to be @SideEffectFree, for the purposes of * org.checkerframework.dataflow analysis. */ private final boolean assumeSideEffectFree; /** * True if all methods should be assumed to be @Deterministic, for the purposes of * org.checkerframework.dataflow analysis. */ private final boolean assumeDeterministic; /** * True if all getter methods should be assumed to be @SideEffectFree and @Deterministic, for * the purposes of org.checkerframework.dataflow analysis. */ private final boolean assumePureGetters; /** * Create a PurityCheckerHelper. * * @param annoProvider the annotation provider * @param assumeSideEffectFree true if all methods should be assumed to be @SideEffectFree * @param assumeDeterministic true if all methods should be assumed to be @Deterministic * @param assumePureGetters true if getter methods should be assumed to be @Pure */ public PurityCheckerHelper( AnnotationProvider annoProvider, boolean assumeSideEffectFree, boolean assumeDeterministic, boolean assumePureGetters) { this.annoProvider = annoProvider; this.assumeSideEffectFree = assumeSideEffectFree; this.assumeDeterministic = assumeDeterministic; this.assumePureGetters = assumePureGetters; } @Override public Void visitCatch(CatchTree tree, Void ignore) { purityResult.addNotDetReason(tree, "catch"); return super.visitCatch(tree, ignore); } /** Represents a method that is both deterministic and side-effect free. */ private static final EnumSet detAndSeFree = EnumSet.of(Pure.Kind.DETERMINISTIC, Pure.Kind.SIDE_EFFECT_FREE); @Override public Void visitMethodInvocation(MethodInvocationTree tree, Void ignore) { ExecutableElement elt = TreeUtils.elementFromUse(tree); if (!PurityUtils.hasPurityAnnotation(annoProvider, elt)) { purityResult.addNotBothReason(tree, "call"); } else { EnumSet purityKinds = ((assumeDeterministic && assumeSideEffectFree) || (assumePureGetters && ElementUtils.isGetter(elt))) // Avoid computation if not necessary ? detAndSeFree : PurityUtils.getPurityKinds(annoProvider, elt); boolean det = assumeDeterministic || purityKinds.contains(Pure.Kind.DETERMINISTIC); boolean seFree = assumeSideEffectFree || purityKinds.contains(Pure.Kind.SIDE_EFFECT_FREE); if (!det && !seFree) { purityResult.addNotBothReason(tree, "call"); } else if (!det) { purityResult.addNotDetReason(tree, "call"); } else if (!seFree) { purityResult.addNotSEFreeReason(tree, "call"); } } return super.visitMethodInvocation(tree, ignore); } @Override public Void visitNewClass(NewClassTree tree, Void ignore) { // Ordinarily, "new MyClass()" is forbidden. It is permitted, however, when it is the // expression in "throw EXPR;". (In the future, more expressions could be permitted.) // // The expression in "throw EXPR;" is allowed to be non-@Deterministic, so long as it is // not within a catch block that could catch an exception that the statement throws. // For example, EXPR can be object creation (a "new" expression) or can call a // non-deterministic method. // // Coarse rule (currently implemented): // * permit only "throw new SomeExpression(args)", where the constructor is // @SideEffectFree and the args are pure, and forbid all enclosing try statements // that have a catch clause. // More precise rule: // * permit other non-deterministic expresssions within throw (at which time move this // logic to visitThrow()). // * the only bad try statements are those with a catch block that is: // * unchecked exceptions // * checked = Exception or lower, but excluding RuntimeException and its // subclasses // * super- or sub-classes of the type of _expr_ // * if _expr_ is exactly "new SomeException", this can be changed to just // "superclasses of SomeException". // * super- or sub-classes of exceptions declared to be thrown by any component of // _expr_. // * need to check every containing try statement, not just the nearest enclosing // one. // Object creation is usually prohibited, but permit "throw new SomeException();" if it // is not contained within any try statement that has a catch clause. (There is no need // to check the latter condition, because the Purity Checker forbids all catch // statements.) Tree parent = getCurrentPath().getParentPath().getLeaf(); boolean okThrowDeterministic = parent.getKind() == Tree.Kind.THROW; ExecutableElement ctorElement = TreeUtils.elementFromUse(tree); boolean deterministic = assumeDeterministic || okThrowDeterministic // No need to check assumePureGetters because a constructor is never a // getter. || PurityUtils.isDeterministic(annoProvider, ctorElement); boolean sideEffectFree = assumeSideEffectFree || PurityUtils.isSideEffectFree(annoProvider, ctorElement); // This does not use "addNotBothReason" because the reasons are different: one is // because the constructor is called at all, and the other is because the constuctor is // not side-effect-free. if (!deterministic) { purityResult.addNotDetReason(tree, "object.creation"); } if (!sideEffectFree) { purityResult.addNotSEFreeReason(tree, "call"); } // TODO: if okThrowDeterministic, permit arguments to the newClass to be // non-deterministic (don't add those to purityResult), but still don't permit them to // have side effects. This should probably wait until a rewrite of the Purity Checker. return super.visitNewClass(tree, ignore); } @Override public Void visitAssignment(AssignmentTree tree, Void ignore) { ExpressionTree variable = tree.getVariable(); assignmentCheck(variable); return super.visitAssignment(tree, ignore); } @Override public Void visitUnary(UnaryTree tree, Void ignore) { switch (tree.getKind()) { case POSTFIX_DECREMENT: case POSTFIX_INCREMENT: case PREFIX_DECREMENT: case PREFIX_INCREMENT: ExpressionTree expression = tree.getExpression(); assignmentCheck(expression); break; default: // Nothing to do break; } return super.visitUnary(tree, ignore); } /** * Check whether {@code variable} is permitted on the left-hand-side of an assignment. * * @param variable the lhs to check */ protected void assignmentCheck(ExpressionTree variable) { variable = TreeUtils.withoutParens(variable); VariableElement fieldElt = TreeUtils.asFieldAccess(variable); if (fieldElt != null && isFieldInCurrentClass(fieldElt) && TreePathUtil.inConstructor(getCurrentPath())) { // assigning a field in a constructor // TODO: add a check for ArrayAccessTree too. return; } if (TreeUtils.isFieldAccess(variable)) { // lhs is a field access purityResult.addNotBothReason(variable, "assign.field"); } else if (variable instanceof ArrayAccessTree) { // lhs is array access purityResult.addNotBothReason(variable, "assign.array"); } else { // lhs is a local variable assert isLocalVariable(variable); } } /** * Returns true if the given field is defined by the current class. * * @param fieldElt a field * @return true if the given field is defined by the current class */ private boolean isFieldInCurrentClass(VariableElement fieldElt) { ClassTree currentTypeTree = TreePathUtil.enclosingClass(getCurrentPath()); assert currentTypeTree != null : "@AssumeAssertion(nullness)"; TypeElement currentType = TreeUtils.elementFromDeclaration(currentTypeTree); assert currentType != null : "@AssumeAssertion(nullness)"; TypeElement definesField = ElementUtils.enclosingTypeElement(fieldElt); assert definesField != null : "@AssumeAssertion(nullness)"; return currentType.equals(definesField); } /** * Checks if the argument is a local variable. * * @param variable the tree to check * @return true if the argument is a local variable */ protected boolean isLocalVariable(ExpressionTree variable) { return variable instanceof IdentifierTree && !TreeUtils.isFieldAccess(variable); } @Override public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void ignore) { ExpressionTree variable = tree.getVariable(); assignmentCheck(variable); return super.visitCompoundAssignment(tree, ignore); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy