diff --git a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java index 56a1cb84ec51..7116877210a4 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java +++ b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java @@ -2,6 +2,8 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.NewClassTree; +import com.sun.source.util.TreePath; +import com.sun.tools.javac.code.Type; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; @@ -13,6 +15,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; @@ -30,6 +33,7 @@ import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.framework.util.AnnotationMirrorMap; import org.checkerframework.framework.util.AnnotationMirrorSet; +import org.checkerframework.framework.util.QualifierPolymorphismUtil; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -184,6 +188,41 @@ public void resolve(MethodInvocationTree tree, AnnotatedExecutableType type) { atypeFactory.getReceiverType(tree), type.getReceiverType())); } + // receiver-sensitive poly resolution: resolve return value with assignment context l-value + TreePath path = atypeFactory.getPath(tree); + if (path != null) { + AnnotatedTypeMirror assignmentContext = + QualifierPolymorphismUtil.assignedTo(atypeFactory, path); + + if (assignmentContext != null) { + // get erased subtype relation + TypeMirror javaLhstype = + atypeFactory.types.erasure(assignmentContext.getUnderlyingType()); + TypeMirror javaReturntype = + atypeFactory.types.erasure(type.getReturnType().getUnderlyingType()); + // primitive types need special care + boolean polyIsSub = + javaReturntype instanceof Type.JCPrimitiveType + || atypeFactory.types.isSubtype(javaReturntype, javaLhstype); + + boolean polyIsSuper = + !(javaReturntype instanceof Type.ArrayType) + && // isSubtype cannot handle this + atypeFactory.types.isSubtype(javaLhstype, javaReturntype); + + // only perform receiver-sensitive poly resolution when return type is erased + // subtype of lhs + if (polyIsSub || polyIsSuper) { // lhs and return type are comparable + instantiationMapping = + collector.reduceWithUpperBounds( + instantiationMapping, + collector.visit( + // Actual assignment lhs type + assignmentContext, type.getReturnType(), polyIsSub)); + } + } + } + if (instantiationMapping != null && !instantiationMapping.isEmpty()) { replacer.visit(type, instantiationMapping); } else { @@ -353,6 +392,10 @@ protected abstract AnnotationMirror combine( protected abstract void replace( AnnotatedTypeMirror type, AnnotationMirrorMap replacements); + private interface PolyCollectorAnnotationCombiner { + AnnotationMirror combine(AnnotationMirror poly, AnnotationMirror a1, AnnotationMirror a2); + } + /** * A helper class that resolves the polymorphic qualifiers with the most restrictive qualifier. * It returns a mapping from the polymorphic qualifier to the substitution for that qualifier. @@ -385,10 +428,10 @@ protected AnnotationMirrorMap scanWithNull( return new AnnotationMirrorMap<>(); } - @Override - public AnnotationMirrorMap reduce( + private AnnotationMirrorMap genericReduce( AnnotationMirrorMap r1, - AnnotationMirrorMap r2) { + AnnotationMirrorMap r2, + PolyCollectorAnnotationCombiner combiner) { if (r1 == null || r1.isEmpty()) { return r2; @@ -408,7 +451,7 @@ public AnnotationMirrorMap reduce( if (a2Annos == null) { res.put(polyQual, a1Annos); } else { - res.put(polyQual, combine(polyQual, a1Annos, a2Annos)); + res.put(polyQual, combiner.combine(polyQual, a1Annos, a2Annos)); } r2remain.remove(polyQual); } @@ -418,6 +461,43 @@ public AnnotationMirrorMap reduce( return res; } + @Override + protected AnnotationMirrorMap reduce( + AnnotationMirrorMap r1, + AnnotationMirrorMap r2) { + return genericReduce(r1, r2, AbstractQualifierPolymorphism.this::combine); + } + + /** + * Reduces lower bounds r1 with upper bounds r2. + * + * @param r1 a map from {@link AnnotationMirror} to {@link AnnotationMirror} + * @param r2 a map from {@link AnnotationMirror} to {@link AnnotationMirror} + * @return the reduced {@link AnnotationMirrorMap} + */ + private AnnotationMirrorMap reduceWithUpperBounds( + AnnotationMirrorMap r1, + AnnotationMirrorMap r2) { + + // Note: a different logic is needed to reduce with assignment context. + // For arguments, @Poly is on l-value of pseudo assignment. + // But when resolving with assignment, @Poly is on r-value of assignment. + return genericReduce( + r1, + r2, + (poly, a1, a2) -> { + AnnotationMirror subres = null; + for (AnnotationMirror top : topQuals) { + if (qualHierarchy.isSubtype(a1, top)) { + subres = a1; + } else if (qualHierarchy.isSubtype(a2, top)) { + subres = a2; + } + } + return subres; + }); + } + /** * Calls {@link #visit(AnnotatedTypeMirror, AnnotatedTypeMirror)} for each type in {@code * types}. @@ -458,10 +538,12 @@ private AnnotationMirrorMap visit( * * @param type AnnotateTypeMirror used to find instantiations * @param polyType AnnotatedTypeMirror that may have polymorphic qualifiers + * @param polyIsSub boolean indicates whether {@code polyType} is the subtype of {@code + * type} * @return a mapping of polymorphic qualifiers to their instantiations */ private AnnotationMirrorMap visit( - AnnotatedTypeMirror type, AnnotatedTypeMirror polyType) { + AnnotatedTypeMirror type, AnnotatedTypeMirror polyType, boolean polyIsSub) { if (type.getKind() == TypeKind.NULL) { return mapQualifierToPoly(type, polyType); } @@ -477,19 +559,39 @@ private AnnotationMirrorMap visit( switch (polyType.getKind()) { case WILDCARD: - AnnotatedTypeMirror asSuper = - AnnotatedTypes.asSuper(atypeFactory, wildcardType, polyType); - return visit(asSuper, polyType, null); - case TYPEVAR: - return mapQualifierToPoly(wildcardType.getExtendsBound(), polyType); + if (polyIsSub) { + return visit( + wildcardType, + AnnotatedTypes.asSuper(atypeFactory, polyType, wildcardType), + null); + } else { + return visit( + AnnotatedTypes.asSuper(atypeFactory, wildcardType, polyType), + polyType, + null); + } default: return mapQualifierToPoly(wildcardType.getExtendsBound(), polyType); } } - AnnotatedTypeMirror asSuper = AnnotatedTypes.asSuper(atypeFactory, type, polyType); + if (polyIsSub) { + return visit(type, AnnotatedTypes.asSuper(atypeFactory, polyType, type), null); + } else { + return visit(AnnotatedTypes.asSuper(atypeFactory, type, polyType), polyType, null); + } + } - return visit(asSuper, polyType, null); + /** + * A helper method of {@link #visit(AnnotatedTypeMirror, AnnotatedTypeMirror, boolean)}. + * + * @param type AnnotateTypeMirror used to find instantiations + * @param polyType AnnotatedTypeMirror that may have polymorphic qualifiers + * @return {@code visit(type, polyType, false)} + */ + private AnnotationMirrorMap visit( + AnnotatedTypeMirror type, AnnotatedTypeMirror polyType) { + return visit(type, polyType, false); } @Override diff --git a/framework/src/main/java/org/checkerframework/framework/util/QualifierPolymorphismUtil.java b/framework/src/main/java/org/checkerframework/framework/util/QualifierPolymorphismUtil.java new file mode 100644 index 000000000000..6570346a9df2 --- /dev/null +++ b/framework/src/main/java/org/checkerframework/framework/util/QualifierPolymorphismUtil.java @@ -0,0 +1,361 @@ +package org.checkerframework.framework.util; + +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeVariable; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.type.visitor.AnnotatedTypeScanner; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeAnnotationUtils; + +/** + * A utility class made for {@link org.checkerframework.framework.type.poly.QualifierPolymorphism}. + */ +public class QualifierPolymorphismUtil { + + /** A set of {@code TreePath} which contains the visited {@code TreePath}s. */ + private static final Set visitedPaths = new HashSet<>(); + + /** + * Returns the annotated type that the leaf of path is assigned to, if it is within an + * assignment context. Returns the annotated type that the method invocation at the leaf is + * assigned to. If the result is a primitive, return the boxed version. + * + * @param atypeFactory the type factory + * @param path the tree path + * @return type that path leaf is assigned to + */ + public static AnnotatedTypeMirror assignedTo(AnnotatedTypeFactory atypeFactory, TreePath path) { + if (visitedPaths.contains(path)) { + // inform the caller to skip assignment context resolution + return null; + } + + visitedPaths.add(path); + + Tree assignmentContext = TreeUtils.getAssignmentContext(path); + AnnotatedTypeMirror res; + if (assignmentContext == null) { + res = null; + } else if (assignmentContext instanceof AssignmentTree) { + ExpressionTree variable = ((AssignmentTree) assignmentContext).getVariable(); + res = atypeFactory.getAnnotatedType(variable); + } else if (assignmentContext instanceof CompoundAssignmentTree) { + ExpressionTree variable = ((CompoundAssignmentTree) assignmentContext).getVariable(); + res = atypeFactory.getAnnotatedType(variable); + } else if (assignmentContext instanceof MethodInvocationTree) { + MethodInvocationTree methodInvocation = (MethodInvocationTree) assignmentContext; + ExecutableElement methodElt = TreeUtils.elementFromUse(methodInvocation); + // TODO move to getAssignmentContext + // if (methodInvocation.getMethodSelect() instanceof MemberSelectTree + // && ((MemberSelectTree) methodInvocation.getMethodSelect()).getExpression() + // == path.getLeaf()) { + // // treepath's leaf is assigned to the method declared receiver type + // AnnotatedExecutableType declMethodType = + // atypeFactory.getAnnotatedType(methodElt); + // return declMethodType.getReceiverType(); + // } // Removing above if block causes StackOverflowException via + // getReceiverType -> + // assignedTo -> getReceiverType loop! + // AnnotatedTypeMirror receiver = null; + // try { + // receiver = atypeFactory.getReceiverType(methodInvocation); + // } catch (Throwable e) { + // new Object(); + // } + AnnotatedTypeMirror receiver = atypeFactory.getReceiverType(methodInvocation); + res = + assignedToExecutable( + atypeFactory, + path, + methodElt, + receiver, + methodInvocation.getArguments()); + } else if (assignmentContext instanceof NewArrayTree) { + // TODO: I left the previous implementation below, it definitely caused infinite loops + // TODO: if you called it from places like the TreeAnnotator. + res = null; + + // TODO: This may cause infinite loop + // AnnotatedTypeMirror type = + // atypeFactory.getAnnotatedType((NewArrayTree)assignmentContext); + // type = AnnotatedTypes.innerMostType(type); + // return type; + + } else if (assignmentContext instanceof NewClassTree) { + // This need to be basically like MethodTree + NewClassTree newClassTree = (NewClassTree) assignmentContext; + ExecutableElement constructorElt = TreeUtils.constructor(newClassTree); + AnnotatedTypeMirror receiver = atypeFactory.fromNewClass(newClassTree); + res = + assignedToExecutable( + atypeFactory, + path, + constructorElt, + receiver, + newClassTree.getArguments()); + } else if (assignmentContext instanceof ReturnTree) { + HashSet kinds = new HashSet<>(Arrays.asList(Kind.LAMBDA_EXPRESSION, Kind.METHOD)); + Tree enclosing = TreeUtils.enclosingOfKind(path, kinds); + + if (enclosing.getKind() == Kind.METHOD) { + res = atypeFactory.getAnnotatedType((MethodTree) enclosing).getReturnType(); + } else { + Pair fninf = + atypeFactory.getFnInterfaceFromTree((LambdaExpressionTree) enclosing); + res = fninf.second.getReturnType(); + } + + } else if (assignmentContext instanceof VariableTree) { + res = assignedToVariable(atypeFactory, assignmentContext); + } else { + throw new BugInCF("AnnotatedTypes.assignedTo: shouldn't be here"); + } + visitedPaths.remove(path); + return res; + } + + /** + * Return the annotated type that is assigned to executable. + * + * @param atypeFactory the type factory + * @param path the tree path + * @param methodElt the method element + * @param receiver the receiver + * @param arguments the passed arguments + * @return the annotated type + */ + private static AnnotatedTypeMirror assignedToExecutable( + AnnotatedTypeFactory atypeFactory, + TreePath path, + ExecutableElement methodElt, + AnnotatedTypeMirror receiver, + List arguments) { + AnnotatedExecutableType method = + AnnotatedTypes.asMemberOf( + atypeFactory.getContext().getTypeUtils(), + atypeFactory, + receiver, + methodElt); + int treeIndex = -1; + for (int i = 0; i < arguments.size(); ++i) { + ExpressionTree argumentTree = arguments.get(i); + if (isArgument(path, argumentTree)) { + treeIndex = i; + break; + } + } + final AnnotatedTypeMirror paramType; + if (treeIndex == -1) { + // The tree wasn't found as an argument, so it has to be the receiver. + // This can happen for inner class constructors that take an outer class argument. + paramType = method.getReceiverType(); + } else if (treeIndex + 1 >= method.getParameterTypes().size() && methodElt.isVarArgs()) { + AnnotatedTypeMirror varArgType = + method.getParameterTypes().get(method.getParameterTypes().size() - 1); + List params = + AnnotatedTypes.expandVarArgs(atypeFactory, method, arguments); + if (params.get(params.size() - 1).equals(varArgType)) { + return varArgType; + } + paramType = ((AnnotatedArrayType) varArgType).getComponentType(); + } else { + paramType = method.getParameterTypes().get(treeIndex); + } + + // Examples like this: + // T outMethod() + // void inMethod(U u); + // inMethod(outMethod()) + // would require solving the constraints for both type argument inferences simultaneously + if (paramType == null || containsUninferredTypeParameter(paramType, method)) { + return null; + } + + return paramType; + } + + /** + * Returns whether argumentTree is the tree at the leaf of path. if tree is a conditional + * expression, isArgument is called recursively on the true and false expressions. + * + * @param path the tree path + * @param argumentTree the argument tree + * @return true if argumentTree is the tree at the leaf of path, else false + */ + private static boolean isArgument(TreePath path, ExpressionTree argumentTree) { + argumentTree = TreeUtils.withoutParens(argumentTree); + if (argumentTree == path.getLeaf()) { + return true; + } else if (argumentTree.getKind() == Kind.CONDITIONAL_EXPRESSION) { + ConditionalExpressionTree conditionalExpressionTree = + (ConditionalExpressionTree) argumentTree; + return isArgument(path, conditionalExpressionTree.getTrueExpression()) + || isArgument(path, conditionalExpressionTree.getFalseExpression()); + } + return false; + } + + /** + * If the variable's type is a type variable, return getAnnotatedTypeLhsNoTypeVarDefault(tree). + * Rational: + * + *

For example: + * + *

{@code
+     *  S bar () {...}
+     *
+     *  T foo(T p) {
+     *     T local = bar();
+     *     return local;
+     *   }
+     * }
+ * + * During type argument inference of {@code bar}, the assignment context is {@code local}. If + * the local variable default is used, then the type of assignment context type is + * {@code @Nullable T} and the type argument inferred for {@code bar()} is {@code @Nullable T}. + * And an incompatible types in return error is issued. + * + *

If instead, the local variable default is not applied, then the assignment context type is + * {@code T} (with lower bound {@code @NonNull Void} and upper bound {@code @Nullable Object}) + * and the type argument inferred for {@code bar()} is {@code T}. During dataflow, the type of + * {@code local} is refined to {@code T} and the return is legal. + * + *

If the assignment context type was a declared type, for example: + * + *

{@code
+     *  S bar () {...}
+     * Object foo() {
+     *     Object local = bar();
+     *     return local;
+     * }
+     * }
+ * + * The local variable default must be used or else the assignment context type is missing an + * annotation. So, an incompatible types in return error is issued in the above code. We could + * improve type argument inference in this case and by using the lower bound of {@code S} + * instead of the local variable default. + * + * @param atypeFactory AnnotatedTypeFactory + * @param assignmentContext VariableTree + * @return AnnotatedTypeMirror of Assignment context + */ + public static AnnotatedTypeMirror assignedToVariable( + AnnotatedTypeFactory atypeFactory, Tree assignmentContext) { + if (atypeFactory instanceof GenericAnnotatedTypeFactory) { + final GenericAnnotatedTypeFactory gatf = + ((GenericAnnotatedTypeFactory) atypeFactory); + return gatf.getAnnotatedTypeLhsNoTypeVarDefault(assignmentContext); + } else { + return atypeFactory.getAnnotatedType(assignmentContext); + } + } + + /** + * Check if the {@code type} contains a use of a type variable from {@code methodType}. + * + * @param type the annotated type + * @param methodType the type of an executable + * @return true if the {@code type} contains a use of a type variable from {@code methodType}. + */ + private static boolean containsUninferredTypeParameter( + AnnotatedTypeMirror type, AnnotatedExecutableType methodType) { + final List annotatedTypeVars = methodType.getTypeVariables(); + final List typeVars = new ArrayList<>(annotatedTypeVars.size()); + + for (AnnotatedTypeVariable annotatedTypeVar : annotatedTypeVars) { + typeVars.add( + (TypeVariable) + TypeAnnotationUtils.unannotatedType( + annotatedTypeVar.getUnderlyingType())); + } + + return containsTypeParameter(type, typeVars); + } + + /** + * Returns true if {@code type} contains a use of a type variable in {@code typeVariables}. + * + * @param type type to search + * @param typeVariables collection of type varibles + * @return true if {@code type} contains a use of a type variable in {@code typeVariables} + */ + public static boolean containsTypeParameter( + AnnotatedTypeMirror type, Collection typeVariables) { + // note NULL values creep in because the underlying visitor uses them in various places + final Boolean result = type.accept(new TypeVariableFinder(), typeVariables); + return result != null && result; + } + + /** + * Used to detect if the visited type contains one of the type variables in the typeVars + * parameter. + */ + private static class TypeVariableFinder + extends AnnotatedTypeScanner> { + + @Override + protected Boolean scan( + Iterable types, Collection typeVars) { + if (types == null) { + return false; + } + Boolean result = false; + Boolean first = true; + for (AnnotatedTypeMirror type : types) { + result = (first ? scan(type, typeVars) : scanAndReduce(type, typeVars, result)); + first = false; + } + return result; + } + + @Override + protected Boolean reduce(Boolean r1, Boolean r2) { + if (r1 == null) { + return r2 != null && r2; + + } else if (r2 == null) { + return r1; + } + + return r1 || r2; + } + + @Override + public Boolean visitTypeVariable( + AnnotatedTypeVariable type, Collection typeVars) { + if (typeVars.contains( + (TypeVariable) TypeAnnotationUtils.unannotatedType(type.getUnderlyingType()))) { + return true; + } else { + return super.visitTypeVariable(type, typeVars); + } + } + } +} diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference/DefaultTypeArgumentInference.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference/DefaultTypeArgumentInference.java index 35688f17eae2..3950bba76bdb 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference/DefaultTypeArgumentInference.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference/DefaultTypeArgumentInference.java @@ -139,7 +139,7 @@ public Map inferTypeArgs( if (TreeUtils.enclosingNonParen(pathToExpression).first.getKind() == Tree.Kind.LAMBDA_EXPRESSION || (assignedTo == null - && TreeUtils.getAssignmentContext(pathToExpression) != null)) { + && TreeUtils.getAssignmentContext(pathToExpression, true) != null)) { // If the type of the assignment context isn't found, but the expression is assigned, // then don't attempt to infer type arguments, because the Java type inferred will be // incorrect. The assignment type is null when it includes uninferred type arguments. diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java index 8287a330dc21..8718ceecdd44 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java @@ -151,13 +151,24 @@ public static AnnotatedTypeMirror assignedTo(AnnotatedTypeFactory atypeFactory, res = atypeFactory.getAnnotatedType(variable); } else if (assignmentContext instanceof MethodInvocationTree) { MethodInvocationTree methodInvocation = (MethodInvocationTree) assignmentContext; + ExecutableElement methodElt = TreeUtils.elementFromUse(methodInvocation); // TODO move to getAssignmentContext if (methodInvocation.getMethodSelect() instanceof MemberSelectTree && ((MemberSelectTree) methodInvocation.getMethodSelect()).getExpression() == path.getLeaf()) { + // treepath's leaf is assigned to the method declared receiver type + // AnnotatedExecutableType declMethodType = + // atypeFactory.getAnnotatedType(methodElt); + // return declMethodType.getReceiverType(); return null; - } - ExecutableElement methodElt = TreeUtils.elementFromUse(methodInvocation); + } // Removing above if block causes StackOverflowException via getReceiverType -> + // assignedTo -> getReceiverType loop! + // AnnotatedTypeMirror receiver = null; + // try { + // receiver = atypeFactory.getReceiverType(methodInvocation); + // } catch (Throwable e) { + // new Object(); + // } AnnotatedTypeMirror receiver = atypeFactory.getReceiverType(methodInvocation); res = assignedToExecutable( diff --git a/framework/tests/all-systems/GetAssignmentContext.java b/framework/tests/all-systems/GetAssignmentContext.java new file mode 100644 index 000000000000..eedae3a293cb --- /dev/null +++ b/framework/tests/all-systems/GetAssignmentContext.java @@ -0,0 +1,26 @@ +public class GetAssignmentContext { + + public A get() { + return new A(); + } + + public A get1(String a) { + return new A(); + } + + public void test() { + GetAssignmentContext t = new GetAssignmentContext(); + // get().a should be skipped + t.get1(get().a); + t.get1(get().bar()); + } + + static class A { + + String a = "1"; + + String bar(A this) { + return ""; + } + } +} diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 5fd50be43ad3..d700234df84a 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -369,7 +369,7 @@ public static Pair enclosingNonParen(final TreePath path) { } /** - * Returns the tree with the assignment context for the treePath leaf node. (Does not handle + * Returns the tree with the assignment context for the treePath leaf node. (Handles * pseudo-assignment of an argument to a parameter or a receiver expression to a receiver.) * *

The assignment context for the {@code treePath} is the leaf of its parent, if the parent @@ -394,9 +394,13 @@ public static Pair enclosingNonParen(final TreePath path) { * *

Otherwise, null is returned. * + * @param treePath the tree path + * @param conservative if conservative result is needed. Conservative result does not include + * pseudo assignment of method invocation receiver * @return the assignment context as described, {@code null} otherwise */ - public static @Nullable Tree getAssignmentContext(final TreePath treePath) { + public static @Nullable Tree getAssignmentContext( + final TreePath treePath, boolean conservative) { TreePath parentPath = treePath.getParentPath(); if (parentPath == null) { @@ -423,6 +427,41 @@ public static Pair enclosingNonParen(final TreePath path) { case RETURN: case VARIABLE: return parent; + case MEMBER_SELECT: + // since :javacutil:checkWorkingNullness seems not support assertion and living + // variable inference, assign to a local variable to pass the test + Element parentEle = TreeUtils.elementFromTree(parent); + + // Don't process method().field + if (parentEle != null && parentEle.getKind().isField()) { + return null; + } + // Also check case when treepath's leaf tree is used as method + // invocation's actual receiver + // If so, return that method invocation tree too as the assignment + // context tree rather than null as we did before + TreePath grandParentPath = parentPath.getParentPath(); + if (grandParentPath != null + && grandParentPath.getLeaf() instanceof MethodInvocationTree) { + + // do not use foo().bar() scheme as assignment context when conservative, since + // assignedTo for that is not implemented yet + if (conservative) { + MethodInvocationTree grandTree = + (MethodInvocationTree) grandParentPath.getLeaf(); + + // adapted from TypeArgInferenceUtil#assignedTo to be consistent + if (grandTree.getMethodSelect() instanceof MemberSelectTree + && ((MemberSelectTree) grandTree.getMethodSelect()).getExpression() + == treePath.getLeaf()) { + return null; + } + } + return grandParentPath.getLeaf(); + } else { + return null; + } + default: // 11 Tree.Kinds are CompoundAssignmentTrees, // so use instanceof rather than listing all 11. @@ -433,6 +472,16 @@ public static Pair enclosingNonParen(final TreePath path) { } } + /** + * See {@code getAssignmentContext}. Not using conservative result. + * + * @param treePath the tree path + * @return the assignment context as described, {@code null} otherwise + */ + public static @Nullable Tree getAssignmentContext(final TreePath treePath) { + return getAssignmentContext(treePath, false); + } + /** * Gets the {@link Element} for the given Tree API node. For an object instantiation returns the * value of the {@link JCNewClass#constructor} field. Note that this result might differ from