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

org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer Maven / Gradle / Ivy

Go to download

The Checker Framework enhances Java's type system to make it more powerful and useful. This lets software developers detect and prevent errors in their Java programs. The Checker Framework includes compiler plug-ins ("checkers") that find bugs or verify their absence. It also permits you to write your own compiler plug-ins.

There is a newer version: 3.48.2
Show newest version
package org.checkerframework.checker.resourceleak;

import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.calledmethods.qual.CalledMethods;
import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression;
import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory;
import org.checkerframework.checker.mustcall.MustCallChecker;
import org.checkerframework.checker.mustcall.qual.MustCall;
import org.checkerframework.checker.mustcall.qual.MustCallAlias;
import org.checkerframework.checker.mustcall.qual.NotOwning;
import org.checkerframework.checker.mustcall.qual.Owning;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.accumulation.AccumulationStore;
import org.checkerframework.common.accumulation.AccumulationValue;
import org.checkerframework.dataflow.cfg.ControlFlowGraph;
import org.checkerframework.dataflow.cfg.UnderlyingAST;
import org.checkerframework.dataflow.cfg.UnderlyingAST.Kind;
import org.checkerframework.dataflow.cfg.block.Block;
import org.checkerframework.dataflow.cfg.block.Block.BlockType;
import org.checkerframework.dataflow.cfg.block.ExceptionBlock;
import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock;
import org.checkerframework.dataflow.cfg.node.AssignmentNode;
import org.checkerframework.dataflow.cfg.node.ClassNameNode;
import org.checkerframework.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.dataflow.cfg.node.LocalVariableNode;
import org.checkerframework.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.dataflow.cfg.node.NullLiteralNode;
import org.checkerframework.dataflow.cfg.node.ObjectCreationNode;
import org.checkerframework.dataflow.cfg.node.ReturnNode;
import org.checkerframework.dataflow.cfg.node.SuperNode;
import org.checkerframework.dataflow.cfg.node.ThisNode;
import org.checkerframework.dataflow.cfg.node.TypeCastNode;
import org.checkerframework.dataflow.expression.FieldAccess;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.dataflow.expression.LocalVariable;
import org.checkerframework.dataflow.expression.ThisReference;
import org.checkerframework.dataflow.util.NodeUtils;
import org.checkerframework.framework.flow.CFStore;
import org.checkerframework.framework.flow.CFValue;
import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException;
import org.checkerframework.framework.util.StringToJavaExpression;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreePathUtil;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypeSystemError;
import org.checkerframework.javacutil.TypesUtils;
import org.plumelib.util.IPair;

/**
 * An analyzer that checks consistency of {@link MustCall} and {@link CalledMethods} types, thereby
 * detecting resource leaks. For any expression e the analyzer ensures that when e
 * goes out of scope, there exists a resource alias r of e (which might be
 * e itself) such that the must-call methods of r (i.e. the values of r's
 * MustCall type) are contained in the value of r's CalledMethods type. For any e
 * for which this property does not hold, the analyzer reports a {@code
 * "required.method.not.called"} error, indicating a possible resource leak.
 *
 * 

Mechanically, the analysis does two tasks. * *

    *
  • Tracks must-aliases, implemented via a dataflow analysis. Each dataflow fact is a set of * resource-aliases that refer to the same resource. Furthermore, that resource is owned. No * dataflow facts are maintained for a non-owned resource. *
  • When the last resource alias in a resource-alias set goes out-of-scope, it checks their * must-call and called-methods types. The analysis does not track must-call or called-methods * types, but queries other checkers to obtain them. *
* *

Class {@link Obligation} represents a single such dataflow fact. Abstractly, each dataflow * fact is a pair: a set of resource aliases to some resource, and the must-call obligations of that * resource (i.e the list of must-call methods that need to be called on one of the resource * aliases). Concretely, the Must Call Checker is responsible for tracking the latter - an * expression's must-call type indicates which methods must be called - so this dataflow analysis * only actually tracks the sets of resource aliases. * *

The dataflow algorithm adds, modifies, or removes dataflow facts when certain code patterns * are encountered, to account for ownership transfer. Here are non-exhaustive examples: * *

    *
  • A new fact is added to the tracked set when a constructor or a method with an owning return * is invoked. *
  • A fact is modified when an expression with a tracked Obligation is the RHS of a * (pseudo-)assignment. The LHS is added to the existing resource alias set. *
  • A fact can be removed when a member of a resource-alias set is assigned to an owning field * or passed to a method in a parameter location that is annotated as {@code @Owning}. *
* *

The dataflow analysis for these Obligations is conservative in that it guarantees that for * every resource which actually does have a must-call obligation, at least one Obligation will * exist. However, it does not guarantee the opposite: Obligations may also exist for resources * without a must-call obligation (or for non-resources) as a result of analysis imprecision. That * is, the set of Obligations tracked by the analysis over-approximates the actual set of resources * in the analyzed program with must-call obligations. * *

Throughout, this class uses the temporary-variable facilities provided by the Must Call and * Resource Leak type factories both to emulate a three-address-form IR (simplifying some analysis * logic) and to permit expressions to have their types refined in their respective checkers' * stores. These temporary variables can be members of resource-alias sets. Without temporary * variables, the checker wouldn't be able to verify code such as {@code new Socket(host, * port).close()}, which would cause false positives. Temporaries are created for {@code new} * expressions, method calls (for the return value), and ternary expressions. Other types of * expressions may be supported in the future. */ /*package-private*/ class MustCallConsistencyAnalyzer { /** True if errors related to static owning fields should be suppressed. */ private final boolean permitStaticOwning; /** True if errors related to field initialization should be suppressed. */ private final boolean permitInitializationLeak; /** * Aliases about which the checker has already reported about a resource leak, to avoid duplicate * reports. */ private final Set reportedErrorAliases = new HashSet<>(); /** * The type factory for the Resource Leak Checker, which is used to get called methods types and * to access the Must Call Checker. */ private final ResourceLeakAnnotatedTypeFactory typeFactory; /** * A cache for the result of calling {@code ResourceLeakAnnotatedTypeFactory.getStoreAfter()} on a * node. The cache prevents repeatedly computing least upper bounds on stores */ private final IdentityHashMap cmStoreAfter = new IdentityHashMap<>(); /** * A cache for the result of calling {@code MustCallAnnotatedTypeFactory.getStoreAfter()} on a * node. The cache prevents repeatedly computing least upper bounds on stores */ private final IdentityHashMap mcStoreAfter = new IdentityHashMap<>(); /** The Resource Leak Checker, used to issue errors. */ private final ResourceLeakChecker checker; /** The analysis from the Resource Leak Checker, used to get input stores based on CFG blocks. */ private final ResourceLeakAnalysis analysis; /** True if -AnoLightweightOwnership was passed on the command line. */ private final boolean noLightweightOwnership; /** True if -AcountMustCall was passed on the command line. */ private final boolean countMustCall; /** A description for how a method might exit. */ /*package-private*/ enum MethodExitKind { /** The method exits normally by returning. */ NORMAL_RETURN, /** The method exits by throwing an exception. */ EXCEPTIONAL_EXIT; /** An immutable set containing all possible ways for a method to exit. */ public static final Set ALL = ImmutableSet.copyOf(EnumSet.allOf(MethodExitKind.class)); } /** * An Obligation is a dataflow fact: a set of resource aliases and when those resources need to be * cleaned up. Abstractly, each Obligation represents a resource for which the analyzed program * might have a must-call obligation. Each Obligation is a pair of a set of resource aliases and * their must-call obligation. Must-call obligations are tracked by the {@link MustCallChecker} * and are accessed by looking up the type(s) in its type system of the resource aliases contained * in each {@code Obligation} using {@link #getMustCallMethods(ResourceLeakAnnotatedTypeFactory, * CFStore)}. * *

An Obligation might not matter on all paths out of a method. For instance, after a * constructor assigns a resource to an {@link Owning} field, the resource only needs to be closed * if the constructor throws an exception. If the constructor exits normally then the obligation * is satisfied because the field is now responsible for its must-call obligations. See {@link * #whenToEnforce}, which defines when the Obligation needs to be enforced. * *

There is no guarantee that a given Obligation represents a resource with a real must-call * obligation. When the analysis can conclude that a given Obligation certainly does not represent * a real resource with a real must-call obligation (such as if the only resource alias is * certainly a null pointer, or if the must-call obligation is the empty set), the analysis can * discard the Obligation. */ /*package-private*/ static class Obligation { /** * The set of resource aliases through which a must-call obligation can be satisfied. Calling * the required method(s) in the must-call obligation through any of them satisfies the * must-call obligation: that is, if the called-methods type of any alias contains the required * method(s), then the must-call obligation is satisfied. See {@link #getMustCallMethods}. * *

{@code Obligation} is deeply immutable. If some code were to accidentally mutate a {@code * resourceAliases} set it could be really nasty to debug, so this set is always immutable. */ public final ImmutableSet resourceAliases; /** * The ways a method can exit along which this Obligation has to be enforced. For example, this * will usually be {@link MethodExitKind#ALL}, indicating that this Obligation has to be * enforced no matter how the method exits. It may also be a smaller set indicating that the * Obligation only has to be enforced on certain exit conditions. * *

If this set is empty then the Obligation can be dropped as it never needs to be enforced. */ public final ImmutableSet whenToEnforce; /** * Create an Obligation from a set of resource aliases. * * @param resourceAliases a set of resource aliases * @param whenToEnforce when this Obligation should be enforced */ public Obligation(Set resourceAliases, Set whenToEnforce) { this.resourceAliases = ImmutableSet.copyOf(resourceAliases); this.whenToEnforce = ImmutableSet.copyOf(whenToEnforce); } /** * Returns the resource alias in this Obligation's resource alias set corresponding to {@code * localVariableNode} if one is present. Otherwise, returns null. * * @param localVariableNode a local variable * @return the resource alias corresponding to {@code localVariableNode} if one is present; * otherwise, null */ private @Nullable ResourceAlias getResourceAlias(LocalVariableNode localVariableNode) { Element element = localVariableNode.getElement(); for (ResourceAlias alias : resourceAliases) { if (alias.reference instanceof LocalVariable && alias.element.equals(element)) { return alias; } } return null; } /** * Returns the resource alias in this Obligation's resource alias set corresponding to {@code * expression} if one is present. Otherwise, returns null. * * @param expression a Java expression * @return the resource alias corresponding to {@code expression} if one is present; otherwise, * null */ private @Nullable ResourceAlias getResourceAlias(JavaExpression expression) { for (ResourceAlias alias : resourceAliases) { if (alias.reference.equals(expression)) { return alias; } } return null; } /** * Returns true if this contains a resource alias corresponding to {@code localVariableNode}, * meaning that calling the required methods on {@code localVariableNode} is sufficient to * satisfy the must-call obligation this object represents. * * @param localVariableNode a local variable node * @return true if a resource alias corresponding to {@code localVariableNode} is present */ private boolean canBeSatisfiedThrough(LocalVariableNode localVariableNode) { return getResourceAlias(localVariableNode) != null; } /** * Does this Obligation contain any resource aliases that were derived from {@link * MustCallAlias} parameters? * * @return the logical or of the {@link ResourceAlias#derivedFromMustCallAliasParam} fields of * this Obligation's resource aliases */ public boolean derivedFromMustCallAlias() { for (ResourceAlias ra : resourceAliases) { if (ra.derivedFromMustCallAliasParam) { return true; } } return false; } /** * Gets the must-call methods (i.e. the list of methods that must be called to satisfy the * must-call obligation) of each resource alias represented by this Obligation. * * @param rlAtf a Resource Leak Annotated Type Factory * @param mcStore a CFStore produced by the MustCall checker's dataflow analysis. If this is * null, then the default MustCall type of each variable's class will be used. * @return a map from each resource alias of this to a list of its must-call method names, or * null if the must-call obligations are unsatisfiable (i.e. the value of some tracked * resource alias of this in the Must Call store is MustCallUnknown) */ public @Nullable Map> getMustCallMethods( ResourceLeakAnnotatedTypeFactory rlAtf, @Nullable CFStore mcStore) { Map> result = new HashMap<>(this.resourceAliases.size()); MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = rlAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); for (ResourceAlias alias : this.resourceAliases) { AnnotationMirror mcAnno = getMustCallValue(alias, mcStore, mustCallAnnotatedTypeFactory); if (!AnnotationUtils.areSameByName(mcAnno, MustCall.class.getCanonicalName())) { // MustCallUnknown; cannot be satisfied return null; } List annoVals = rlAtf.getMustCallValues(mcAnno); // Really, annoVals should never be empty here; we should not have created the obligation in // the first place // TODO: add an assertion that annoVals is non-empty and address any failures result.put(alias, annoVals); } return result; } /** * Gets the must-call type associated with the given resource alias, falling on back on the * declared type if there is no refined type for the alias in the store. * * @param alias a resource alias * @param mcStore the must-call checker's store * @param mcAtf the must-call checker's annotated type factory * @return the annotation from the must-call type hierarchy associated with {@code alias} */ private static AnnotationMirror getMustCallValue( ResourceAlias alias, @Nullable CFStore mcStore, MustCallAnnotatedTypeFactory mcAtf) { JavaExpression reference = alias.reference; CFValue value = mcStore == null ? null : mcStore.getValue(reference); if (value != null) { AnnotationMirror result = AnnotationUtils.getAnnotationByClass(value.getAnnotations(), MustCall.class); if (result != null) { return result; } } AnnotationMirror result = mcAtf.getAnnotatedType(alias.element).getEffectiveAnnotationInHierarchy(mcAtf.TOP); if (result != null && !AnnotationUtils.areSame(result, mcAtf.TOP)) { return result; } // There wasn't an @MustCall annotation for it in the store and the type factory has no // information, so fall back to the default must-call type for the class. // TODO: we currently end up in this case when checking a call to the return type // of a returns-receiver method on something with a MustCall type; for example, // see tests/socket/ZookeeperReport6.java. We should instead use a poly type if we can. TypeElement typeElt = TypesUtils.getTypeElement(reference.getType()); if (typeElt == null) { // typeElt is null if reference.getType() was not a class, interface, annotation // type, or enum -- that is, was not an annotatable type. // That happens rarely, such as when it is a wildcard type. In these cases, fall // back on a safe default: top. return mcAtf.TOP; } if (typeElt.asType().getKind() == TypeKind.VOID) { // Void types can't have methods called on them, so returning bottom is safe. return mcAtf.BOTTOM; } return mcAtf.getAnnotatedType(typeElt).getPrimaryAnnotationInHierarchy(mcAtf.TOP); } @Override public String toString() { return "Obligation: resourceAliases=" + Iterables.toString(resourceAliases) + ", whenToEnforce=" + whenToEnforce; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Obligation that = (Obligation) obj; return this.resourceAliases.equals(that.resourceAliases) && this.whenToEnforce.equals(that.whenToEnforce); } @Override public int hashCode() { return Objects.hash(resourceAliases, whenToEnforce); } } // Is there a different Obligation on every line of the program, or is Obligation mutable? // (Or maybe Obligation is abstractly mutable when you consider the @MustCall types that are not // recorded in Obligation's representation.) Could you clarify? I found the first paragraph // confusing, including "correspond to". /** * A resource alias is a reference through which a must-call obligation can be satisfied. Any * must-call obligation might be satisfiable through one or more resource aliases. An {@link * Obligation} tracks one set of resource aliases that correspond to one must-call obligation in * the program. * *

A resource alias is always owning; non-owning aliases are, by definition, not tracked. * *

Internally, a resource alias is represented by a pair of a {@link JavaExpression} (the * "reference" through which the must-call obligations for the alias set to which it belongs can * be satisfied) and a tree that "assigns" the reference. */ /*package-private*/ static class ResourceAlias { /** An expression from the source code or a temporary variable for an expression. */ public final JavaExpression reference; /** The element for {@link #reference}. */ public final Element element; /** The tree at which {@code reference} was assigned, for the purpose of error reporting. */ public final Tree tree; /** * Was this ResourceAlias derived from a parameter to a method that was annotated as {@link * MustCallAlias}? If so, the obligation containing this resource alias must be discharged only * in one of the following ways: * *

    *
  • it is passed to another method or constructor in an @MustCallAlias position, and then * the containing method returns that method’s result, or the call is a super() * constructor call annotated with {@link MustCallAlias}, or *
  • it is stored in an owning field of the class under analysis *
*/ public final boolean derivedFromMustCallAliasParam; /** * Create a new resource alias. This constructor should only be used if the resource alias was * not derived from a method parameter annotated as {@link MustCallAlias}. * * @param reference the local variable * @param tree the tree */ public ResourceAlias(LocalVariable reference, Tree tree) { this(reference, reference.getElement(), tree); } /** * Create a new resource alias. This constructor should only be used if the resource alias was * not derived from a method parameter annotated as {@link MustCallAlias}. * * @param reference the reference * @param element the element for the given reference * @param tree the tree */ public ResourceAlias(JavaExpression reference, Element element, Tree tree) { this(reference, element, tree, false); } /** * Create a new resource alias. * * @param reference the local variable * @param element the element for the reference * @param tree the tree * @param derivedFromMustCallAliasParam true iff this resource alias was created because of an * {@link MustCallAlias} parameter */ public ResourceAlias( JavaExpression reference, Element element, Tree tree, boolean derivedFromMustCallAliasParam) { this.reference = reference; this.element = element; this.tree = tree; this.derivedFromMustCallAliasParam = derivedFromMustCallAliasParam; } @Override public String toString() { return "(ResourceAlias: reference: " + reference + " |||| tree: " + tree + ")"; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ResourceAlias that = (ResourceAlias) o; return reference.equals(that.reference) && tree.equals(that.tree); } @Override public int hashCode() { return Objects.hash(reference, tree); } /** * Returns an appropriate String for representing this in an error message. In particular, if * {@link #reference} is a temporary variable, we return the String representation of {@link * #tree}, to avoid exposing the temporary name (which has no meaning for the user) in the error * message * * @return an appropriate String for representing this in an error message */ public String stringForErrorMessage() { String referenceStr = reference.toString(); // we assume that any temporary variable name will not be a syntactically-valid identifier // or keyword return !SourceVersion.isIdentifier(referenceStr) ? tree.toString() : referenceStr; } } /** * Creates a consistency analyzer. Typically, the type factory's postAnalyze method would * instantiate a new consistency analyzer using this constructor and then call {@link * #analyze(ControlFlowGraph)}. * * @param typeFactory the type factory * @param analysis the analysis from the type factory. Usually this would have protected access, * so this constructor cannot get it directly. */ /*package-private*/ MustCallConsistencyAnalyzer( ResourceLeakAnnotatedTypeFactory typeFactory, ResourceLeakAnalysis analysis) { this.typeFactory = typeFactory; this.checker = (ResourceLeakChecker) typeFactory.getChecker(); this.analysis = analysis; this.permitStaticOwning = checker.hasOption("permitStaticOwning"); this.permitInitializationLeak = checker.hasOption("permitInitializationLeak"); this.noLightweightOwnership = checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP); this.countMustCall = checker.hasOption(ResourceLeakChecker.COUNT_MUST_CALL); } /** * The main function of the consistency dataflow analysis. The analysis tracks dataflow facts * ("Obligations") of type {@link Obligation}, each representing a set of owning resource aliases * for some value with a non-empty {@code @MustCall} obligation. The set of tracked Obligations is * guaranteed to include at least one Obligation for each actual resource in the program, but * might include other, spurious Obligations, too (that is, it is a conservative * over-approximation of the true Obligation set). * *

The analysis improves its precision by removing Obligations from tracking when it can prove * that they do not represent real resources. For example, it is not necessary to track * expressions with empty {@code @MustCall} obligations, because they are trivially fulfilled. Nor * is tracking non-owning aliases necessary, because by definition they cannot be used to fulfill * must-call obligations. * * @param cfg the control flow graph of the method to check */ // TODO: This analysis is currently implemented directly using a worklist; in the future, it // should be rewritten to use the dataflow framework of the Checker Framework. /*package-private*/ void analyze(ControlFlowGraph cfg) { // The `visited` set contains everything that has been added to the worklist, even if it has // not yet been removed and analyzed. Set visited = new HashSet<>(); Deque worklist = new ArrayDeque<>(); // Add any owning parameters to the initial set of variables to track. BlockWithObligations entry = new BlockWithObligations(cfg.getEntryBlock(), computeOwningParameters(cfg)); worklist.add(entry); visited.add(entry); while (!worklist.isEmpty()) { BlockWithObligations current = worklist.remove(); propagateObligationsToSuccessorBlocks( cfg, current.obligations, current.block, visited, worklist); } } /** * Update a set of Obligations to account for a method or constructor invocation. * * @param obligations the Obligations to update * @param node the method or constructor invocation * @param exceptionType a description of the outgoing CFG edge from the node: null to * indicate normal return, or a {@link TypeMirror} to indicate a subclass of the given * throwable class was thrown */ private void updateObligationsForInvocation( Set obligations, Node node, @Nullable TypeMirror exceptionType) { removeObligationsAtOwnershipTransferToParameters(obligations, node, exceptionType); if (node instanceof MethodInvocationNode && typeFactory.canCreateObligations() && typeFactory.hasCreatesMustCallFor((MethodInvocationNode) node)) { checkCreatesMustCallForInvocation(obligations, (MethodInvocationNode) node); // Count calls to @CreatesMustCallFor methods as creating new resources. Doing so could // result in slightly over-counting, because @CreatesMustCallFor doesn't guarantee that // a new resource is created: it just means that a new resource might have been created. incrementNumMustCall(node); } if (!shouldTrackInvocationResult(obligations, node)) { return; } if (typeFactory.declaredTypeHasMustCall(node.getTree())) { // The incrementNumMustCall call above increments the count for the target of the // @CreatesMustCallFor annotation. By contrast, this call increments the count for the // return value of the method (which can't be the target of the annotation, because our // syntax doesn't support that). incrementNumMustCall(node); } updateObligationsWithInvocationResult(obligations, node); } /** * Checks that an invocation of a CreatesMustCallFor method is valid. * *

Such an invocation is valid if any of the conditions in {@link * #isValidCreatesMustCallForExpression(Set, JavaExpression, TreePath)} is true for each * expression in the argument to the CreatesMustCallFor annotation. As a special case, the * invocation of a CreatesMustCallFor method with "this" as its expression is permitted in the * constructor of the relevant class (invoking a constructor already creates an obligation). If * none of these conditions are true for any of the expressions, this method issues a * reset.not.owning error. * *

For soundness, this method also guarantees that if any of the expressions in the * CreatesMustCallFor annotation has a tracked Obligation, any tracked resource aliases of it will * be removed (lest the analysis conclude that it is already closed because one of these aliases * was closed before the method was invoked). Aliases created after the CreatesMustCallFor method * is invoked are still permitted. * * @param obligations the currently-tracked Obligations; this value is side-effected if there is * an Obligation in it which tracks any expression from the CreatesMustCallFor annotation as * one of its resource aliases * @param node a method invocation node, invoking a method with a CreatesMustCallFor annotation */ private void checkCreatesMustCallForInvocation( Set obligations, MethodInvocationNode node) { TreePath currentPath = typeFactory.getPath(node.getTree()); List cmcfExpressions = CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( node, typeFactory, typeFactory); List missing = new ArrayList<>(0); for (JavaExpression expression : cmcfExpressions) { if (!isValidCreatesMustCallForExpression(obligations, expression, currentPath)) { missing.add(expression); } } if (missing.isEmpty()) { // All expressions matched one of the rules, so the invocation is valid. return; } // Special case for invocations of CreatesMustCallFor("this") methods in the constructor. if (missing.size() == 1) { JavaExpression expression = missing.get(0); if (expression instanceof ThisReference && TreePathUtil.inConstructor(currentPath)) { return; } } StringJoiner missingStrs = new StringJoiner(","); for (JavaExpression m : missing) { String s = m.toString(); missingStrs.add(s.equals("this") ? s + " of type " + m.getType() : s); } checker.reportError( node.getTree(), "reset.not.owning", node.getTarget().getMethod().getSimpleName().toString(), missingStrs.toString()); } /** * Checks the validity of the given expression from an invoked method's {@link * org.checkerframework.checker.mustcall.qual.CreatesMustCallFor} annotation. Helper method for * {@link #checkCreatesMustCallForInvocation(Set, MethodInvocationNode)}. * *

An expression is valid if one of the following conditions is true: * *

    *
  • 1) the expression is an owning pointer, *
  • 2) the expression already has a tracked Obligation (i.e. there is already a resource * alias in some Obligation's resource alias set that refers to the expression), or *
  • 3) the method in which the invocation occurs also has an @CreatesMustCallFor annotation, * with the same expression. *
* * @param obligations the currently-tracked Obligations; this value is side-effected if there is * an Obligation in it which tracks {@code expression} as one of its resource aliases * @param expression an element of a method's @CreatesMustCallFor annotation * @param invocationPath the path to the invocation of the method from whose @CreateMustCallFor * annotation {@code expression} came * @return true iff the expression is valid, as defined above */ private boolean isValidCreatesMustCallForExpression( Set obligations, JavaExpression expression, TreePath invocationPath) { if (expression instanceof FieldAccess) { Element elt = ((FieldAccess) expression).getField(); if (!noLightweightOwnership && typeFactory.hasOwning(elt)) { // The expression is an Owning field. This satisfies case 1. return true; } } else if (expression instanceof LocalVariable) { Element elt = ((LocalVariable) expression).getElement(); if (!noLightweightOwnership && typeFactory.hasOwning(elt)) { // The expression is an Owning formal parameter. Note that this cannot actually // be a local variable (despite expressions's type being LocalVariable) because // the @Owning annotation can only be written on methods, parameters, and fields; // formal parameters are also represented by LocalVariable in the bodies of methods. // This satisfies case 1. return true; } else { Obligation toRemove = null; Obligation toAdd = null; for (Obligation obligation : obligations) { ResourceAlias alias = obligation.getResourceAlias(expression); if (alias != null) { // This satisfies case 2 above. Remove all its aliases, then return below. if (toRemove != null) { throw new TypeSystemError( "tried to remove multiple sets containing a reset expression at once"); } toRemove = obligation; toAdd = new Obligation(ImmutableSet.of(alias), obligation.whenToEnforce); } } if (toRemove != null) { obligations.remove(toRemove); obligations.add(toAdd); // This satisfies case 2. return true; } } } // TODO: Getting this every time is inefficient if a method has many @CreatesMustCallFor // annotations, but that should be rare. MethodTree callerMethodTree = TreePathUtil.enclosingMethod(invocationPath); if (callerMethodTree == null) { return false; } ExecutableElement callerMethodElt = TreeUtils.elementFromDeclaration(callerMethodTree); MustCallAnnotatedTypeFactory mcAtf = typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); List callerCmcfValues = ResourceLeakVisitor.getCreatesMustCallForValues(callerMethodElt, mcAtf, typeFactory); if (callerCmcfValues.isEmpty()) { return false; } for (String callerCmcfValue : callerCmcfValues) { JavaExpression callerTarget; try { callerTarget = StringToJavaExpression.atMethodBody(callerCmcfValue, callerMethodTree, checker); } catch (JavaExpressionParseException e) { // Do not issue an error here, because it would be a duplicate. // The error will be issued by the Transfer class of the checker, // via the CreatesMustCallForElementSupplier interface. callerTarget = null; } if (areSame(expression, callerTarget)) { // This satisfies case 3. return true; } } return false; } /** * Checks whether the two JavaExpressions are the same. This is identical to calling equals() on * one of them, with two exceptions: the second expression can be null, and {@code this} * references are compared using their underlying type. (ThisReference#equals always returns true, * which is probably a bug and isn't accurate in the case of nested classes.) * * @param target a JavaExpression * @param enclosingTarget another, possibly null, JavaExpression * @return true iff they represent the same program element */ private boolean areSame(JavaExpression target, @Nullable JavaExpression enclosingTarget) { if (enclosingTarget == null) { return false; } if (enclosingTarget instanceof ThisReference && target instanceof ThisReference) { return enclosingTarget.getType().toString().equals(target.getType().toString()); } else { return enclosingTarget.equals(target); } } /** * Given a node representing a method or constructor call, updates the set of Obligations to * account for the result, which is treated as a new resource alias. Adds the new resource alias * to the set of an Obligation in {@code obligations}: either an existing Obligation if the result * is definitely resource-aliased with it, or a new Obligation if not. * * @param obligations the currently-tracked Obligations. This is always side-effected: either a * new resource alias is added to the resource alias set of an existing Obligation, or a new * Obligation with a single-element resource alias set is created and added. * @param node the invocation node whose result is to be tracked; must be {@link * MethodInvocationNode} or {@link ObjectCreationNode} */ /*package-private*/ void updateObligationsWithInvocationResult( Set obligations, Node node) { Tree tree = node.getTree(); // Only track the result of the call if there is a temporary variable for the call node // (because if there is no temporary, then the invocation must produce an untrackable value, // such as a primitive type). LocalVariableNode tmpVar = typeFactory.getTempVarForNode(node); if (tmpVar == null) { return; } // `mustCallAliases` is a (possibly-empty) list of arguments passed in a MustCallAlias // position. List mustCallAliases = getMustCallAliasArgumentNodes(node); // If call returns @This, add the receiver to mustCallAliases. if (node instanceof MethodInvocationNode && typeFactory.returnsThis((MethodInvocationTree) tree)) { mustCallAliases.add( removeCastsAndGetTmpVarIfPresent( ((MethodInvocationNode) node).getTarget().getReceiver())); } if (mustCallAliases.isEmpty()) { // If mustCallAliases is an empty List, add tmpVarAsResourceAlias to a new set. ResourceAlias tmpVarAsResourceAlias = new ResourceAlias(new LocalVariable(tmpVar), tree); obligations.add(new Obligation(ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL)); } else { for (Node mustCallAlias : mustCallAliases) { if (mustCallAlias instanceof FieldAccessNode) { // Do not track the call result if the MustCallAlias argument is a field. // Handling of @Owning fields is a completely separate check, and there is never // a need to track an alias of a non-@Owning field, as by definition such a // field does not have must-call obligations! } else if (mustCallAlias instanceof LocalVariableNode) { // If mustCallAlias is a local variable already being tracked, add // tmpVarAsResourceAlias to the set containing mustCallAlias. Obligation obligationContainingMustCallAlias = getObligationForVar(obligations, (LocalVariableNode) mustCallAlias); if (obligationContainingMustCallAlias != null) { ResourceAlias tmpVarAsResourceAlias = new ResourceAlias( new LocalVariable(tmpVar), tmpVar.getElement(), tree, obligationContainingMustCallAlias.derivedFromMustCallAlias()); Set newResourceAliasSet = FluentIterable.from(obligationContainingMustCallAlias.resourceAliases) .append(tmpVarAsResourceAlias) .toSet(); obligations.remove(obligationContainingMustCallAlias); obligations.add( new Obligation( newResourceAliasSet, obligationContainingMustCallAlias.whenToEnforce)); // It is not an error if there is no Obligation containing the must-call // alias. In that case, what has usually happened is that no Obligation was // created in the first place. // For example, when checking the invocation of a "wrapper stream" // constructor, if the argument in the must-call alias position is some // stream with no must-call obligations like a ByteArrayInputStream, then no // Obligation object will have been created for it and therefore // obligationContainingMustCallAlias will be null. } } } } } /** * Returns true if the result of the given method or constructor invocation node should be tracked * in {@code obligations}. In some cases, there is no need to track the result because the * must-call obligations are already satisfied in some other way or there cannot possibly be * must-call obligations because of the structure of the code. * *

Specifically, an invocation result does NOT need to be tracked if any of the following is * true: * *

    *
  • The invocation is a call to a {@code this()} or {@code super()} constructor. *
  • The method's return type is annotated with MustCallAlias and the argument passed in this * invocation in the corresponding position is an owning field. *
  • The method's return type is non-owning, which can either be because the method has no * return type or because the return type is annotated with {@link NotOwning}. *
* *

This method can also side-effect {@code obligations}, if node is a super or this constructor * call with MustCallAlias annotations, by removing that Obligation. * * @param obligations the current set of Obligations, which may be side-effected * @param node the invocation node to check; must be {@link MethodInvocationNode} or {@link * ObjectCreationNode} * @return true iff the result of {@code node} should be tracked in {@code obligations} */ private boolean shouldTrackInvocationResult(Set obligations, Node node) { Tree callTree = node.getTree(); if (callTree.getKind() == Tree.Kind.NEW_CLASS) { // Constructor results from new expressions are tracked as long as the declared type has // a non-empty @MustCall annotation. NewClassTree newClassTree = (NewClassTree) callTree; ExecutableElement executableElement = TreeUtils.elementFromUse(newClassTree); TypeElement typeElt = TypesUtils.getTypeElement(ElementUtils.getType(executableElement)); return typeElt == null || !typeFactory.hasEmptyMustCallValue(typeElt) || !typeFactory.hasEmptyMustCallValue(newClassTree); } // Now callTree.getKind() == Tree.Kind.METHOD_INVOCATION. MethodInvocationTree methodInvokeTree = (MethodInvocationTree) callTree; if (TreeUtils.isSuperConstructorCall(methodInvokeTree) || TreeUtils.isThisConstructorCall(methodInvokeTree)) { List mustCallAliasArguments = getMustCallAliasArgumentNodes(node); // If there is a MustCallAlias argument that is also in the set of Obligations, then // remove it; its must-call obligation has been fulfilled by being passed on to the // MustCallAlias constructor (because a this/super constructor call can only occur in // the body of another constructor). for (Node mustCallAliasArgument : mustCallAliasArguments) { if (mustCallAliasArgument instanceof LocalVariableNode) { removeObligationsContainingVar(obligations, (LocalVariableNode) mustCallAliasArgument); } } return false; } return !returnTypeIsMustCallAliasWithUntrackable((MethodInvocationNode) node) && shouldTrackReturnType((MethodInvocationNode) node); } /** * Returns true if this node represents a method invocation of a must-call-alias method, where the * argument in the must-call-alias position is untrackable: an owning field or a pointer that is * guaranteed to be non-owning, such as {@code "this"} or a non-owning field. Owning fields are * handled by the rest of the checker, not by this algorithm, so they are "untrackable". * Non-owning fields and this nodes are guaranteed to be non-owning, and are therefore also * "untrackable". Because both owning and non-owning fields are untrackable (and there are no * other kinds of fields), this method returns true for all field accesses. * * @param node a method invocation node * @return true if this is the invocation of a method whose return type is MCA with an owning * field or a definitely non-owning pointer */ private boolean returnTypeIsMustCallAliasWithUntrackable(MethodInvocationNode node) { List mustCallAliasArguments = getMustCallAliasArgumentNodes(node); for (Node mustCallAliasArg : mustCallAliasArguments) { if (!(mustCallAliasArg instanceof FieldAccessNode || mustCallAliasArg instanceof ThisNode)) { return false; } } return !mustCallAliasArguments.isEmpty(); } /** * Checks if {@code node} is either directly enclosed by a {@link TypeCastNode}, by looking at the * successor block in the CFG. In this case the enclosing operator is a "no-op" that evaluates to * the same value as {@code node}. This method is only used within {@link * #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, Deque)} to ensure * Obligations are propagated to cast nodes properly. It relies on the assumption that a {@link * TypeCastNode} will only appear in a CFG as the first node in a block. * * @param node the CFG node * @return {@code true} if {@code node} is in a {@link SingleSuccessorBlock} {@code b}, the first * {@link Node} in {@code b}'s successor block is a {@link TypeCastNode}, and {@code node} is * an operand of the successor node; {@code false} otherwise */ private boolean inCast(Node node) { if (!(node.getBlock() instanceof SingleSuccessorBlock)) { return false; } Block successorBlock = ((SingleSuccessorBlock) node.getBlock()).getSuccessor(); if (successorBlock != null) { List succNodes = successorBlock.getNodes(); if (succNodes.size() > 0) { Node succNode = succNodes.get(0); if (succNode instanceof TypeCastNode) { return ((TypeCastNode) succNode).getOperand().equals(node); } } } return false; } /** * Transfer ownership of any locals passed as arguments to {@code @Owning} parameters at a method * or constructor call by removing the Obligations corresponding to those locals. * * @param obligations the current set of Obligations, which is side-effected to remove Obligations * for locals that are passed as owning parameters to the method or constructor * @param node a method or constructor invocation node * @param exceptionType a description of the outgoing CFG edge from the node: null to * indicate normal return, or a {@link TypeMirror} to indicate a subclass of the given * throwable class was thrown */ private void removeObligationsAtOwnershipTransferToParameters( Set obligations, Node node, @Nullable TypeMirror exceptionType) { if (exceptionType != null) { // Do not transfer ownership if the called method throws an exception. return; } if (noLightweightOwnership) { // Never transfer ownership to parameters, matching the default in the analysis built // into Eclipse. return; } List arguments = getArgumentsOfInvocation(node); List parameters = getParametersOfInvocation(node); if (arguments.size() != parameters.size()) { // This could happen, e.g., with varargs, or with strange cases like generated Enum // constructors. In the varargs case (i.e. if the varargs parameter is owning), // only the first of the varargs arguments will actually get transferred: the second // and later varargs arguments will continue to be tracked at the call-site. // For now, just skip this case - the worst that will happen is a false positive in // cases like the varargs one described above. // TODO allow for ownership transfer here if needed in future return; } for (int i = 0; i < arguments.size(); i++) { Node n = removeCastsAndGetTmpVarIfPresent(arguments.get(i)); if (n instanceof LocalVariableNode) { LocalVariableNode local = (LocalVariableNode) n; if (varTrackedInObligations(obligations, local)) { // check if parameter has an @Owning annotation VariableElement parameter = parameters.get(i); if (typeFactory.hasOwning(parameter)) { Obligation localObligation = getObligationForVar(obligations, local); // Passing to an owning parameter is not sufficient to resolve the // obligation created from a MustCallAlias parameter, because the // containing method must actually return the value. if (!localObligation.derivedFromMustCallAlias()) { // Transfer ownership! obligations.remove(localObligation); } } } } } } /** * If the return type of the enclosing method is {@code @Owning}, treat the must-call obligations * of the return expression as satisfied by removing all references to them from {@code * obligations}. * * @param obligations the current set of tracked Obligations. If ownership is transferred, it is * side-effected to remove any Obligations that are resource-aliased to the return node. * @param cfg the CFG of the enclosing method * @param node a return node */ private void updateObligationsForOwningReturn( Set obligations, ControlFlowGraph cfg, ReturnNode node) { if (isTransferOwnershipAtReturn(cfg)) { Node returnExpr = node.getResult(); returnExpr = getTempVarOrNode(returnExpr); if (returnExpr instanceof LocalVariableNode) { removeObligationsContainingVar(obligations, (LocalVariableNode) returnExpr); } } } /** * Helper method that gets the temporary node corresponding to {@code node}, if one exists. If * not, this method returns its input. * * @param node a node * @return the temporary for node, or node if no temporary exists */ /*package-private*/ Node getTempVarOrNode(Node node) { Node temp = typeFactory.getTempVarForNode(node); if (temp != null) { return temp; } return node; } /** * Should ownership be transferred to the return type of the method corresponding to a CFG? * Returns true when there is no {@link NotOwning} annotation on the return type. * * @param cfg the CFG of the method * @return true iff ownership should be transferred to the return type of the method corresponding * to a CFG */ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { if (noLightweightOwnership) { // If not using LO, default to always transfer at return, just like Eclipse does. return true; } UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); if (underlyingAST instanceof UnderlyingAST.CFGMethod) { // TODO: lambdas? In that case false is returned below, which means that ownership will // not be transferred. MethodTree method = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); ExecutableElement executableElement = TreeUtils.elementFromDeclaration(method); return !typeFactory.hasNotOwning(executableElement); } return false; } /** * Updates a set of Obligations to account for an assignment. Assigning to an owning field might * remove Obligations, assigning to a resource variable might remove obligations, assigning to a * new local variable might modify an Obligation (by increasing the size of its resource alias * set), etc. * * @param obligations the set of Obligations to update * @param cfg the control flow graph that contains {@code assignmentNode} * @param assignmentNode the assignment */ private void updateObligationsForAssignment( Set obligations, ControlFlowGraph cfg, AssignmentNode assignmentNode) { Node lhs = assignmentNode.getTarget(); Element lhsElement = TreeUtils.elementFromTree(lhs.getTree()); if (lhsElement == null) { return; } // Use the temporary variable for the rhs if it exists. Node rhs = NodeUtils.removeCasts(assignmentNode.getExpression()); rhs = getTempVarOrNode(rhs); // Ownership transfer to @Owning field. if (lhsElement.getKind() == ElementKind.FIELD) { boolean isOwningField = !noLightweightOwnership && typeFactory.hasOwning(lhsElement); // Check that the must-call obligations of the lhs have been satisfied, if the field is // non-final and owning. if (isOwningField && typeFactory.canCreateObligations() && !ElementUtils.isFinal(lhsElement)) { checkReassignmentToField(obligations, assignmentNode); } // Remove Obligations from local variables, now that the owning field is responsible. // (When obligation creation is turned off, non-final fields cannot take ownership.) if (isOwningField && rhs instanceof LocalVariableNode && (typeFactory.canCreateObligations() || ElementUtils.isFinal(lhsElement))) { LocalVariableNode rhsVar = (LocalVariableNode) rhs; MethodTree containingMethod = cfg.getContainingMethod(assignmentNode.getTree()); boolean inConstructor = containingMethod != null && TreeUtils.isConstructor(containingMethod); // Determine which obligations this field assignment can clear. In a constructor, // assignments to `this.field` only clears obligations on normal return, since // on exception `this` becomes inaccessible. Set toClear; if (inConstructor && lhs instanceof FieldAccessNode && ((FieldAccessNode) lhs).getReceiver() instanceof ThisNode) { toClear = Collections.singleton(MethodExitKind.NORMAL_RETURN); } else { toClear = MethodExitKind.ALL; } @Nullable Element enclosingElem = lhsElement.getEnclosingElement(); @Nullable TypeElement enclosingType = enclosingElem != null ? ElementUtils.enclosingTypeElement(enclosingElem) : null; // Assigning to an owning field is sufficient to clear a must-call alias obligation // in a constructor, if the enclosing class has at most one @Owning field. If the // class had multiple owning fields, then a soundness bug would occur: the must call // alias relationship would allow the whole class' obligation to be fulfilled by // closing only one of the parameters passed to the constructor (but the other // owning fields might not actually have had their obligations fulfilled). See test // case checker/tests/resourceleak/TwoOwningMCATest.java for an example. if (hasAtMostOneOwningField(enclosingType)) { removeObligationsContainingVar( obligations, rhsVar, MustCallAliasHandling.NO_SPECIAL_HANDLING, toClear); } else { removeObligationsContainingVar( obligations, rhsVar, MustCallAliasHandling.RETAIN_OBLIGATIONS_DERIVED_FROM_A_MUST_CALL_ALIAS_PARAMETER, toClear); } // Finally, if any obligations containing this var remain, then closing the field will // satisfy them. Here we are overly cautious and only track final fields. In the // future we could perhaps relax this guard with careful handling for field reassignments. if (ElementUtils.isFinal(lhsElement)) { addAliasToObligationsContainingVar( obligations, rhsVar, new ResourceAlias(JavaExpression.fromNode(lhs), lhsElement, lhs.getTree())); } } } else if (lhs instanceof LocalVariableNode) { LocalVariableNode lhsVar = (LocalVariableNode) lhs; updateObligationsForPseudoAssignment(obligations, assignmentNode, lhsVar, rhs); } } /** * Returns true iff the given type element has 0 or 1 @Owning fields. * * @param element an element for a class * @return true iff element has no more than 1 owning field */ private boolean hasAtMostOneOwningField(TypeElement element) { List fields = ElementUtils.getAllFieldsIn(element, typeFactory.getElementUtils()); // Has an owning field already been encountered? boolean hasOwningField = false; for (VariableElement field : fields) { if (typeFactory.hasOwning(field)) { if (hasOwningField) { return false; } else { hasOwningField = true; } } } // We haven't seen two owning fields, so there must be 1 or 0. return true; } /** * Add a new alias to all Obligations that have {@code var} in their resource-alias set. This * method should be used when {@code var} and {@code newAlias} definitively point to the same * object in memory. * * @param obligations the set of Obligations to modify * @param var a variable * @param newAlias a new {@link ResourceAlias} to add */ private void addAliasToObligationsContainingVar( Set obligations, LocalVariableNode var, ResourceAlias newAlias) { Iterator it = obligations.iterator(); List newObligations = new ArrayList<>(); while (it.hasNext()) { Obligation obligation = it.next(); if (obligation.canBeSatisfiedThrough(var)) { it.remove(); Set newAliases = new LinkedHashSet<>(obligation.resourceAliases); newAliases.add(newAlias); newObligations.add(new Obligation(newAliases, obligation.whenToEnforce)); } } obligations.addAll(newObligations); } /** * Remove any Obligations that contain {@code var} in their resource-alias set. * * @param obligations the set of Obligations to modify * @param var a variable */ /*package-private*/ void removeObligationsContainingVar( Set obligations, LocalVariableNode var) { removeObligationsContainingVar( obligations, var, MustCallAliasHandling.NO_SPECIAL_HANDLING, MethodExitKind.ALL); } /** * Helper type for {@link #removeObligationsContainingVar(Set, LocalVariableNode, * MustCallAliasHandling, Set)} */ private enum MustCallAliasHandling { /** * Obligations derived from {@link MustCallAlias} parameters do not require special handling, * and they should be removed like any other obligation. */ NO_SPECIAL_HANDLING, /** * Obligations derived from {@link MustCallAlias} parameters are not satisfied and should be * retained. */ RETAIN_OBLIGATIONS_DERIVED_FROM_A_MUST_CALL_ALIAS_PARAMETER, } /** * Remove Obligations that contain {@code var} in their resource-alias set. * *

Some operations do not satisfy all Obligations. For instance, assigning to a field in a * constructor only satisfies Obligations when the constructor exits normally (i.e. without * throwing an exception). The last two arguments to this method can be used to retain some * Obligations in special circumstances. * * @param obligations the set of Obligations to modify * @param var a variable * @param mustCallAliasHandling how to treat Obligations derived from {@link MustCallAlias} * parameters * @param whatToClear the kind of Obligations to remove */ private void removeObligationsContainingVar( Set obligations, LocalVariableNode var, MustCallAliasHandling mustCallAliasHandling, Set whatToClear) { List newObligations = new ArrayList<>(); Iterator it = obligations.iterator(); while (it.hasNext()) { Obligation obligation = it.next(); if (obligation.canBeSatisfiedThrough(var) && (mustCallAliasHandling == MustCallAliasHandling.NO_SPECIAL_HANDLING || !obligation.derivedFromMustCallAlias())) { it.remove(); Set whenToEnforce = new HashSet<>(obligation.whenToEnforce); whenToEnforce.removeAll(whatToClear); if (!whenToEnforce.isEmpty()) { newObligations.add(new Obligation(obligation.resourceAliases, whenToEnforce)); } } } obligations.addAll(newObligations); } /** * Update a set of tracked Obligations to account for a (pseudo-)assignment to some variable, as * in a gen-kill dataflow analysis problem. That is, add ("gen") and remove ("kill") resource * aliases from Obligations in the {@code obligations} set as appropriate based on the * (pseudo-)assignment performed by {@code node}. This method may also remove an Obligation * entirely if the analysis concludes that its resource alias set is empty because the last * tracked alias to it has been overwritten (including checking that the must-call obligations * were satisfied before the assignment). * *

Pseudo-assignments may include operations that "assign" to a temporary variable, exposing * the possible value flow into the variable. E.g., for a ternary expression {@code b ? x : y} * whose temporary variable is {@code t}, this method may process "assignments" {@code t = x} and * {@code t = y}, thereby capturing the two possible values of {@code t}. * * @param obligations the tracked Obligations, which will be side-effected * @param node the node performing the pseudo-assignment; it is not necessarily an assignment node * @param lhsVar the left-hand side variable for the pseudo-assignment * @param rhs the right-hand side for the pseudo-assignment, which must have been converted to a * temporary variable (via a call to {@link * ResourceLeakAnnotatedTypeFactory#getTempVarForNode}) */ /*package-private*/ void updateObligationsForPseudoAssignment( Set obligations, Node node, LocalVariableNode lhsVar, Node rhs) { // Replacements to eventually perform in Obligations. This map is kept to avoid a // ConcurrentModificationException in the loop below. Map replacements = new LinkedHashMap<>(); // Cache to re-use on subsequent iterations. ResourceAlias aliasForAssignment = null; for (Obligation obligation : obligations) { // This is a non-null value iff the resource alias set for obligation needs to // change because of the pseudo-assignment. The value of this variable is the new // alias set for `obligation` if it is non-null. Set newResourceAliasesForObligation = null; // Always kill the lhs var if it is present in the resource alias set for this // Obligation by removing it from the resource alias set. ResourceAlias aliasForLhs = obligation.getResourceAlias(lhsVar); if (aliasForLhs != null) { newResourceAliasesForObligation = new LinkedHashSet<>(obligation.resourceAliases); newResourceAliasesForObligation.remove(aliasForLhs); } // If rhs is a variable tracked in the Obligation's resource alias set, gen the lhs // by adding it to the resource alias set. if (rhs instanceof LocalVariableNode && obligation.canBeSatisfiedThrough((LocalVariableNode) rhs)) { LocalVariableNode rhsVar = (LocalVariableNode) rhs; if (newResourceAliasesForObligation == null) { newResourceAliasesForObligation = new LinkedHashSet<>(obligation.resourceAliases); } if (aliasForAssignment == null) { // It is possible to observe assignments to temporary variables, e.g., // synthetic assignments to ternary expression variables in the CFG. For such // cases, use the tree associated with the temp var for the resource alias, // as that is the tree where errors should be reported. Tree treeForAlias = typeFactory.isTempVar(lhsVar) ? typeFactory.getTreeForTempVar(lhsVar) : node.getTree(); aliasForAssignment = new ResourceAlias(new LocalVariable(lhsVar), treeForAlias); } newResourceAliasesForObligation.add(aliasForAssignment); // Remove temp vars from tracking once they are assigned to another location. if (typeFactory.isTempVar(rhsVar)) { ResourceAlias aliasForRhs = obligation.getResourceAlias(rhsVar); if (aliasForRhs != null) { newResourceAliasesForObligation.remove(aliasForRhs); } } } // If no changes were made to the resource alias set, there is no need to update the // Obligation. if (newResourceAliasesForObligation == null) { continue; } if (newResourceAliasesForObligation.isEmpty()) { // Because the last reference to the resource has been overwritten, check the // must-call obligation. MustCallAnnotatedTypeFactory mcAtf = typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); checkMustCall( obligation, typeFactory.getStoreBefore(node), mcAtf.getStoreBefore(node), "variable overwritten by assignment " + node.getTree()); replacements.put(obligation, null); } else { replacements.put( obligation, new Obligation(newResourceAliasesForObligation, MethodExitKind.ALL)); } } // Finally, update the set of Obligations according to the replacements. for (Map.Entry entry : replacements.entrySet()) { obligations.remove(entry.getKey()); if (entry.getValue() != null && !entry.getValue().resourceAliases.isEmpty()) { obligations.add(entry.getValue()); } } } /** * Issues an error if the given re-assignment to a non-final, owning field is not valid. A * re-assignment is valid if the called methods type of the lhs before the assignment satisfies * the must-call obligations of the field. * *

Despite the name of this method, the argument {@code node} might be the first and only * assignment to a field. * * @param obligations current tracked Obligations * @param node an assignment to a non-final, owning field */ private void checkReassignmentToField(Set obligations, AssignmentNode node) { Node lhsNode = node.getTarget(); if (!(lhsNode instanceof FieldAccessNode)) { throw new TypeSystemError( "checkReassignmentToField: non-field node " + node + " of class " + node.getClass()); } FieldAccessNode lhs = (FieldAccessNode) lhsNode; Node receiver = lhs.getReceiver(); if (permitStaticOwning && receiver instanceof ClassNameNode) { return; } // TODO: it would be better to defer getting the path until after checking // for a CreatesMustCallFor annotation, because getting the path can be expensive. // It might be possible to exploit the CFG structure to find the containing // method (rather than using the path, as below), because if a method is being // analyzed then it should be the root of the CFG (I think). TreePath currentPath = typeFactory.getPath(node.getTree()); MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); if (enclosingMethodTree == null) { // The assignment is taking place outside of a method: in a variable declaration's // initializer or in an initializer block. // The Resource Leak Checker issues no error if the assignment is a field initializer. if (node.getTree().getKind() == Tree.Kind.VARIABLE) { // An assignment to a field that is also a declaration must be a field initializer // (VARIABLE Trees are only used for declarations). Assignment in a field // initializer is always permitted. return; } else if (permitInitializationLeak && TreePathUtil.isTopLevelAssignmentInInitializerBlock(currentPath)) { // This is likely not reassignment; if reassignment, the number of assignments that // were not warned about is limited to other initializations (is not unbounded). // This behavior is unsound; see InstanceInitializer.java test case. return; } else { // Issue an error if the field has a non-empty must-call type. MustCallAnnotatedTypeFactory mcTypeFactory = typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); AnnotationMirror mcAnno = mcTypeFactory.getAnnotatedType(lhs.getElement()).getPrimaryAnnotation(MustCall.class); List mcValues = AnnotationUtils.getElementValueArray( mcAnno, mcTypeFactory.getMustCallValueElement(), String.class); if (mcValues.isEmpty()) { return; } VariableElement lhsElement = TreeUtils.variableElementFromTree(lhs.getTree()); checker.reportError( node.getTree(), "required.method.not.called", formatMissingMustCallMethods(mcValues), "field " + lhsElement.getSimpleName().toString(), lhsElement.asType().toString(), "Field assignment outside method or declaration might overwrite field's current value"); return; } } else if (permitInitializationLeak && TreeUtils.isConstructor(enclosingMethodTree)) { Element enclosingClassElement = TreeUtils.elementFromDeclaration(enclosingMethodTree).getEnclosingElement(); if (ElementUtils.isTypeElement(enclosingClassElement)) { Element receiverElement = TypesUtils.getTypeElement(receiver.getType()); if (Objects.equals(enclosingClassElement, receiverElement)) { return; } } } // Check that there is a corresponding CreatesMustCallFor annotation, unless this is // 1) an assignment to a field of a newly-declared local variable whose scope does not // extend beyond the method's body (and which therefore could not be targeted by an // annotation on the method declaration), or 2) the rhs is a null literal (so there's // nothing to reset). if (!(receiver instanceof LocalVariableNode && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) && !(node.getExpression() instanceof NullLiteralNode)) { checkEnclosingMethodIsCreatesMustCallFor(node, enclosingMethodTree); } // The following code handles a special case where the field being assigned is itself getting // passed in an owning position to another method on the RHS of the assignment. // For example, if the field's type is a class whose constructor takes another instance // of itself (such as a node in a linked list) in an owning position, re-assigning the field // to a new instance that takes the field's value as an owning parameter is safe (the new value // has taken responsibility for closing the old value). In such a case, it is not required // that the must-call obligation of the field be satisfied via method calls before the // assignment, since the invoked method will take ownership of the object previously // referenced by the field and handle the obligation. This fixes the false positive in // https://github.com/typetools/checker-framework/issues/5971. Node rhs = node.getExpression(); if (!noLightweightOwnership && (rhs instanceof ObjectCreationNode || rhs instanceof MethodInvocationNode)) { List arguments = getArgumentsOfInvocation(rhs); List parameters = getParametersOfInvocation(rhs); if (arguments.size() == parameters.size()) { for (int i = 0; i < arguments.size(); i++) { VariableElement param = parameters.get(i); if (typeFactory.hasOwning(param)) { Node argument = arguments.get(i); if (argument.equals(lhs)) { return; } } } } else { // This could happen, e.g., with varargs, or with strange cases like generated Enum // constructors. In the varargs case (i.e. if the varargs parameter is owning), // only the first of the varargs arguments will actually get transferred: the second // and later varargs arguments will continue to be tracked at the call-site. // For now, just skip this case - the worst that will happen is a false positive in // cases like the varargs one described above. // TODO allow for ownership transfer here if needed in future, but for now do nothing } } MustCallAnnotatedTypeFactory mcTypeFactory = typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); // Get the Must Call type for the field. If there's info about this field in the store, use // that. Otherwise, use the declared type of the field CFStore mcStore = mcTypeFactory.getStoreBefore(lhs); CFValue mcValue = mcStore.getValue(lhs); AnnotationMirror mcAnno = null; if (mcValue != null) { mcAnno = AnnotationUtils.getAnnotationByClass(mcValue.getAnnotations(), MustCall.class); } if (mcAnno == null) { // No stored value (or the stored value is Poly/top), so use the declared type. mcAnno = mcTypeFactory.getAnnotatedType(lhs.getElement()).getPrimaryAnnotation(MustCall.class); } // if mcAnno is still null, then the declared type must be something other than // @MustCall (probably @MustCallUnknown). Do nothing in this case: a warning // about the field will be issued elsewhere (it will be impossible to satisfy its // obligations!). if (mcAnno == null) { return; } List mcValues = AnnotationUtils.getElementValueArray( mcAnno, mcTypeFactory.getMustCallValueElement(), String.class); if (mcValues.isEmpty()) { return; } // Get the store before the RHS rather than the assignment node, because the CFG always has // the RHS first. If the RHS has side-effects, then the assignment node's store will have // had its inferred types erased. AccumulationStore cmStoreBefore = typeFactory.getStoreBefore(rhs); AccumulationValue cmValue = cmStoreBefore == null ? null : cmStoreBefore.getValue(lhs); AnnotationMirror cmAnno = null; if (cmValue != null) { // When store contains the lhs Set accumulatedValues = cmValue.getAccumulatedValues(); if (accumulatedValues != null) { // type variable or wildcard type cmAnno = typeFactory.createCalledMethods(accumulatedValues.toArray(new String[0])); } else { for (AnnotationMirror anno : cmValue.getAnnotations()) { if (AnnotationUtils.areSameByName( anno, "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { cmAnno = anno; } } } } if (cmAnno == null) { cmAnno = typeFactory.top; } if (!calledMethodsSatisfyMustCall(mcValues, cmAnno)) { VariableElement lhsElement = TreeUtils.variableElementFromTree(lhs.getTree()); if (!checker.shouldSkipUses(lhsElement)) { checker.reportError( node.getTree(), "required.method.not.called", formatMissingMustCallMethods(mcValues), "field " + lhsElement.getSimpleName().toString(), lhsElement.asType().toString(), " Non-final owning field might be overwritten"); } } } /** * Checks that the method that encloses an assignment is marked with @CreatesMustCallFor * annotation whose target is the object whose field is being re-assigned. * * @param node an assignment node whose lhs is a non-final, owning field * @param enclosingMethod the MethodTree in which the re-assignment takes place */ private void checkEnclosingMethodIsCreatesMustCallFor( AssignmentNode node, MethodTree enclosingMethod) { Node lhs = node.getTarget(); if (!(lhs instanceof FieldAccessNode)) { return; } if (permitStaticOwning && ((FieldAccessNode) lhs).getReceiver() instanceof ClassNameNode) { return; } String receiverString = receiverAsString((FieldAccessNode) lhs); if ("this".equals(receiverString) && TreeUtils.isConstructor(enclosingMethod)) { // Constructors always create must-call obligations, so there is no need for them to // be annotated. return; } ExecutableElement enclosingMethodElt = TreeUtils.elementFromDeclaration(enclosingMethod); MustCallAnnotatedTypeFactory mcAtf = typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); List cmcfValues = ResourceLeakVisitor.getCreatesMustCallForValues(enclosingMethodElt, mcAtf, typeFactory); if (cmcfValues.isEmpty()) { checker.reportError( enclosingMethod, "missing.creates.mustcall.for", enclosingMethodElt.getSimpleName().toString(), receiverString, ((FieldAccessNode) lhs).getFieldName()); return; } List checked = new ArrayList<>(); for (String targetStrWithoutAdaptation : cmcfValues) { String targetStr; try { targetStr = StringToJavaExpression.atMethodBody( targetStrWithoutAdaptation, enclosingMethod, checker) .toString(); } catch (JavaExpressionParseException e) { targetStr = targetStrWithoutAdaptation; } if (targetStr.equals(receiverString)) { // This @CreatesMustCallFor annotation matches. return; } checked.add(targetStr); } checker.reportError( enclosingMethod, "incompatible.creates.mustcall.for", enclosingMethodElt.getSimpleName().toString(), receiverString, ((FieldAccessNode) lhs).getFieldName(), String.join(", ", checked)); } /** * Gets a standardized name for an object whose field is being re-assigned. * * @param fieldAccessNode a field access node * @return the name of the object whose field is being accessed (the receiver), as a string */ private String receiverAsString(FieldAccessNode fieldAccessNode) { Node receiver = fieldAccessNode.getReceiver(); if (receiver instanceof ThisNode) { return "this"; } if (receiver instanceof LocalVariableNode) { return ((LocalVariableNode) receiver).getName(); } if (receiver instanceof ClassNameNode) { return ((ClassNameNode) receiver).getElement().toString(); } if (receiver instanceof SuperNode) { return "super"; } throw new TypeSystemError( "unexpected receiver of field assignment: " + receiver + " of type " + receiver.getClass()); } /** * Finds the arguments passed in the {@code @MustCallAlias} positions for a call. * * @param callNode callNode representing the call; must be {@link MethodInvocationNode} or {@link * ObjectCreationNode} * @return if {@code callNode} invokes a method with a {@code @MustCallAlias} annotation on some * formal parameter(s) (or the receiver), returns the result of calling {@link * #removeCastsAndGetTmpVarIfPresent(Node)} on the argument(s) passed in corresponding * position(s). Otherwise, returns an empty List. */ private List getMustCallAliasArgumentNodes(Node callNode) { Preconditions.checkArgument( callNode instanceof MethodInvocationNode || callNode instanceof ObjectCreationNode); List result = new ArrayList<>(); if (!typeFactory.hasMustCallAlias(callNode.getTree())) { return result; } List args = getArgumentsOfInvocation(callNode); List parameters = getParametersOfInvocation(callNode); for (int i = 0; i < args.size(); i++) { if (typeFactory.hasMustCallAlias(parameters.get(i))) { result.add(removeCastsAndGetTmpVarIfPresent(args.get(i))); } } // If none of the parameters were @MustCallAlias, it must be the receiver if (result.isEmpty() && callNode instanceof MethodInvocationNode) { result.add( removeCastsAndGetTmpVarIfPresent( ((MethodInvocationNode) callNode).getTarget().getReceiver())); } return result; } /** * If a temporary variable exists for node after typecasts have been removed, return it. * Otherwise, return node. * * @param node a node * @return either a tempvar for node's content sans typecasts, or node */ private Node removeCastsAndGetTmpVarIfPresent(Node node) { // TODO: Create temp vars for TypeCastNodes as well, so there is no need to explicitly // remove casts here. node = NodeUtils.removeCasts(node); return getTempVarOrNode(node); } /** * Get the nodes representing the arguments of a method or constructor invocation from the * invocation node. * * @param node a MethodInvocation or ObjectCreation node * @return the arguments, in order */ /*package-private*/ List getArgumentsOfInvocation(Node node) { if (node instanceof MethodInvocationNode) { MethodInvocationNode invocationNode = (MethodInvocationNode) node; return invocationNode.getArguments(); } else if (node instanceof ObjectCreationNode) { return ((ObjectCreationNode) node).getArguments(); } else { throw new TypeSystemError("unexpected node type " + node.getClass()); } } /** * Get the elements representing the formal parameters of a method or constructor, from an * invocation of that method or constructor. * * @param node a method invocation or object creation node * @return a list of the declarations of the formal parameters of the method or constructor being * invoked */ /*package-private*/ List getParametersOfInvocation(Node node) { ExecutableElement executableElement; if (node instanceof MethodInvocationNode) { MethodInvocationNode invocationNode = (MethodInvocationNode) node; executableElement = TreeUtils.elementFromUse(invocationNode.getTree()); } else if (node instanceof ObjectCreationNode) { executableElement = TreeUtils.elementFromUse(((ObjectCreationNode) node).getTree()); } else { throw new TypeSystemError("unexpected node type " + node.getClass()); } return executableElement.getParameters(); } /** * Is the return type of the invoked method one that should be tracked? * * @param node a method invocation * @return true iff the checker is in no-lightweight-ownership mode, or the method has a * {@code @MustCallAlias} annotation, or (1) the method has a return type that needs to be * tracked (i.e., it has a non-empty {@code @MustCall} obligation and (2) the method * declaration does not have a {@code @NotOwning} annotation */ private boolean shouldTrackReturnType(MethodInvocationNode node) { if (noLightweightOwnership) { // Default to always transferring at return if not using LO, just like Eclipse does. return true; } MethodInvocationTree methodInvocationTree = node.getTree(); ExecutableElement executableElement = TreeUtils.elementFromUse(methodInvocationTree); if (typeFactory.hasMustCallAlias(executableElement)) { // assume tracking is required return true; } TypeMirror type = ElementUtils.getType(executableElement); // void or primitive-returning methods are "not owning" by construction if (type.getKind() == TypeKind.VOID || type.getKind().isPrimitive()) { return false; } TypeElement typeElt = TypesUtils.getTypeElement(type); // no need to track if type has no possible @MustCall obligation if (typeElt != null && typeFactory.hasEmptyMustCallValue(typeElt) && typeFactory.hasEmptyMustCallValue(methodInvocationTree)) { return false; } // check for absence of @NotOwning annotation return !typeFactory.hasNotOwning(executableElement); } /** * Get all successor blocks for some block, except for those corresponding to ignored exception * types. See {@link ResourceLeakAnalysis#isIgnoredExceptionType(TypeMirror)}. Each exceptional * successor is paired with the type of exception that leads to it, for use in error messages. * * @param block input block * @return set of pairs (b, t), where b is a successor block, and t is the type of exception for * the CFG edge from block to b, or {@code null} if b is a non-exceptional successor */ private Set> getSuccessorsExceptIgnoredExceptions( Block block) { if (block.getType() == Block.BlockType.EXCEPTION_BLOCK) { ExceptionBlock excBlock = (ExceptionBlock) block; Set> result = new LinkedHashSet<>(); // regular successor Block regularSucc = excBlock.getSuccessor(); if (regularSucc != null) { result.add(IPair.of(regularSucc, null)); } // non-ignored exception successors Map> exceptionalSuccessors = excBlock.getExceptionalSuccessors(); for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { TypeMirror exceptionType = entry.getKey(); if (!analysis.isIgnoredExceptionType(exceptionType)) { for (Block exSucc : entry.getValue()) { result.add(IPair.of(exSucc, exceptionType)); } } } return result; } else { Set> result = new LinkedHashSet<>(); for (Block b : block.getSuccessors()) { result.add(IPair.of(b, null)); } return result; } } /** * Propagates a set of Obligations to successors, and performs consistency checks when variables * are going out of scope. * *

The basic algorithm loops over the successor blocks of the current block. For each * successor, two things happen: * *

First, it constructs an updated set of Obligations using {@code incomingObligations}, the * nodes in {@code currentBlock}, and the nature of the edge from {@code currentBlock} to the * successor. The edge can either be normal control flow or an exception. See * *

    *
  • {@link #updateObligationsForAssignment(Set, ControlFlowGraph, AssignmentNode)} *
  • {@link #updateObligationsForOwningReturn(Set, ControlFlowGraph, ReturnNode)} *
  • {@link #updateObligationsForInvocation(Set, Node, TypeMirror)} *
* *

Second, it checks every Obligation in obligations. If the successor is an exit block or all * of an Obligation's resource aliases might be going out of scope, then a consistency check * occurs (with two exceptions, both related to temporary variables that don't actually get * assigned; see code comments for details) and an error is issued if it fails. If the successor * is any other kind of block and there is information about at least one of the Obligation's * aliases in the successor store (i.e. the resource itself definitely does not go out of scope), * then the Obligation is passed forward to the successor ("propagated") with any definitely * out-of-scope aliases removed from its resource alias set. * * @param cfg the control flow graph * @param incomingObligations the Obligations for the current block * @param currentBlock the current block * @param visited block-Obligations pairs already analyzed or already on the worklist * @param worklist current worklist */ private void propagateObligationsToSuccessorBlocks( ControlFlowGraph cfg, Set incomingObligations, Block currentBlock, Set visited, Deque worklist) { // For each successor block that isn't caused by an ignored exception type, this loop // computes the set of Obligations that should be propagated to it and then adds it to the // worklist if any of its resource aliases are still in scope in the successor block. If // none are, then the loop performs a consistency check for that Obligation. for (IPair successorAndExceptionType : getSuccessorsExceptIgnoredExceptions(currentBlock)) { // A *mutable* set that eventually holds the set of dataflow facts to be propagated to // successor blocks. The set is initialized to the current dataflow facts and updated by // the methods invoked in the for loop below. Set obligations = new LinkedHashSet<>(incomingObligations); // PERFORMANCE NOTE: The computed changes to `obligations` are mostly the same for each // successor block, but can vary slightly depending on the exception type. There might // be some opportunities for optimization in this mostly-redundant work. for (Node node : currentBlock.getNodes()) { if (node instanceof AssignmentNode) { updateObligationsForAssignment(obligations, cfg, (AssignmentNode) node); } else if (node instanceof ReturnNode) { updateObligationsForOwningReturn(obligations, cfg, (ReturnNode) node); } else if (node instanceof MethodInvocationNode || node instanceof ObjectCreationNode) { updateObligationsForInvocation(obligations, node, successorAndExceptionType.second); } // All other types of nodes are ignored. This is safe, because other kinds of // nodes cannot create or modify the resource-alias sets that the algorithm is // tracking. } propagateObligationsToSuccessorBlock( obligations, currentBlock, successorAndExceptionType.first, successorAndExceptionType.second, visited, worklist); } } /** * Helper for {@link #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, * Deque)} that propagates obligations along a single edge. * * @param obligations the Obligations for the current block * @param currentBlock the current block * @param successor a successor of the current block * @param exceptionType the type of edge from currentBlock to successor: * null for normal control flow, or a throwable type for exceptional control flow * @param visited block-Obligations pairs already analyzed or already on the worklist * @param worklist current worklist */ private void propagateObligationsToSuccessorBlock( Set obligations, Block currentBlock, Block successor, @Nullable TypeMirror exceptionType, Set visited, Deque worklist) { List currentBlockNodes = currentBlock.getNodes(); // successorObligations eventually contains the Obligations to propagate to successor. // The loop below mutates it. Set successorObligations = new LinkedHashSet<>(); // A detailed reason to give in the case that the last resource alias of an Obligation // goes out of scope without a called-methods type that satisfies the corresponding // must-call obligation along the current control-flow edge. Computed here for // efficiency; used in the loop over the Obligations, below. String exitReasonForErrorMessage = exceptionType == null ? // Technically the variable may be going out of scope before the method // exit, but that doesn't seem to provide additional helpful // information. "regular method exit" : "possible exceptional exit due to " + ((ExceptionBlock) currentBlock).getNode().getTree() + " with exception type " + exceptionType; // Computed outside the Obligation loop for efficiency. AccumulationStore regularStoreOfSuccessor = analysis.getInput(successor).getRegularStore(); for (Obligation obligation : obligations) { // This boolean is true if there is no evidence that the Obligation does not go out // of scope - that is, if there is definitely a resource alias that is in scope in // the successor. boolean obligationGoesOutOfScopeBeforeSuccessor = true; for (ResourceAlias resourceAlias : obligation.resourceAliases) { if (aliasInScopeInSuccessor(regularStoreOfSuccessor, resourceAlias)) { obligationGoesOutOfScopeBeforeSuccessor = false; break; } } // This check is to determine if this Obligation's resource aliases are definitely // going out of scope: if this is an exit block or there is no information about any // of them in the successor store, all aliases must be going out of scope and a // consistency check should occur. if (successor.getType() == BlockType.SPECIAL_BLOCK /* special blocks are exit blocks */ || obligationGoesOutOfScopeBeforeSuccessor) { MustCallAnnotatedTypeFactory mcAtf = typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); // If successor is an exceptional successor, and Obligation represents the // temporary variable for currentBlock's node, do not propagate or do a // consistency check, as in the exceptional case the "assignment" to the // temporary variable does not succeed. // // Note that this test cannot be "successor.getType() == // BlockType.EXCEPTIONAL_BLOCK", because not every exceptional successor is an // exceptional block. For example, the successor might be a regular block // (containing a catch clause, for example), or a special block indicating an // exceptional exit. Nor can this test be "currentBlock.getType() == // BlockType.EXCEPTIONAL_BLOCK", because some exception types are ignored. // Whether exceptionType is null captures the logic of both of these cases. if (exceptionType != null) { Node exceptionalNode = NodeUtils.removeCasts(((ExceptionBlock) currentBlock).getNode()); LocalVariableNode tmpVarForExcNode = typeFactory.getTempVarForNode(exceptionalNode); if (tmpVarForExcNode != null && obligation.resourceAliases.size() == 1 && obligation.canBeSatisfiedThrough(tmpVarForExcNode)) { continue; } } // Always propagate the Obligation to the successor if current block represents // code nested in a cast. Without this logic, the analysis may report a false // positive when the Obligation represents a temporary variable for a nested // expression, as the temporary may not appear in the successor store and hence // seems to be going out of scope. The temporary will be handled with special // logic; casts are unwrapped at various points in the analysis. if (currentBlockNodes.size() == 1 && inCast(currentBlockNodes.get(0))) { successorObligations.add(obligation); continue; } // At this point, a consistency check will definitely occur, unless the // obligation was derived from a MustCallAlias parameter. If it was, an error is // immediately issued, because such a parameter should not go out of scope // without its obligation being resolved some other way. if (obligation.derivedFromMustCallAlias()) { // MustCallAlias annotations only have meaning if the method returns // normally, so issue an error if and only if this exit is happening on a // normal exit path. if (exceptionType == null && obligation.whenToEnforce.contains(MethodExitKind.NORMAL_RETURN)) { checker.reportError( obligation.resourceAliases.asList().get(0).tree, "mustcallalias.out.of.scope", exitReasonForErrorMessage); } // Whether or not an error is issued, the check is now complete - there is // no further checking to do on a must-call-alias-derived obligation along // an exceptional path. continue; } // Which stores from the called-methods and must-call checkers are used in // the consistency check varies depending on the context. The rules are: // 1. if the current block has no nodes (and therefore the store must come from // a block // rather than a node): // 1a. if there is information about any alias in the resource alias set // in the successor store, use the successor's CM and MC stores, which // contain whatever information is true after this block finishes. // 1b. if there is not any information about any alias in the resource alias // set in the successor store, use the current blocks' CM and MC stores, // which contain whatever information is true before this (empty) block. // 2. if the current block has one or more nodes, always use the CM store after // the last node. To decide which MC store to use: // 2a. if the last node in the block is the invocation of an // @CreatesMustCallFor method that might throw an exception, and the // consistency check is for an exceptional path, use the MC store // immediately before the method invocation, because the method threw an // exception rather than finishing and therefore did not actually create // any must-call obligation, so the MC store after might contain // must-call obligations that do not need to be fulfilled along this // path. // 2b. in all other cases, use the MC store from after the last node in // the block. CFStore mcStore; AccumulationStore cmStore; if (currentBlockNodes.size() == 0 /* currentBlock is special or conditional */) { cmStore = obligationGoesOutOfScopeBeforeSuccessor ? analysis.getInput(currentBlock).getRegularStore() // 1a. (CM) : regularStoreOfSuccessor; // 1b. (CM) mcStore = mcAtf.getStoreForBlock( obligationGoesOutOfScopeBeforeSuccessor, currentBlock, // 1a. (MC) successor); // 1b. (MC) } else { // In this case, current block has at least one node. // Use the called-methods store immediately after the last node in // currentBlock. Node last = currentBlockNodes.get(currentBlockNodes.size() - 1); // 2. (CM) if (cmStoreAfter.containsKey(last)) { cmStore = cmStoreAfter.get(last); } else { cmStore = typeFactory.getStoreAfter(last); cmStoreAfter.put(last, cmStore); } // If this is an exceptional block, check the MC store beforehand to avoid // issuing an error about a call to a CreatesMustCallFor method that might // throw an exception. Otherwise, use the store after. if (exceptionType != null && isInvocationOfCreatesMustCallForMethod(last)) { mcStore = mcAtf.getStoreBefore(last); // 2a. (MC) } else { if (mcStoreAfter.containsKey(last)) { mcStore = mcStoreAfter.get(last); } else { mcStore = mcAtf.getStoreAfter(last); // 2b. (MC) mcStoreAfter.put(last, mcStore); } } } MethodExitKind exitKind = exceptionType == null ? MethodExitKind.NORMAL_RETURN : MethodExitKind.EXCEPTIONAL_EXIT; if (obligation.whenToEnforce.contains(exitKind)) { checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); } } else { // In this case, there is info in the successor store about some alias in the // Obligation. // Handles the possibility that some resource in the Obligation may go out of // scope. Set copyOfResourceAliases = new LinkedHashSet<>(obligation.resourceAliases); copyOfResourceAliases.removeIf( alias -> !aliasInScopeInSuccessor(regularStoreOfSuccessor, alias)); successorObligations.add(new Obligation(copyOfResourceAliases, obligation.whenToEnforce)); } } propagate(new BlockWithObligations(successor, successorObligations), visited, worklist); } /** * Returns true if {@code alias.reference} is definitely in-scope in the successor store: that is, * there is a value for it in {@code successorStore}. * * @param successorStore the regular store of the successor block * @param alias the resource alias to check * @return true if the variable is definitely in scope for the purposes of the consistency * checking algorithm in the successor block from which the store came */ private boolean aliasInScopeInSuccessor(AccumulationStore successorStore, ResourceAlias alias) { return successorStore.getValue(alias.reference) != null; } /** * Returns true if node is a MethodInvocationNode of a method with a CreatesMustCallFor * annotation. * * @param node a node * @return true if node is a MethodInvocationNode of a method with a CreatesMustCallFor annotation */ private boolean isInvocationOfCreatesMustCallForMethod(Node node) { if (!(node instanceof MethodInvocationNode)) { return false; } MethodInvocationNode miNode = (MethodInvocationNode) node; return typeFactory.hasCreatesMustCallFor(miNode); } /** * Finds {@link Owning} formal parameters for the method corresponding to a CFG. * * @param cfg the CFG * @return the owning formal parameters of the method that corresponds to the given cfg, or an * empty set if the given CFG doesn't correspond to a method body */ private Set computeOwningParameters(ControlFlowGraph cfg) { // TODO what about lambdas? if (cfg.getUnderlyingAST().getKind() == Kind.METHOD) { MethodTree method = ((UnderlyingAST.CFGMethod) cfg.getUnderlyingAST()).getMethod(); Set result = new LinkedHashSet<>(1); for (VariableTree param : method.getParameters()) { VariableElement paramElement = TreeUtils.elementFromDeclaration(param); boolean hasMustCallAlias = typeFactory.hasMustCallAlias(paramElement); if (hasMustCallAlias || (typeFactory.declaredTypeHasMustCall(param) && !noLightweightOwnership && paramElement.getAnnotation(Owning.class) != null)) { result.add( new Obligation( ImmutableSet.of( new ResourceAlias( new LocalVariable(paramElement), paramElement, param, hasMustCallAlias)), Collections.singleton(MethodExitKind.NORMAL_RETURN))); // Increment numMustCall for each @Owning parameter tracked by the enclosing // method. incrementNumMustCall(paramElement); } } return result; } return Collections.emptySet(); } /** * Checks whether there is some resource alias set R in {@code obligations} such that * R contains a {@link ResourceAlias} whose local variable is {@code node}. * * @param obligations the set of Obligations to search * @param var the local variable to look for * @return true iff there is a resource alias set in {@code obligations} that contains node */ private static boolean varTrackedInObligations( Set obligations, LocalVariableNode var) { for (Obligation obligation : obligations) { if (obligation.canBeSatisfiedThrough(var)) { return true; } } return false; } /** * Gets the Obligation whose resource aliase set contains the given local variable, if one exists * in {@code obligations}. * * @param obligations set of Obligations * @param node variable of interest * @return the Obligation in {@code obligations} whose resource alias set contains {@code node}, * or {@code null} if there is no such Obligation */ /*package-private*/ static @Nullable Obligation getObligationForVar( Set obligations, LocalVariableNode node) { for (Obligation obligation : obligations) { if (obligation.canBeSatisfiedThrough(node)) { return obligation; } } return null; } /** * For the given Obligation, checks that at least one of its variables has its {@code @MustCall} * obligation satisfied, based on {@code @CalledMethods} and {@code @MustCall} types in the given * stores. * * @param obligation the Obligation * @param cmStore the called-methods store * @param mcStore the must-call store * @param outOfScopeReason if the {@code @MustCall} obligation is not satisfied, a useful * explanation to include in the error message */ private void checkMustCall( Obligation obligation, AccumulationStore cmStore, CFStore mcStore, String outOfScopeReason) { Map> mustCallValues = obligation.getMustCallMethods(typeFactory, mcStore); // optimization: if mustCallValues is null, always issue a warning (there is no way to satisfy // the check). A null mustCallValue occurs when the type is top (@MustCallUnknown). if (mustCallValues == null) { // Report the error at the first alias' definition. This choice is arbitrary but // consistent. ResourceAlias firstAlias = obligation.resourceAliases.iterator().next(); if (!reportedErrorAliases.contains(firstAlias)) { if (!checker.shouldSkipUses(TreeUtils.elementFromTree(firstAlias.tree))) { reportedErrorAliases.add(firstAlias); checker.reportError( firstAlias.tree, "required.method.not.known", firstAlias.stringForErrorMessage(), firstAlias.reference.getType().toString(), outOfScopeReason); } } return; } if (mustCallValues.isEmpty()) { throw new TypeSystemError("unexpected empty must-call values for obligation " + obligation); } boolean mustCallSatisfied = false; for (ResourceAlias alias : obligation.resourceAliases) { List mustCallValuesForAlias = mustCallValues.get(alias); // optimization when there are no methods to call if (mustCallValuesForAlias.isEmpty()) { mustCallSatisfied = true; break; } // sometimes the store is null! this looks like a bug in checker dataflow. // TODO track down and report the root-cause bug AccumulationValue cmValue = cmStore != null ? cmStore.getValue(alias.reference) : null; AnnotationMirror cmAnno = null; if (cmValue != null) { // When store contains the lhs Set accumulatedValues = cmValue.getAccumulatedValues(); if (accumulatedValues != null) { // type variable or wildcard type cmAnno = typeFactory.createCalledMethods(accumulatedValues.toArray(new String[0])); } else { for (AnnotationMirror anno : cmValue.getAnnotations()) { if (AnnotationUtils.areSameByName( anno, "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { cmAnno = anno; } } } } if (cmAnno == null) { cmAnno = typeFactory .getAnnotatedType(alias.element) .getEffectiveAnnotationInHierarchy(typeFactory.top); } if (calledMethodsSatisfyMustCall(mustCallValuesForAlias, cmAnno)) { mustCallSatisfied = true; break; } } if (!mustCallSatisfied) { // Report the error at the first alias' definition. This choice is arbitrary but // consistent. ResourceAlias firstAlias = obligation.resourceAliases.iterator().next(); if (!reportedErrorAliases.contains(firstAlias)) { if (!checker.shouldSkipUses(TreeUtils.elementFromTree(firstAlias.tree))) { reportedErrorAliases.add(firstAlias); checker.reportError( firstAlias.tree, "required.method.not.called", formatMissingMustCallMethods(mustCallValues.get(firstAlias)), firstAlias.stringForErrorMessage(), firstAlias.reference.getType().toString(), outOfScopeReason); } } } } /** * Increment the -AcountMustCall counter. * * @param node the node being counted, to extract the type */ private void incrementNumMustCall(Node node) { if (countMustCall) { TypeMirror type = node.getType(); incrementMustCallImpl(type); } } /** * Increment the -AcountMustCall counter. * * @param elt the elt being counted, to extract the type */ private void incrementNumMustCall(Element elt) { if (countMustCall) { TypeMirror type = elt.asType(); incrementMustCallImpl(type); } } /** * Shared implementation for the two version of countMustCall. Don't call this directly. * * @param type the type of the object that has a must call obligation */ private void incrementMustCallImpl(TypeMirror type) { // only count uses of JDK classes, since that's what the paper reported if (!isJdkClass(TypesUtils.getTypeElement(type).getQualifiedName().toString())) { return; } checker.numMustCall++; } /** * Is the given class a java* class? This is a heuristic for whether the class was defined in the * JDK. * * @param qualifiedName a fully qualified name of a class * @return true iff the type's fully-qualified name starts with "java", indicating that it is from * a java.* or javax.* package (probably) */ /*package-private*/ static boolean isJdkClass(String qualifiedName) { return qualifiedName.startsWith("java"); } /** * Do the called methods represented by the {@link CalledMethods} type {@code cmAnno} include all * the methods in {@code mustCallValues}? * * @param mustCallValues the strings representing the must-call obligations * @param cmAnno an annotation from the called-methods type hierarchy * @return true iff cmAnno is a subtype of a called-methods annotation with the same values as * mustCallValues */ /*package-private*/ boolean calledMethodsSatisfyMustCall( List mustCallValues, AnnotationMirror cmAnno) { // Create this annotation and use a subtype test because there's no guarantee that // cmAnno is actually an instance of CalledMethods: it could be CMBottom or CMPredicate. AnnotationMirror cmAnnoForMustCallMethods = typeFactory.createCalledMethods(mustCallValues.toArray(new String[mustCallValues.size()])); return typeFactory .getQualifierHierarchy() .isSubtypeQualifiersOnly(cmAnno, cmAnnoForMustCallMethods); } /** * If the input {@code state} has not been visited yet, add it to {@code visited} and {@code * worklist}. * * @param state the current state * @param visited the states that have been analyzed or are already on the worklist * @param worklist the states that will be analyzed */ private static void propagate( BlockWithObligations state, Set visited, Deque worklist) { if (visited.add(state)) { worklist.add(state); } } /** * Formats a list of must-call method names to be printed in an error message. * * @param mustCallVal the list of must-call strings * @return a formatted string */ /*package-private*/ static String formatMissingMustCallMethods(List mustCallVal) { int size = mustCallVal.size(); if (size == 0) { throw new TypeSystemError("empty mustCallVal " + mustCallVal); } else if (size == 1) { return "method " + mustCallVal.get(0); } else { return "methods " + String.join(", ", mustCallVal); } } /** * A pair of a {@link Block} and a set of dataflow facts on entry to the block. Each dataflow fact * represents a set of resource aliases for some tracked resource. The analyzer's worklist * consists of BlockWithObligations objects, each representing the need to handle the set of * dataflow facts reaching the block during analysis. */ /*package-private*/ static class BlockWithObligations { /** The block. */ public final Block block; /** The dataflow facts. */ public final ImmutableSet obligations; /** * Create a new BlockWithObligations from a block and a set of dataflow facts. * * @param b the block * @param obligations the set of incoming Obligations at the start of the block (may be the * empty set) */ public BlockWithObligations(Block b, Set obligations) { this.block = b; this.obligations = ImmutableSet.copyOf(obligations); } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } BlockWithObligations that = (BlockWithObligations) o; return block.equals(that.block) && obligations.equals(that.obligations); } @Override public int hashCode() { return Objects.hash(block, obligations); } @Override public String toString() { return String.format( "BWO{%s %d, %d obligations %d}", block.getType(), block.getUid(), obligations.size(), obligations.hashCode()); } /** * Returns a printed representation of a collection of BlockWithObligations. If a * BlockWithObligations appears multiple times in the collection, it is printed more succinctly * after the first time. * * @param bwos a collection of BlockWithObligations, to format * @return a printed representation of a collection of BlockWithObligations */ public static String collectionToString(Collection bwos) { List blocksWithDuplicates = new ArrayList<>(); for (BlockWithObligations bwo : bwos) { blocksWithDuplicates.add(bwo.block); } List duplicateBlocks = duplicates(blocksWithDuplicates); StringJoiner result = new StringJoiner(", ", "BWOs[", "]"); for (BlockWithObligations bwo : bwos) { ImmutableSet obligations = bwo.obligations; if (duplicateBlocks.contains(bwo.block)) { result.add( String.format( "BWO{%s %d, %d obligations %s}", bwo.block.getType(), bwo.block.getUid(), obligations.size(), obligations)); } else { result.add( String.format( "BWO{%s %d, %d obligations}", bwo.block.getType(), bwo.block.getUid(), obligations.size())); } } return result.toString(); } } // TODO: Use from plume-lib's CollectionsPlume once version 1.9.0 is released. /** * Returns the elements (once each) that appear more than once in the given collection. * * @param the type of elements * @param c a collection * @return the elements (once each) that appear more than once in the given collection */ public static List duplicates(Collection c) { // Inefficient (because of streams) but simple implementation. Set withoutDuplicates = new HashSet<>(); return c.stream().filter(n -> !withoutDuplicates.add(n)).collect(Collectors.toList()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy