diff --git a/checker/tests/nullness/java8/Issue496.java b/checker/tests/nullness/java8/Issue496.java index 071be672b14f..11fae06d0d8a 100644 --- a/checker/tests/nullness/java8/Issue496.java +++ b/checker/tests/nullness/java8/Issue496.java @@ -2,6 +2,7 @@ // https://github.com/typetools/checker-framework/issues/496 import java.util.Optional; +import org.checkerframework.dataflow.qual.SideEffectFree; public class Issue496 { @@ -9,6 +10,9 @@ public static class Entity { public final T value; public final Class cls; + // Optional.map is @SideEffectFree, so the mapper below must be too, and calling this + // constructor is side-effect-free only if it says so. + @SideEffectFree public Entity(T value, Class cls) { this.value = value; this.cls = cls; diff --git a/checker/tests/optional/LambdaMethodCallValue.java b/checker/tests/optional/LambdaMethodCallValue.java index 1802baf88620..e3b8cd12dd39 100644 --- a/checker/tests/optional/LambdaMethodCallValue.java +++ b/checker/tests/optional/LambdaMethodCallValue.java @@ -64,6 +64,34 @@ void test7(OptContainer container) { } } + // A lambda's body may call a functional-interface parameter of the enclosing method. The + // caller of that method was required to pass pure code, so the call does not discard + // refinements. (This test does not use -AcheckPurityAnnotations, so the bodies below are not + // checked against their purity annotations.) + @Pure + String test8(OptContainer container, List strs, Consumer op) { + if (container.getOpt().isPresent()) { + strs.forEach( + s -> { + container.getOpt().get(); // OK + op.accept(s); + }); + } + return ""; + } + + /** An unannotated method promises nothing about the code it is passed. */ + void test9(OptContainer container, List strs, Consumer op) { + if (container.getOpt().isPresent()) { + strs.forEach( + s -> { + // :: error: [method.invocation] + container.getOpt().get(); // Not ok: the lambda body is not pure + op.accept(s); + }); + } + } + class OptContainer { @SuppressWarnings("optional:field") // Don't care about this warning, unrelated to the test case diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java index a3a2c9f32e26..e565665fbad1 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java @@ -23,17 +23,24 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; +import javax.annotation.processing.ProcessingEnvironment; +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.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Deterministic; import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.dataflow.qual.SideEffectsOnly; import org.checkerframework.javacutil.AnnotationProvider; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; /** * A visitor that determines the purity (as defined by {@link @@ -65,6 +72,16 @@ private PurityChecker() { * * @param statement the statement to check * @param annoProvider the annotation provider + * @param enclosingMethod the method declaration that lexically encloses {@code statement}, or + * null if none does. A call to the functional method of one of that method's + * functional-interface parameters has the purity that the method's callers required of the + * argument; see {@link #functionalParameterCallPurity(AnnotationProvider, ExpressionTree, + * ExecutableElement, MethodTree, ProcessingEnvironment)}. Pass the enclosing method for a + * lambda body too: the body is checked against the functional method that the lambda + * implements, but the enclosing method's parameters still hold values that its caller was + * required to check, whenever the lambda runs. Pass null for an arbitrary expression, which + * no method's contract governs. + * @param env the processing environment; used only if {@code enclosingMethod} is non-null * @param assumeSideEffectFree true if all methods should be assumed to be @SideEffectFree * @param assumeDeterministic true if all methods should be assumed to be @Deterministic * @param assumePureGetters true if all getter methods should be assumed to be @Pure @@ -74,12 +91,16 @@ private PurityChecker() { public static PurityResult checkPurity( TreePath statement, AnnotationProvider annoProvider, + @Nullable MethodTree enclosingMethod, + @Nullable ProcessingEnvironment env, boolean assumeSideEffectFree, boolean assumeDeterministic, boolean assumePureGetters) { return checkPurity( Collections.singletonList(statement), annoProvider, + enclosingMethod, + env, assumeSideEffectFree, assumeDeterministic, assumePureGetters); @@ -89,13 +110,17 @@ public static PurityResult checkPurity( * Compute whether the given statements, taken together, are side-effect-free, deterministic, or * both. Returns a result that can be queried. * - *

Use this rather than calling {@link #checkPurity(TreePath, AnnotationProvider, boolean, - * boolean, boolean)} once per statement, for code that runs as a unit but is not contiguous in - * the source code: a constructor together with the instance initializers that run as part of it, - * for example. + *

Use this rather than calling {@link #checkPurity(TreePath, AnnotationProvider, MethodTree, + * ProcessingEnvironment, boolean, boolean, boolean)} once per statement, for code that runs as a + * unit but is not contiguous in the source code: a constructor together with the instance + * initializers that run as part of it, for example. * * @param statements the statements to check * @param annoProvider the annotation provider + * @param enclosingMethod the method declaration that lexically encloses {@code statements}, or + * null if none does; see {@link #checkPurity(TreePath, AnnotationProvider, MethodTree, + * ProcessingEnvironment, boolean, boolean, boolean)} + * @param env the processing environment; used only if {@code enclosingMethod} is non-null * @param assumeSideEffectFree true if all methods should be assumed to be @SideEffectFree * @param assumeDeterministic true if all methods should be assumed to be @Deterministic * @param assumePureGetters true if all getter methods should be assumed to be @Pure @@ -105,18 +130,265 @@ public static PurityResult checkPurity( public static PurityResult checkPurity( List statements, AnnotationProvider annoProvider, + @Nullable MethodTree enclosingMethod, + @Nullable ProcessingEnvironment env, boolean assumeSideEffectFree, boolean assumeDeterministic, boolean assumePureGetters) { PurityCheckerHelper helper = new PurityCheckerHelper( - annoProvider, assumeSideEffectFree, assumeDeterministic, assumePureGetters); + annoProvider, + enclosingMethod, + env, + assumeSideEffectFree, + assumeDeterministic, + assumePureGetters); for (TreePath statement : statements) { helper.scan(statement, null); } return helper.purityResult; } + /** + * Returns the kinds of purity that a method promises for the functional method of its + * functional-interface parameters. At a call to {@code method}, each argument passed to a + * functional-interface parameter is required to have those kinds, so within the body the + * parameters may be assumed to have them. + * + * @param annoProvider the annotation provider + * @param method a method or constructor, or null + * @return the purity kinds that {@code method} requires of its functional-interface arguments; + * empty if {@code method} is null or has no purity annotation + */ + public static EnumSet functionalParameterKinds( + AnnotationProvider annoProvider, @Nullable ExecutableElement method) { + if (method == null) { + return EnumSet.noneOf(PurityKind.class); + } + EnumSet result = EnumSet.copyOf(PurityUtils.getPurityKinds(annoProvider, method)); + if (annoProvider.getDeclAnnotation(method, SideEffectsOnly.class) != null) { + // A @SideEffectsOnly method may modify the listed expressions, but the code it is handed + // may not modify anything. + result.add(PurityKind.SIDE_EFFECT_FREE); + } + return result; + } + + /** + * Returns the purity that a call of {@code invoked} on {@code expr} may be assumed to have beyond + * {@code invoked}'s own annotations, if {@code expr} is an effectively final functional-interface + * parameter of {@code method} and {@code invoked} is the functional method of {@code expr}'s type + * (or overrides it); otherwise returns an empty set. See {@link #functionalParameterPurity( + * AnnotationProvider, ExecutableElement, int, ProcessingEnvironment)}. + * + *

The code that such an expression denotes has the purity that {@code method} promises, + * because at every call to {@code method} the argument was required to have it. Only the + * functional method carries that guarantee; a default method such as {@code Function.andThen} + * does not. + * + * @param annoProvider the annotation provider + * @param expr an expression, or null + * @param invoked a method that {@code expr} is used to invoke or to refer to + * @param method a method or constructor declaration, or null + * @param env the processing environment + * @return the purity that the call may be assumed to have, or an empty set + */ + public static EnumSet functionalParameterCallPurity( + AnnotationProvider annoProvider, + @Nullable ExpressionTree expr, + ExecutableElement invoked, + @Nullable MethodTree method, + ProcessingEnvironment env) { + VariableElement parameter = effectivelyFinalParameter(expr, method); + if (parameter == null) { + return EnumSet.noneOf(PurityKind.class); + } + ExecutableElement parameterFunction = functionalMethodOfType(parameter.asType(), env); + if (parameterFunction == null || !isSameOrOverrides(invoked, parameterFunction, env)) { + return EnumSet.noneOf(PurityKind.class); + } + assert method != null + : "@AssumeAssertion(nullness): effectivelyFinalParameter returns null if method is null"; + ExecutableElement methodElt = TreeUtils.elementFromDeclaration(method); + assert methodElt != null + : "@AssumeAssertion(nullness): method has been entered, since its parameter has an element"; + return functionalParameterPurity( + annoProvider, methodElt, methodElt.getParameters().indexOf(parameter), env); + } + + /** + * Returns the purity that the argument passed to {@code method}'s parameter at {@code index} is + * known to have, beyond the annotations on the functional method of the parameter's type. It is + * empty if that parameter's type is not a functional interface. + * + *

A call reaches {@code method} through its own declaration or through any declaration that it + * overrides, and each call checks its arguments against the declaration that it invokes; see + * {@link #functionalParameterKinds}. A call through an overridden declaration that requires less + * of the argument also relies on less: it does not rely on purity that the declaration does not + * promise. The exception is an overridden declaration that promises purity but whose parameter at + * {@code index} does not have the functional method of {@code method}'s parameter: either it is + * not a functional interface, as when the overridden parameter's type is a type variable that the + * overriding class instantiates with a functional interface, or its functional method is one that + * the functional method of {@code method}'s parameter does not override, as when the overriding + * class instantiates the type variable with a functional subinterface that declares an unrelated + * functional method. A call through such a declaration relies on the promise without checking the + * code that {@code method}'s parameter runs, so the result excludes the purity that it promises. + * + *

Code that calls the functional method of a functional-interface parameter relies on the + * purity of that call even when the functional method promises none. Such code may run {@code + * method} with an argument that no declaration checked, so {@code BaseTypeVisitor} checks the + * code that is passed as a functional argument against the result of this method. + * + * @param annoProvider the annotation provider + * @param method a method or constructor + * @param index the index of one of {@code method}'s formal parameters + * @param env the processing environment + * @return the purity that the argument at {@code index} is known to have + */ + public static EnumSet functionalParameterPurity( + AnnotationProvider annoProvider, + ExecutableElement method, + int index, + ProcessingEnvironment env) { + ExecutableElement parameterFunction = parameterFunctionalMethod(method, index, env); + if (parameterFunction == null) { + return EnumSet.noneOf(PurityKind.class); + } + EnumSet result = functionalParameterKinds(annoProvider, method); + for (ExecutableElement overridden : + ElementUtils.getOverriddenMethods(method, env.getTypeUtils())) { + if (result.isEmpty()) { + break; + } + ExecutableElement overriddenFunction = parameterFunctionalMethod(overridden, index, env); + if (overriddenFunction == null + || !isSameOrOverrides(parameterFunction, overriddenFunction, env)) { + result.removeAll(functionalParameterKinds(annoProvider, overridden)); + } + } + return result; + } + + /** + * Returns the functional method of the type of {@code method}'s formal parameter at {@code + * index}, or null if that parameter does not exist or its type is not a functional interface. A + * varargs parameter's type is an array, which is not. + * + * @param method a method or constructor + * @param index the index of a formal parameter + * @param env the processing environment + * @return the functional method of the parameter's type, or null + */ + private static @Nullable ExecutableElement parameterFunctionalMethod( + ExecutableElement method, int index, ProcessingEnvironment env) { + List parameters = method.getParameters(); + if (index < 0 || index >= parameters.size()) { + return null; + } + return functionalMethodOfType(parameters.get(index).asType(), env); + } + + /** + * Returns the functional method of {@code type}, or null if {@code type} is not a functional + * interface type; see {@link #functionalInterfaceType}. + * + * @param type a type + * @param env the processing environment + * @return the functional method of {@code type}, or null + */ + private static @Nullable ExecutableElement functionalMethodOfType( + TypeMirror type, ProcessingEnvironment env) { + TypeMirror functionalType = functionalInterfaceType(type, env); + return functionalType == null ? null : TypesUtils.findFunction(functionalType, env); + } + + /** + * Returns true if {@code method} is {@code other} or overrides it. + * + * @param method a method + * @param other another method + * @param env the processing environment + * @return true if {@code method} is {@code other} or overrides it + */ + public static boolean isSameOrOverrides( + ExecutableElement method, ExecutableElement other, ProcessingEnvironment env) { + return method.equals(other) + || env.getElementUtils() + .overrides(method, other, (TypeElement) method.getEnclosingElement()); + } + + /** + * Returns the parameter that {@code expr} denotes, if {@code expr} is an effectively final formal + * parameter of {@code method}; otherwise returns null. + * + *

The expression must be the parameter itself. A local variable that aliases it, a field that + * holds it, or the result of a call does not qualify: only the parameter is known to hold the + * value that the caller supplied. The parameter must be effectively final because a body that + * reassigns it no longer holds that value. + * + * @param expr an expression, or null + * @param method a method or constructor declaration, or null + * @return the parameter that {@code expr} denotes, or null + */ + private static @Nullable VariableElement effectivelyFinalParameter( + @Nullable ExpressionTree expr, @Nullable MethodTree method) { + if (expr == null + || method == null + || !(TreeUtils.withoutParens(expr) instanceof IdentifierTree id)) { + return null; + } + Element element = TreeUtils.elementFromTree(id); + if (element == null + || element.getKind() != ElementKind.PARAMETER + || !ElementUtils.isEffectivelyFinal(element)) { + return null; + } + // Test membership in the parameter list rather than the enclosing element, because javac + // gives a lambda's parameters the enclosing method as their enclosing element. A lambda's + // parameter is not checked at any call site. + if (!isParameterOf(element, method)) { + return null; + } + return (VariableElement) element; + } + + /** + * Returns true if {@code element} is one of {@code method}'s formal parameters. + * + * @param element an element + * @param method a method or constructor declaration + * @return true if {@code element} is a formal parameter of {@code method} + */ + @SuppressWarnings("interning:not.interned") // Checking for exact object. + private static boolean isParameterOf(Element element, MethodTree method) { + for (VariableTree parameter : method.getParameters()) { + if (TreeUtils.elementFromDeclaration(parameter) == element) { + return true; + } + } + return false; + } + + /** + * Returns {@code type}, or its upper bound if it is a type variable, if that is a functional + * interface type; otherwise returns null. Use {@link TypesUtils#findFunction} on the result to + * obtain the functional method. + * + * @param type a type + * @param env the processing environment + * @return the functional interface type that {@code type} denotes, or null + */ + public static @Nullable TypeMirror functionalInterfaceType( + TypeMirror type, ProcessingEnvironment env) { + if (type.getKind() == TypeKind.TYPEVAR) { + type = TypesUtils.upperBound(type); + } + if (type.getKind() != TypeKind.DECLARED && type.getKind() != TypeKind.INTERSECTION) { + return null; + } + return TypesUtils.isFunctionalInterface(type, env) ? type : null; + } + /** * Result of the {@link PurityChecker}. Can be queried regarding whether a given tree was * side-effect-free, deterministic, or both; also gives reasons if the answer is "no". @@ -261,6 +533,20 @@ protected static class PurityCheckerHelper extends TreePathScanner { /** The annotation provider (typically an AnnotatedTypeFactory). */ protected final AnnotationProvider annoProvider; + /** The method declaration that lexically encloses the checked statement, or null if none. */ + private final @Nullable MethodTree enclosingMethod; + + /** The processing environment; null if {@link #enclosingMethod} is null. */ + private final @Nullable ProcessingEnvironment env; + + /** + * The purity that {@link #enclosingMethod} promises for the functional method of its + * functional-interface parameters, before accounting for the methods that it overrides. Empty + * if there is no enclosing method or it has no purity annotation, in which case no call gets + * the assumption. + */ + private final EnumSet functionalParameterKinds; + /** * True if all methods should be assumed to be @SideEffectFree, for the purposes of * org.checkerframework.dataflow analysis. @@ -283,21 +569,55 @@ protected static class PurityCheckerHelper extends TreePathScanner { * Create a PurityCheckerHelper. * * @param annoProvider the annotation provider + * @param enclosingMethod the method declaration that lexically encloses the checked statement, + * or null if none does + * @param env the processing environment; used only if {@code enclosingMethod} is non-null * @param assumeSideEffectFree true if all methods should be assumed to be @SideEffectFree * @param assumeDeterministic true if all methods should be assumed to be @Deterministic * @param assumePureGetters true if getter methods should be assumed to be @Pure */ public PurityCheckerHelper( AnnotationProvider annoProvider, + @Nullable MethodTree enclosingMethod, + @Nullable ProcessingEnvironment env, boolean assumeSideEffectFree, boolean assumeDeterministic, boolean assumePureGetters) { this.annoProvider = annoProvider; + this.enclosingMethod = enclosingMethod; + this.env = env; + this.functionalParameterKinds = + enclosingMethod == null + ? EnumSet.noneOf(PurityKind.class) + : functionalParameterKinds( + annoProvider, TreeUtils.elementFromDeclaration(enclosingMethod)); this.assumeSideEffectFree = assumeSideEffectFree; this.assumeDeterministic = assumeDeterministic; this.assumePureGetters = assumePureGetters; } + /** + * Returns the purity that {@code tree} may be assumed to have beyond {@code invoked}'s own + * annotations: if {@code tree} invokes the functional method of an effectively final + * functional-interface parameter of the method being checked, the purity that every call to the + * method required of the argument; otherwise, an empty set. + * + * @param tree a method invocation + * @param invoked the invoked method + * @return the purity that a call to a functional-interface parameter of the enclosing method + * may be assumed to have, or an empty set + */ + private EnumSet functionalParameterCallPurity( + MethodInvocationTree tree, ExecutableElement invoked) { + ProcessingEnvironment env = this.env; + if (functionalParameterKinds.isEmpty() || env == null) { + // There is no enclosing method, or it promises nothing. + return EnumSet.noneOf(PurityKind.class); + } + return PurityChecker.functionalParameterCallPurity( + annoProvider, TreeUtils.getReceiverTree(tree), invoked, enclosingMethod, env); + } + @Override public Void visitCatch(CatchTree tree, Void ignore) { purityResult.addNotDetReason(tree, "catch"); @@ -374,6 +694,12 @@ public Void visitClass(ClassTree tree, Void ignore) { public Void visitMethodInvocation(MethodInvocationTree tree, Void ignore) { ExecutableElement elt = TreeUtils.elementFromUse(tree); EnumSet eltPurityKinds = PurityUtils.getPurityKinds(annoProvider, elt); + EnumSet parameterPurity = functionalParameterCallPurity(tree, elt); + if (!parameterPurity.isEmpty()) { + eltPurityKinds = EnumSet.copyOf(eltPurityKinds); + eltPurityKinds.addAll(parameterPurity); + } + boolean pureGetter = assumePureGetters && ElementUtils.isGetter(elt); boolean seFree = assumeSideEffectFree diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1a8ba768e808..171a4c95842e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -38,6 +38,10 @@ single-page version, there is a ### Changes for type system implementers +`PurityChecker.checkPurity()` takes two new arguments: the method declaration +that lexically encloses the statement being checked (or null, as for an +arbitrary expression) and the processing environment. + `JavaParserUtil`: moved `DEFAULT_LANGUAGE_LEVEL`, `parseCompilationUnit()`, `parseStubUnit()`, and `parseExpression()` into new class `StaticJavaParserUtil`. diff --git a/docs/manual/purity-checker.tex b/docs/manual/purity-checker.tex index 65f58a544127..60ebaffc6ed1 100644 --- a/docs/manual/purity-checker.tex +++ b/docs/manual/purity-checker.tex @@ -1,21 +1,29 @@ \chapterAndLabel{Purity Checker}{purity-checker} -The Purity Checker identifies methods that have no side effects, that -return the same value each time they are called on the same argument, or both. +A \emph{side-effect-free} method is one that has no externally-visible side +effects: it changes no object that exists when it is called, except in ways +that no caller can observe, such as updating a private cache. +A \emph{deterministic} method returns the same value each time it is called +on the same arguments. +A \emph{pure} method is both side-effect-free and deterministic. +The Purity Checker can check and infer all three types of methods, +and ``purity'' refers to all concepts. -Purity analysis aids type refinement (Section~\ref{type-refinement}). +All checkers utilize purity annotations to aid type refinement +(Section~\ref{type-refinement}). By default, purity annotations are +trusted, not checked. -All checkers utilize purity annotations on called methods. -You do not need to run the Purity Checker directly. -However, you may run just the Purity Checker by -supplying the following command-line option to javac: +To check purity annotations, pass \<-AcheckPurityAnnotations> to any +checker. +To run \emph{only} the Purity Checker (this is uncommon), +supply the following command-line option to javac: \code{-processor org.checkerframework.framework.util.PurityChecker}. That implies \<-AcheckPurityAnnotations>, so you do not need to supply both options. -The Checker Framework can infer purity annotations. -If you supply the command-line option \<-AsuggestPureMethods>, then the -Checker Framework will suggest methods that can be marked as +To infer purity annotations, +supply the command-line option \<-AsuggestPureMethods>. +The Checker Framework will suggest methods that can be marked as \<@SideEffectFree>, \<@Deterministic>, or \<@Pure>. In addition, such suggestions are output by \<-Ainfer> and when using whole-program inference. @@ -44,10 +52,167 @@ \end{description} +\sectionAndLabel{Lambdas, method references, and functional interfaces}{purity-functional-interfaces} + +Creating a lambda, or declaring a local or anonymous class, is not itself a +side effect. A lambda expression evaluates to an object but does not run the code +in the lambda's body. A side-effect-free method may therefore create a lambda +whose body has side effects: + +\begin{Verbatim} + @SideEffectFree + Runnable makeRunnable() { + return () -> count++; // OK: creating the lambda has no side effect + } +\end{Verbatim} + +\noindent +The effects of the body occur where the functional method is invoked, and +each such invocation is checked on its own. + +The body of a lambda is checked against the purity annotations on the +functional interface method that the lambda implements, not against those on +the method that creates the lambda. The errors shown in this section are +reported only when purity annotations are checked, such as with +\<-AcheckPurityAnnotations>; they are not reported by default or by +\<-Ainfer>. For example: + +\begin{Verbatim} + @FunctionalInterface + interface PureSupplier { + @SideEffectFree String get(); + } + + PureSupplier s1 = () -> "hello"; // OK + PureSupplier s2 = () -> { count++; return ""; }; // error: the body has a side effect +\end{Verbatim} + + +\subsectionAndLabel{Functional-interface parameters of methods with purity annotations}{purity-functional-interface-parameters} + +A method annotated with a purity annotation may call the functional method of +its own functional-interface parameters. Such a call is treated as having the +purity that the method promises: + +\begin{itemize} +\item + In a \refqualclass{dataflow/qual}{SideEffectFree} method, the call is + treated as side-effect-free. +\item + In a \refqualclass{dataflow/qual}{Deterministic} method, the call is + treated as deterministic. +\item + In a \refqualclass{dataflow/qual}{Pure} method, the call is treated as + pure. +\iffalse +\item + In a \refqualclass{dataflow/qual}{SideEffectsOnly} method, the call is + treated as side-effect-free. The method itself may modify the expressions + listed in its annotation, but the code it is handed may not modify anything. +\fi +\end{itemize} + +\begin{Verbatim} + @SideEffectFree + int m(Function f, String s) { + return f.apply(s); // permitted: apply is treated as side-effect-free + } +\end{Verbatim} + +The assumption applies only when the parameter is effectively final and the +functional method is called directly on the parameter. It does not apply to a +call through a local variable or field that aliases the parameter, nor to a +method of the parameter's type other than its functional method, such as +\: + +\begin{Verbatim} + @SideEffectFree + int m2(Function f, String s) { + Function g = f; + return g.apply(s); // error: g is not a parameter + } +\end{Verbatim} + +The assumption also does not apply if the method overrides a method that +makes the same promise but whose parameter at that position has a type +variable as its type, because a call through the overridden method relies on +the promise without checking the argument when the type variable is not +instantiated with a functional interface at the call. An overridden method that makes +no promise does not limit the assumption: a call through it may pass an +argument with side effects, but that call does not rely on the purity of the +method either. + +The exception is code that calls the functional method of one of its own +functional-interface parameters, which relies on the call's purity even if +the functional method makes no promise. Suppose that class \ implements +interface \, and \ is \<@SideEffectFree> but \ is not. +Then passing a \ where an \ is required, to a parameter of a +\<@SideEffectFree> method, is an error if \ assumes that its own +functional-interface argument is side-effect-free, because a call through +\ does not check that argument. The same holds for a method +reference to \, which is also an error wherever the functional +method that it implements makes the promise without checking the argument. + +At each call to such a method, every argument passed to a functional-interface +parameter must have the purity that the method promises (side-effect-free for +a \<@SideEffectsOnly> method). For a varargs parameter whose element type is +a functional interface, such as \, each argument that the call +passes as an element of the array is checked, as is each initializer of an +array literal such as \ that the call passes. A +parameter whose type is a type variable is a functional-interface parameter +at a call that instantiates the type variable with a functional interface. +For \ in \<@SideEffectFree int apply(T t)> in \code{interface I}, a +call through \code{I>} checks its argument against +\. Likewise, a call \code{List.of(() -> count++)} +that instantiates \ in \ with \ checks its argument against +\: a method that is passed a functional-interface argument is +assumed to call it. How the argument is checked depends on its form: + +\begin{itemize} +\item + A method reference is checked via the referenced method, which must have + the required purity annotations. +\item + A lambda is checked via its body, which must have the required purity. +\item + Any other argument is checked via the functional method of the argument's + declared type, which must have the required purity annotations. +\end{itemize} + +\begin{Verbatim} + m(String::length, "hi"); // OK: String.length() is side-effect-free + m(s -> s.length(), "hi"); // OK: the lambda body is side-effect-free + m(s -> count++, "hi"); // error: the lambda body has a side effect + + void caller(Function g) { + m(g, "hi"); // error: [purity.functional.argument] Function.apply is not @SideEffectFree + } +\end{Verbatim} + +If the functional method returns \, a \<@Pure> method requires only +that the argument be side-effect-free, because a side-effect-free call that +returns no value is deterministic. + +%% TODO: we could implement a type system for functional interfaces with a +%% qualifier \<@SideEffectFreeFunctionalInterface>. Then the default for a +%% \<@SideEffectFree> method would be: +%For a side-effect-free method, the default type is +%\<@SideEffectFreeFunctionalInterface> for any parameter that is a functional +%interface. That is, the following two declarations are equivalent: +% +%\begin{Verbatim} +% @SideEffectFree +% int apply(Function f, String s) { ... } +% +% @SideEffectFree +% int apply(@SideEffectFreeFunctionalInterface Function f, String s) { ... } +%\end{Verbatim} + + \sectionAndLabel{Purity annotations are trusted}{purity-trusted} -By default, purity annotations are trusted. Purity annotations on called -methods affect type-checking of client code. However, you can make a +By default, purity annotations are trusted by every checker built with the +Checker Framework, and the Purity Checker is not run. However, you can make a mistake % by writing \refqualclass{dataflow/qual}{SideEffectFree} on the @@ -141,5 +306,6 @@ \end{itemize} -%% LocalWords: AsuggestPureMethods AcheckPurityAnnotations -%% LocalWords: AsuppressWarnings AassumeSideEffectFree +%% LocalWords: AsuggestPureMethods AcheckPurityAnnotations Ainfer +%% LocalWords: AsuppressWarnings AassumeSideEffectFree SideEffectsOnly +% LocalWords: TODO AassumeDeterministic AassumePure AassumePureGetters diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index c98386234127..4657295e502e 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -71,10 +71,12 @@ import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; import javax.tools.Diagnostic; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.interning.qual.FindDistinct; @@ -1157,12 +1159,22 @@ protected boolean shouldPerformContractInference() { * * @param code the paths to the code that runs as part of the method or lambda: its body and, for * a constructor, the instance initializers it runs; empty if the method has no body + * @param enclosingMethod the method declaration that lexically encloses {@code code}, or null if + * none does * @return the purity of {@code code}, ignoring the {@code -Aassume*} command-line options */ - private PurityResult purityForInference(List code) { + private PurityResult purityForInference( + List code, @Nullable MethodTree enclosingMethod) { return code.isEmpty() ? new PurityResult() - : PurityChecker.checkPurity(code, atypeFactory, false, false, false); + : PurityChecker.checkPurity( + code, + atypeFactory, + enclosingMethod, + atypeFactory.getProcessingEnv(), + false, + false, + false); } /** @@ -1241,6 +1253,8 @@ protected void checkPurityAnnotations(MethodTree tree) { : PurityChecker.checkPurity( toCheck, atypeFactory, + tree, + atypeFactory.getProcessingEnv(), assumeSideEffectFree, assumeDeterministic, assumePureGetters); @@ -1251,7 +1265,7 @@ protected void checkPurityAnnotations(MethodTree tree) { if (suggestPureMethods && !TreeUtils.isSynthetic(tree)) { // Issue a warning if the method is pure, but not annotated as such. - EnumSet additionalKinds = purityForInference(toCheck).getKinds().clone(); + EnumSet additionalKinds = purityForInference(toCheck, tree).getKinds().clone(); if (!infer) { // During WPI, propagate all purity kinds, even those that are already // present (because they were inferred in a previous WPI round). @@ -1263,6 +1277,11 @@ protected void checkPurityAnnotations(MethodTree tree) { if (infer) { WholeProgramInference wpi = atypeFactory.getWholeProgramInference(); ExecutableElement methodElt = TreeUtils.elementFromDeclaration(tree); + if (wpi == null || methodElt == null) { + throw new BugInCF("No WPI or no element while inferring purity of " + tree); + } + // Do not infer a kind that would newly constrain the arguments at call sites. + additionalKinds = kindsSafeToInfer(methodElt, additionalKinds); inferPurityAnno(additionalKinds, wpi, methodElt); // The purity of overridden methods is impacted by the purity of this method. If // a superclass method is pure, but an implementation in a subclass is not, WPI @@ -1630,6 +1649,600 @@ private enum PurityViolation { } } + /** + * If the invoked method or constructor has a purity annotation, checks each argument that is + * passed to a functional-interface parameter: the code that the argument denotes must have the + * purity that the callee requires of it, because the callee's body is permitted to call it. + * + *

How the argument is checked depends on its form. The body of a lambda is checked directly. A + * method reference is checked against the declaration of the method it refers to. A lambda or + * method reference whose functional method does not override the parameter's, because its type is + * a functional subinterface that declares an unrelated functional method, is checked like any + * other argument, since the subinterface's implementation of the parameter's functional method is + * what runs. Each result expression of a conditional or switch expression is checked on its own, + * because the type of such an expression is the parameter's type and says nothing about the code + * that any result expression denotes. An effectively final functional-interface parameter of the + * enclosing method needs no check, since the caller of that method already performed one. Each + * initializer of an array literal is checked on its own, because such an argument is the array + * that a varargs call passes in its array form. Any other argument is checked against the + * functional method of its declared type. A cast of an argument that denotes code is ignored; see + * {@link #withoutCastsOfCode}. + * + *

This check is skipped for a requirement that the parameter's own functional method already + * makes, because {@link #checkLambdaPurity} and {@link + * BaseTypeVisitor.OverrideChecker#checkMethodReferencePurity} check that. + * + *

A parameter whose declared type is a type variable is checked against the functional method + * of its type at the call site, in which the receiver's type arguments and the callee's inferred + * or explicit type arguments are substituted for the type variables. A call through {@code + * I>} of {@code @SideEffectFree int apply(T t)} may run an implementation whose + * parameter is a {@code Supplier} and that calls it. A callee that receives a + * functional-interface argument through a type variable of its own, such as {@code List.of(E)}, + * is assumed to call it, too. + * + * @param callee the invoked method or constructor + * @param calleeType the type of {@code callee} at the call site, or null if it is not known + * @param args the arguments to {@code callee} + * @param varargsCall true if {@code args} are in the expanded form of a varargs call, so that the + * arguments from the last parameter onward are elements of its array + */ + protected void checkFunctionalArguments( + ExecutableElement callee, + @Nullable AnnotatedExecutableType calleeType, + List args, + boolean varargsCall) { + if (!checkPurityAnnotations || infer) { + return; + } + EnumSet calleeKinds = PurityChecker.functionalParameterKinds(atypeFactory, callee); + if (calleeKinds.isEmpty()) { + return; + } + List params = callee.getParameters(); + int numParams = params.size(); + if (numParams == 0) { + return; + } + // In the expanded form of a varargs call, every argument from the last parameter onward is + // an element of that parameter's array. Otherwise each argument is passed to the parameter + // at its own index; the counts can still differ, as for an enum constructor, whose two + // parameters no call passes. + List paramTypesAtCall = + calleeType == null || calleeType.getParameterTypes().size() != numParams + ? null + : calleeType.getParameterTypes(); + int numToCheck = varargsCall ? args.size() : Math.min(numParams, args.size()); + for (int i = 0; i < numToCheck; i++) { + int paramIndex = Math.min(i, numParams - 1); + ExecutableElement paramFunction = parameterFunctionalMethod(callee, paramIndex); + if (paramFunction == null + && paramTypesAtCall != null + && parameterArgumentType(callee, paramIndex, null).getKind() == TypeKind.TYPEVAR) { + paramFunction = + functionalMethodOfType( + parameterArgumentType( + callee, paramIndex, paramTypesAtCall.get(paramIndex).getUnderlyingType())); + } + if (paramFunction == null) { + continue; + } + EnumSet required = purityRequiredOfArgument(paramFunction, calleeKinds, true); + if (required.isEmpty()) { + continue; + } + checkFunctionalArgument(args.get(i), required, paramFunction, params.get(paramIndex), callee); + } + } + + /** + * Returns the functional method of the type of {@code method}'s parameter at {@code index}, or + * null if that type is not a functional interface. For a varargs parameter, this is the + * functional method of the array's component type, which is the type of each argument that a call + * passes to it. + * + * @param method a method or constructor + * @param index the index of one of {@code method}'s formal parameters + * @return the functional method of that parameter's type, or null + */ + private @Nullable ExecutableElement parameterFunctionalMethod( + ExecutableElement method, int index) { + return functionalMethodOfType(parameterArgumentType(method, index, null)); + } + + /** + * Returns the type of each argument that a call passes to {@code method}'s parameter at {@code + * index}: the parameter's type, or for a varargs parameter the array's component type. + * + * @param method a method or constructor + * @param index the index of one of {@code method}'s formal parameters + * @param paramType the type of that parameter at a call site, or null to use its declared type + * @return the type of each argument passed to that parameter + */ + private static TypeMirror parameterArgumentType( + ExecutableElement method, int index, @Nullable TypeMirror paramType) { + if (paramType == null) { + paramType = method.getParameters().get(index).asType(); + } + if (method.isVarArgs() + && index == method.getParameters().size() - 1 + && paramType.getKind() == TypeKind.ARRAY) { + paramType = ((ArrayType) paramType).getComponentType(); + } + return paramType; + } + + /** + * Returns the functional method of {@code type}, or null if {@code type} is not a functional + * interface. + * + * @param type a type + * @return the functional method of {@code type}, or null + */ + private @Nullable ExecutableElement functionalMethodOfType(TypeMirror type) { + ProcessingEnvironment env = atypeFactory.getProcessingEnv(); + TypeMirror functionalType = PurityChecker.functionalInterfaceType(type, env); + return functionalType == null ? null : TypesUtils.findFunction(functionalType, env); + } + + /** + * Returns the purity that a method whose purity is {@code methodKinds} requires of the argument + * that is passed to a functional-interface parameter whose functional method is {@code + * paramFunction}: the kinds that {@code paramFunction} does not already promise, less determinism + * when the argument is a side-effect-free method that returns no value. + * + * @param paramFunction the functional method of a parameter's type + * @param methodKinds the purity that a method requires of its functional-interface arguments + * @param applyAssumptions whether to apply the {@code -AassumeSideEffectFree} and {@code + * -AassumePure} command-line options + * @return the purity required of the argument, which may be empty + */ + private EnumSet purityRequiredOfArgument( + ExecutableElement paramFunction, EnumSet methodKinds, boolean applyAssumptions) { + EnumSet paramKinds = PurityUtils.getPurityKinds(atypeFactory, paramFunction); + EnumSet required = EnumSet.copyOf(methodKinds); + required.removeAll(paramKinds); + if (paramFunction.getReturnType().getKind() == TypeKind.VOID) { + // A side-effect-free method that returns no value is deterministic: two calls return the + // same (absent) value. Without a guarantee that the argument is side-effect-free, the + // argument must still be checked for determinism, since its side effects can make a later + // call compute a different value. + if (paramKinds.contains(PurityKind.SIDE_EFFECT_FREE) + || methodKinds.contains(PurityKind.SIDE_EFFECT_FREE)) { + required.remove(PurityKind.DETERMINISTIC); + } else if (applyAssumptions + && assumeSideEffectFree + && required.remove(PurityKind.DETERMINISTIC)) { + // The assumption covers the methods that the argument calls, not the argument's own + // code, so check that the argument is side-effect-free instead. + required.add(PurityKind.SIDE_EFFECT_FREE); + } + } + return required; + } + + /** + * Returns the subset of {@code kinds} that whole-program inference may infer for {@code method}: + * those that no call to {@code method} would require of an argument that is passed to a + * functional-interface parameter. See {@link #checkFunctionalArguments}. + * + *

Inference cannot annotate a lambda expression or a method reference, so it cannot make an + * argument meet such a requirement; inferring the kind would introduce errors at call sites. A + * parameter whose type is a type variable bounded by {@code Object} is a functional-interface + * parameter at any call that instantiates the type variable with a functional interface, whose + * functional method may make no promise, so no kind is inferred for a method with such a + * parameter. + * + *

Discarding one kind can make another one required: determinism is required of an argument + * that returns no value only when the argument is known to be side-effect-free. The computation + * therefore repeats until what remains requires nothing. + * + * @param method a method or constructor + * @param kinds the purity kinds that might be inferred for {@code method} + * @return the subset of {@code kinds} that calls to {@code method} would not require of arguments + */ + private EnumSet kindsSafeToInfer( + ExecutableElement method, EnumSet kinds) { + EnumSet result = EnumSet.copyOf(kinds); + while (!result.isEmpty()) { + EnumSet required = EnumSet.noneOf(PurityKind.class); + for (int i = 0; i < method.getParameters().size(); i++) { + ExecutableElement paramFunction = parameterFunctionalMethod(method, i); + if (paramFunction != null) { + required.addAll(purityRequiredOfArgument(paramFunction, result, false)); + } else if (mayBeInstantiatedWithFunctionalInterface( + parameterArgumentType(method, i, null))) { + required.addAll(result); + } + } + if (required.isEmpty()) { + break; + } + result.removeAll(required); + } + return result; + } + + /** + * Returns true if {@code type} is a type variable that a call may instantiate with a functional + * interface, even though its upper bound is not one. A type variable whose upper bound is a + * functional interface is handled by {@link #parameterFunctionalMethod}. A type variable bounded + * by an interface with no abstract methods, such as {@code Serializable}, is not handled, + * although a call may instantiate it with an intersection that includes a functional interface. + * + * @param type a type + * @return true if {@code type} is a type variable whose upper bound is {@code Object} + */ + private static boolean mayBeInstantiatedWithFunctionalInterface(TypeMirror type) { + return type.getKind() == TypeKind.TYPEVAR && TypesUtils.isObject(TypesUtils.upperBound(type)); + } + + /** + * Returns {@code expr} without parentheses, and also without casts if what they wrap denotes + * code: a lambda, a method reference, a conditional expression, or a switch expression. A cast of + * such an expression does not change the code that the expression denotes, so the operand is what + * {@link #checkFunctionalArgument} checks. + * + *

A cast of any other expression is left in place, so that the argument is checked against the + * functional method of the cast's type. That is the type of the value that the call receives, + * just as for an argument that is not cast. + * + * @param expr an expression + * @return {@code expr} without parentheses, and without casts of an expression that denotes code + */ + private static ExpressionTree withoutCastsOfCode(ExpressionTree expr) { + ExpressionTree withoutParens = TreeUtils.withoutParens(expr); + ExpressionTree operand = TreeUtils.withoutParensOrCasts(withoutParens); + return operand instanceof LambdaExpressionTree + || operand instanceof MemberReferenceTree + || operand instanceof ConditionalExpressionTree + || operand instanceof SwitchExpressionTree + ? operand + : withoutParens; + } + + /** + * Checks that one argument to a functional-interface parameter has the given purity. + * + * @param arg the argument + * @param required the purity that the argument's functional method must have + * @param paramFunction the functional method of the parameter's type + * @param param the parameter that {@code arg} is passed to + * @param callee the invoked method or constructor + */ + protected void checkFunctionalArgument( + ExpressionTree arg, + EnumSet required, + ExecutableElement paramFunction, + VariableElement param, + ExecutableElement callee) { + ProcessingEnvironment env = atypeFactory.getProcessingEnv(); + ExpressionTree argument = withoutCastsOfCode(arg); + // A lambda or method reference whose type is a functional subinterface may implement an + // abstract method that is unrelated to paramFunction; then the subinterface's implementation + // of paramFunction is what runs, so the argument is checked against that implementation. + boolean implementsParamFunction = + !(argument instanceof LambdaExpressionTree || argument instanceof MemberReferenceTree) + || implementsFunction(argument, paramFunction, env); + + if (implementsParamFunction && argument instanceof LambdaExpressionTree lambda) { + TreePath lambdaPath = new TreePath(getCurrentPath(), argument); + PurityResult r = + PurityChecker.checkPurity( + new TreePath(lambdaPath, lambda.getBody()), + atypeFactory, + // The body may call the enclosing method's functional-interface parameters: they + // hold values that the caller of that method was required to check, whenever the + // lambda runs. + TreePathUtil.enclosingMethod(getCurrentPath()), + env, + assumeSideEffectFree, + assumeDeterministic, + assumePureGetters); + if (!r.isPure(required)) { + reportPurityErrors(r, required); + } + return; + } + + if (argument instanceof NewArrayTree newArray) { + // The argument is the array of a varargs call in its array form. Each initializer of an + // array literal denotes code that the call passes, so each is checked on its own. An + // element that is stored into the array elsewhere is not checked, just as the elements of + // an array that the call receives from somewhere else are not. + List initializers = newArray.getInitializers(); + if (initializers != null) { + for (ExpressionTree initializer : initializers) { + checkFunctionalArgument(initializer, required, paramFunction, param, callee); + } + } + return; + } + + if (argument instanceof ConditionalExpressionTree conditional) { + checkFunctionalArgument( + conditional.getTrueExpression(), required, paramFunction, param, callee); + checkFunctionalArgument( + conditional.getFalseExpression(), required, paramFunction, param, callee); + return; + } + + if (argument instanceof SwitchExpressionTree switchExpression) { + SwitchExpressionScanner scanner = + new FunctionalSwitchExpressionScanner<>( + (ExpressionTree resultExpression, Void unused) -> { + checkFunctionalArgument(resultExpression, required, paramFunction, param, callee); + return null; + }, + (r1, r2) -> null); + scanner.scanSwitchExpression(switchExpression, null); + return; + } + + EnumSet argKinds; + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(getCurrentPath()); + if (implementsParamFunction && argument instanceof MemberReferenceTree memberReference) { + if (isArrayConstructorReference(memberReference)) { + // Creating an array modifies nothing that exists before the call. It is not + // deterministic, like any object creation. + argKinds = EnumSet.of(PurityKind.SIDE_EFFECT_FREE); + } else { + ExecutableElement referenced = (ExecutableElement) TreeUtils.elementFromUse(argument); + argKinds = implementationPurityKinds(referenced); + // `f::apply`, where `f` is a functional-interface parameter of the enclosing method, + // denotes the code that the caller of that method was required to check. + argKinds.addAll( + PurityChecker.functionalParameterCallPurity( + atypeFactory, + memberReference.getQualifierExpression(), + referenced, + enclosingMethod, + env)); + // The check of the method reference against its functional method already covers the + // purity that the functional method promises. + EnumSet relied = EnumSet.copyOf(required); + relied.removeAll(PurityChecker.functionalParameterKinds(atypeFactory, paramFunction)); + checkImplementationFunctionalParameters( + memberReference, + referenced, + paramFunction, + MemberReferenceKind.getMemberReferenceKind(memberReference).isUnbound() ? 1 : 0, + relied, + "purity.methodref.functional.parameter"); + } + } else { + ExecutableElement argFunction = + functionalMethodOf(TreeUtils.typeOf(argument), paramFunction, env); + if (argFunction == null) { + // The argument is the null literal, or its type does not implement the parameter's + // functional method (which javac has already reported). There is nothing to check. + return; + } + argKinds = implementationPurityKinds(argFunction); + // If the argument is a functional-interface parameter of the enclosing method, the caller + // of that method was required to check the code that argFunction runs. + argKinds.addAll( + PurityChecker.functionalParameterCallPurity( + atypeFactory, argument, argFunction, enclosingMethod, env)); + checkImplementationFunctionalParameters( + arg, argFunction, paramFunction, 0, required, "purity.functional.argument.parameter"); + } + + if (!argKinds.containsAll(required)) { + reportErrorOnce( + arg, + "purity.functional.argument", + param.getSimpleName(), + ElementUtils.getSimpleDescription(callee), + purityKindsToString(argKinds), + purityKindsToString(required)); + } + } + + /** + * Checks that a call through {@code functionalMethod} passes, to each functional-interface + * parameter of {@code implementation}, code with the purity that {@code implementation}'s body + * assumes of it; see {@link + * PurityChecker#functionalParameterPurity(org.checkerframework.javacutil.AnnotationProvider, + * ExecutableElement, int, ProcessingEnvironment)}. Only the kinds in {@code relied} are checked. + * + *

A call through the functional method checks its arguments against the functional method's + * declaration, not the implementation's. That check may require less, or nothing at all, as when + * the functional method's parameter type is a type variable or the functional method has no + * purity annotation. Then the implementation may run with an argument that lacks the purity that + * its body assumes, so the implementation may lack its own purity. That matters only to code that + * relies on the purity of calls through the functional method. + * + * @param reportTree the tree at which to report an error + * @param implementation the method that a call through {@code functionalMethod} runs + * @param functionalMethod the functional method that {@code implementation} implements + * @param offset the index of {@code functionalMethod}'s parameter that corresponds to {@code + * implementation}'s first parameter: 1 for an unbound method reference such as {@code C::m}, + * whose functional method's first parameter is the receiver, and 0 otherwise + * @param relied the purity of calls through {@code functionalMethod} that some code relies on + * @param messageKey the key of the error message to report + */ + private void checkImplementationFunctionalParameters( + Tree reportTree, + ExecutableElement implementation, + ExecutableElement functionalMethod, + int offset, + EnumSet relied, + @CompilerMessageKey String messageKey) { + if (relied.isEmpty()) { + return; + } + ProcessingEnvironment env = atypeFactory.getProcessingEnv(); + List implementationParams = implementation.getParameters(); + for (int i = 0; i < implementationParams.size(); i++) { + EnumSet assumed = + PurityChecker.functionalParameterPurity(atypeFactory, implementation, i, env); + assumed.retainAll(relied); + if (assumed.isEmpty()) { + continue; + } + int j = i + offset; + EnumSet provided = EnumSet.noneOf(PurityKind.class); + if (j < functionalMethod.getParameters().size()) { + ExecutableElement paramFunction = parameterFunctionalMethod(functionalMethod, j); + if (paramFunction != null) { + provided = + PurityChecker.functionalParameterPurity(atypeFactory, functionalMethod, j, env); + provided.addAll(PurityUtils.getPurityKinds(atypeFactory, paramFunction)); + if (paramFunction.getReturnType().getKind() == TypeKind.VOID + && provided.contains(PurityKind.SIDE_EFFECT_FREE)) { + // A side-effect-free method that returns no value is deterministic. + provided.add(PurityKind.DETERMINISTIC); + } + } + } + if (!provided.containsAll(assumed)) { + reportErrorOnce( + reportTree, + messageKey, + implementationParams.get(i).getSimpleName(), + implementation, + purityKindsToString(assumed), + functionalMethod, + purityKindsToString(provided)); + } + } + } + + /** + * Returns true if the functional method that {@code code} implements is {@code paramFunction} or + * overrides it. + * + * @param code a lambda expression or method reference + * @param paramFunction the functional method of a parameter's type + * @param env the processing environment + * @return true if {@code code} implements {@code paramFunction} + */ + private static boolean implementsFunction( + ExpressionTree code, ExecutableElement paramFunction, ProcessingEnvironment env) { + TypeMirror functionalType = PurityChecker.functionalInterfaceType(TreeUtils.typeOf(code), env); + return functionalType == null + || PurityChecker.isSameOrOverrides( + TypesUtils.findFunction(functionalType, env), paramFunction, env); + } + + /** + * Returns the method that {@code type} supplies for the functional method {@code paramFunction}, + * or null if it has none. + * + *

This is {@code paramFunction} itself when {@code type} is the functional interface, but an + * argument may also be of a class type, including an anonymous class, that implements the + * interface. The implementation is what will run, so its annotations are what matter. Likewise, a + * functional subinterface may implement {@code paramFunction} with a default method and declare + * an unrelated abstract method as its own functional method; the default method is what runs. + * + * @param type the type of an argument + * @param paramFunction the functional method of the parameter's type + * @param env the processing environment + * @return the method of {@code type} that implements {@code paramFunction}, or null + */ + private @Nullable ExecutableElement functionalMethodOf( + TypeMirror type, ExecutableElement paramFunction, ProcessingEnvironment env) { + Elements elements = env.getElementUtils(); + TypeMirror functionalType = PurityChecker.functionalInterfaceType(type, env); + if (functionalType != null) { + ExecutableElement function = TypesUtils.findFunction(functionalType, env); + if (PurityChecker.isSameOrOverrides(function, paramFunction, env)) { + return function; + } + } + if (type.getKind() == TypeKind.TYPEVAR) { + type = TypesUtils.upperBound(type); + } + if (type.getKind() != TypeKind.DECLARED && type.getKind() != TypeKind.INTERSECTION) { + // Only a class or interface type declares methods. The null type, which is the type of + // the null literal, has an element whose members cannot be queried. + return null; + } + TypeElement typeElement = TypesUtils.getTypeElement(type); + if (typeElement == null) { + return null; + } + // The members of a class that inherits its implementation from a superclass include both + // that implementation and the interface's abstract method. The implementation is what runs. + ExecutableElement abstractMatch = null; + for (ExecutableElement method : ElementFilter.methodsIn(elements.getAllMembers(typeElement))) { + if (method.equals(paramFunction) || elements.overrides(method, paramFunction, typeElement)) { + if (!method.getModifiers().contains(Modifier.ABSTRACT)) { + return method; + } + if (abstractMatch == null) { + abstractMatch = method; + } + } + } + return abstractMatch; + } + + /** + * Returns true if the given method reference creates an array, as in {@code String[]::new}. + * + * @param tree a method reference + * @return true if {@code tree} refers to an array constructor + */ + private static boolean isArrayConstructorReference(MemberReferenceTree tree) { + return tree.getMode() == MemberReferenceTree.ReferenceMode.NEW + && TreeUtils.withoutParens(tree.getQualifierExpression()) instanceof ArrayTypeTree; + } + + /** + * Returns the purity of a method that implements a functional method: its declared purity, plus + * whatever the command-line assumptions grant it, plus determinism if it is side-effect-free and + * returns no value. + * + * @param method a method or constructor + * @return the purity kinds of {@code method} + */ + private EnumSet implementationPurityKinds(ExecutableElement method) { + EnumSet result = EnumSet.copyOf(PurityUtils.getPurityKinds(atypeFactory, method)); + // Like PurityChecker, apply each assumption to every method, including one with no purity + // annotation, so that a method reference is treated exactly like a lambda whose body calls + // the referenced method. + if (assumeSideEffectFree) { + result.add(PurityKind.SIDE_EFFECT_FREE); + } + if (assumeDeterministic) { + result.add(PurityKind.DETERMINISTIC); + } + if (assumePureGetters && ElementUtils.isGetter(method)) { + result.add(PurityKind.SIDE_EFFECT_FREE); + result.add(PurityKind.DETERMINISTIC); + } + if (method.getKind() != ElementKind.CONSTRUCTOR + && method.getReturnType().getKind() == TypeKind.VOID + && result.contains(PurityKind.SIDE_EFFECT_FREE)) { + // Like PurityChecker, treat a side-effect-free method that returns no value as + // deterministic: two calls return the same (absent) value. + result.add(PurityKind.DETERMINISTIC); + } + return result; + } + + /** + * Formats purity kinds for a diagnostic message, as the annotations that a user writes rather + * than as the enum constant names that {@code EnumSet.toString} would produce. + * + * @param purityKinds a set of purity kinds + * @return the annotations corresponding to {@code purityKinds}, space-separated + */ + protected static String purityKindsToString(EnumSet purityKinds) { + if (purityKinds.isEmpty()) { + return "(no side effect annotation)"; + } + StringJoiner result = new StringJoiner(" "); + for (PurityKind purityKind : purityKinds) { + switch (purityKind) { + case SIDE_EFFECT_FREE -> result.add("@SideEffectFree"); + case DETERMINISTIC -> result.add("@Deterministic"); + } + } + return result.toString(); + } + /** * Reports errors found during purity checking. * @@ -1703,8 +2316,20 @@ private void reportPurityError(PurityViolation violation, ImpurityReason r) { args = new Object[] {violation.purityAdjective}; } // The same tree can be checked more than once: an initializer is checked as part of every - // constructor that runs it, and every checker of a compound checker checks purity - // independently. Report each message at each tree only once. + // constructor that runs it. + reportErrorOnce(tree, msgKey, args); + } + + /** + * Reports an error at {@code tree}, unless this checker or another checker of the same compound + * checker has already reported it there. Every checker of a compound checker checks purity + * independently, so a purity error would otherwise be reported once per checker. + * + * @param tree the tree at which to report the error + * @param msgKey the message key + * @param args the arguments of the message + */ + private void reportErrorOnce(Tree tree, @CompilerMessageKey String msgKey, Object... args) { TreePath path = atypeFactory.getPath(tree); if (path == null) { checker.reportError(tree, msgKey, args); @@ -2273,6 +2898,8 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { AnnotatedTypes.adaptParameters(atypeFactory, invokedMethod, tree.getArguments(), tree); checkArguments(params, tree.getArguments(), methodName, method.getParameters()); checkVarargs(invokedMethod, tree); + checkFunctionalArguments( + method, invokedMethod, tree.getArguments(), TreeUtils.isVarargsCall(tree)); if (ElementUtils.isMethod( invokedMethod.getElement(), vectorCopyInto, atypeFactory.getProcessingEnv())) { @@ -2618,6 +3245,17 @@ public Void visitNewClass(NewClassTree tree, Void p) { checkArguments(params, passedArguments, constructorName, constructor.getParameters()); checkVarargs(constructorType, tree); + if (checkPurityAnnotations && !infer) { + // For an anonymous class, the arguments are passed to the super constructor; the anonymous + // class's own constructor is synthetic and carries no annotation. Do not compute the super + // constructor unless it is needed: for an anonymous class, doing so searches the class's + // synthetic constructor for the super call. + checkFunctionalArguments( + TreeUtils.getSuperConstructor(tree), + constructorType, + passedArguments, + TreeUtils.isVarargsCall(tree)); + } List paramBounds = CollectionsP.mapList(AnnotatedTypeVariable::getBounds, constructorType.getTypeVariables()); @@ -2805,7 +3443,16 @@ protected void checkLambdaPurity( if (needToCheck) { PurityResult r = PurityChecker.checkPurity( - body, atypeFactory, assumeSideEffectFree, assumeDeterministic, assumePureGetters); + body, + atypeFactory, + // The functional method that the lambda implements constrains the body, but the + // enclosing method's functional-interface parameters still hold values that its + // caller was required to check, whenever the lambda runs. + TreePathUtil.enclosingMethod(getCurrentPath()), + atypeFactory.getProcessingEnv(), + assumeSideEffectFree, + assumeDeterministic, + assumePureGetters); if (!r.isPure(purityKinds)) { reportPurityErrors(r, purityKinds); } @@ -2816,11 +3463,19 @@ protected void checkLambdaPurity( // purity just as an overriding method does; see the treatment of overridden methods in // `checkPurityAnnotations`. EnumSet lambdaKinds = - purityForInference(Collections.singletonList(body)).getKinds().clone(); + purityForInference( + Collections.singletonList(body), TreePathUtil.enclosingMethod(getCurrentPath())) + .getKinds() + .clone(); if (functionalMethod.getReturnType().getKind() == TypeKind.VOID) { lambdaKinds.remove(PurityKind.DETERMINISTIC); } + // Do not infer a kind that would newly constrain the arguments at call sites. + lambdaKinds = kindsSafeToInfer(functionalMethod, lambdaKinds); WholeProgramInference wpi = atypeFactory.getWholeProgramInference(); + if (wpi == null) { + throw new BugInCF("No WPI while inferring purity of " + tree); + } inferPurityAnno(lambdaKinds, wpi, functionalMethod); for (ExecutableElement overriddenElt : ElementUtils.getOverriddenMethods(functionalMethod, types)) { @@ -4724,27 +5379,26 @@ private void checkMethodReferencePurity() { purityKindsToString(superPurity), overridden); } + checkMethodReferenceFunctionalParameters(); } /** - * Formats purity kinds for a diagnostic message, as the annotations that a user writes rather - * than as the enum constant names that {@code EnumSet.toString} would produce. - * - * @param purityKinds a set of purity kinds - * @return the annotations corresponding to {@code purityKinds}, space-separated + * Check that a call through the functional method passes, to each functional-interface + * parameter of the referenced method, code with the purity that the referenced method's body + * assumes of it, so far as the functional method promises that purity to its callers. */ - private String purityKindsToString(EnumSet purityKinds) { - if (purityKinds.isEmpty()) { - return "(no side effect annotation)"; - } - StringJoiner result = new StringJoiner(" "); - for (PurityKind purityKind : purityKinds) { - switch (purityKind) { - case SIDE_EFFECT_FREE -> result.add("@SideEffectFree"); - case DETERMINISTIC -> result.add("@Deterministic"); - } - } - return result.toString(); + private void checkMethodReferenceFunctionalParameters() { + ExecutableElement functionalMethod = overridden.getElement(); + checkImplementationFunctionalParameters( + overriderTree, + overrider.getElement(), + functionalMethod, + MemberReferenceKind.getMemberReferenceKind((MemberReferenceTree) overriderTree) + .isUnbound() + ? 1 + : 0, + PurityChecker.functionalParameterKinds(atypeFactory, functionalMethod), + "purity.methodref.functional.parameter"); } /** diff --git a/framework/src/main/java/org/checkerframework/common/basetype/messages.properties b/framework/src/main/java/org/checkerframework/common/basetype/messages.properties index f3f501b79070..ffec87a4c074 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/messages.properties +++ b/framework/src/main/java/org/checkerframework/common/basetype/messages.properties @@ -65,6 +65,8 @@ cast.redundant=Redundant cast;%ntype : %s purity.deterministic.constructor=a constructor cannot be annotated @Deterministic purity.deterministic.void.method=a method without return value cannot be annotated @Deterministic purity.methodref=Incompatible purity declaration%nMethod in %s%n %s %s%n is not a valid method reference for method in %s%n %s %s +purity.methodref.functional.parameter=incompatible purity of argument to parameter %s of referenced method %s%n the referenced method assumes: %s%n a call to %s guarantees: %s +purity.functional.argument.parameter=incompatible purity of argument to parameter %s of %s, which the argument runs%n the argument's method assumes: %s%n a call to %s guarantees: %s purity.sideeffectsonly.overriding=Incompatible @SideEffectsOnly declaration%nMethod in %s%n %s %s%ncannot override method in %s%n %s %s%nbecause the overriding method may side-effect %s purity.assign.array=array assignment not allowed in %s method purity.assign.field=field assignment not allowed in %s method @@ -74,6 +76,7 @@ purity.object.creation=object creation not allowed in %s method purity.more.deterministic=the method %s could be declared as @Deterministic purity.more.pure=the method %s could be declared as @Pure purity.more.sideeffectfree=the method %s could be declared as @SideEffectFree +purity.functional.argument=incompatible purity of argument to parameter %s of %s%n found: %s%n required: %s purity.incorrect.sideeffectsonly=%s may side-effect %s purity.unknown.sideeffectsonly=call to %s may side-effect arbitrary expressions purity.unparseable.sideeffectsonly=cannot parse @SideEffectsOnly expression "%s" at call to %s diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java index 88a80f69c911..399565426507 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java @@ -477,6 +477,10 @@ private boolean isExpressionOrStatementPure( PurityChecker.checkPurity( expressionOrStatement, aTypeFactory, + // The enclosing method's functional-interface parameters hold values that its + // caller was required to check, whenever the checked code runs. + TreePathUtil.enclosingMethod(expressionOrStatement), + aTypeFactory.getProcessingEnv(), isAssumeSideEffectFreeEnabled, isAssumeDeterministicEnabled, aTypeFactory.getChecker().hasOption("assumePureGetters")); diff --git a/framework/src/test/java/org/checkerframework/framework/test/junit/PurityAssumptionsTest.java b/framework/src/test/java/org/checkerframework/framework/test/junit/PurityAssumptionsTest.java new file mode 100644 index 000000000000..fff39a12a52d --- /dev/null +++ b/framework/src/test/java/org/checkerframework/framework/test/junit/PurityAssumptionsTest.java @@ -0,0 +1,28 @@ +package org.checkerframework.framework.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.testchecker.util.FlowTestChecker; +import org.junit.runners.Parameterized.Parameters; + +/** Tests purity checking under the command-line arguments that assume purity. */ +public class PurityAssumptionsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public PurityAssumptionsTest(List testFiles) { + super( + testFiles, + FlowTestChecker.class, + "flow", + "-AcheckPurityAnnotations", + "-AassumeSideEffectFree"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"purity-assumptions"}; + } +} diff --git a/framework/tests/all-systems/Issue6891.java b/framework/tests/all-systems/Issue6891.java index c2c7cbc8313f..80b647c655c0 100644 --- a/framework/tests/all-systems/Issue6891.java +++ b/framework/tests/all-systems/Issue6891.java @@ -8,7 +8,10 @@ public class Issue6891 { - @SuppressWarnings("optional") // true positives, but this test is not about Optional + // Optional.map is @SideEffectFree, whose declaration comments that "the mapper must not have + // side effects", so the mapper below is a true positive under -AcheckPurityAnnotations. The + // expected error is asserted in framework/tests/flow/PurityFunctionalArgumentJdk.java. + @SuppressWarnings({"optional", "purity"}) // true positives, but this test is not about them private Map func(Set entities) { return Optional.ofNullable(entities) diff --git a/framework/tests/all-systems/Issue7680.java b/framework/tests/all-systems/Issue7680.java index fa125a378486..c3e297c6e2a4 100644 --- a/framework/tests/all-systems/Issue7680.java +++ b/framework/tests/all-systems/Issue7680.java @@ -1,9 +1,13 @@ import java.util.Comparator; +import org.checkerframework.dataflow.qual.Pure; public class Issue7680 { interface Foo { + // Comparator.comparing and Comparator.thenComparing require a side-effect-free key extractor. + @Pure int x(); + @Pure int y(); } diff --git a/framework/tests/flow/PurityClassTypeVariableArgument.java b/framework/tests/flow/PurityClassTypeVariableArgument.java new file mode 100644 index 000000000000..19eb84d8fc8f --- /dev/null +++ b/framework/tests/flow/PurityClassTypeVariableArgument.java @@ -0,0 +1,89 @@ +import java.util.function.Supplier; +import org.checkerframework.dataflow.qual.SideEffectFree; + +/** A parameter whose type is a type variable is checked at the call site. */ +public class PurityClassTypeVariableArgument { + static int count; + + static class Base { + @SideEffectFree + public int apply(Supplier s) { + return s.get(); + } + } + + interface I { + @SideEffectFree + int apply(T t); + } + + /** + * Sub inherits Base.apply as its implementation of I>.apply, so a call through + * I> checks its argument at the call site. + */ + static class Sub extends Base implements I> {} + + static class DirectOverride implements I> { + @Override + @SideEffectFree + public int apply(Supplier s) { + // :: error: [purity.call] + return s.get(); + } + } + + void callThroughInterface(I> i) { + // :: error: [purity.assign.field] + i.apply(() -> count++); + i.apply(() -> 1); + } + + static class Box { + @SideEffectFree + Box(T t) {} + + @SideEffectFree + void put(T t) {} + + @SafeVarargs + @SideEffectFree + final void putAll(T... ts) {} + } + + @SideEffectFree + static T id(T t) { + return t; + } + + void others(Box b) { + // Method type variables are checked, too. + // :: error: [purity.assign.field] + java.util.List l = java.util.List.of(() -> count++); + // :: error: [purity.assign.field] + Runnable r = id(() -> count++); + // :: error: [purity.assign.field] + b.put(() -> count++); + // :: error: [purity.assign.field] + b.putAll(() -> count++); + // :: error: [purity.assign.field] + new Box(() -> count++); + // :: error: [purity.assign.field] + new Box(() -> count++) {}; + } + + static class SubBox extends Box { + SubBox() { + super(() -> {}); + } + + void self() { + // Within Box's own subclass, T is Runnable. + // :: error: [purity.assign.field] + put(() -> count++); + } + } + + void callWithSub() { + callThroughInterface(new Sub()); + } +} diff --git a/framework/tests/flow/PurityFunctionalArgument.java b/framework/tests/flow/PurityFunctionalArgument.java new file mode 100644 index 000000000000..8816b5e06f4e --- /dev/null +++ b/framework/tests/flow/PurityFunctionalArgument.java @@ -0,0 +1,344 @@ +// At a call to a @SideEffectFree method, every argument passed to a functional-interface parameter +// must have a side-effect-free functional method: +// +// * a lambda: its body must be side-effect-free; +// * a method reference: the referenced method must be @SideEffectFree; +// * a conditional or switch expression: each of its result expressions is checked on its own; +// * a functional-interface parameter of an enclosing @SideEffectFree method: the caller of that +// method has already discharged the obligation; +// * anything else: checked against the functional method of the argument's declared type. +// +// Each call site is in an unannotated method, so the only diagnostic a call site can produce is the +// one under test. The callees do not call their parameters; that is tested in +// PurityFunctionalParameter.java. + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalArgument { + + int count = 0; + + /** A functional interface whose functional method is annotated. */ + @FunctionalInterface + interface PureFunction extends Function { + @Override + @SideEffectFree + R apply(T t); + } + + @SideEffectFree + static int callee(Function f, String s) { + return 0; + } + + static int unannotatedCallee(Function f, String s) { + return 0; + } + + @SideEffectFree + int pureLength(String s) { + return s.length(); + } + + int impureLength(String s) { + count++; + return s.length(); + } + + Function unannotatedVariable = s -> s.length(); + + PureFunction annotatedVariable = s -> s.length(); + + Function lengthFunction() { + return s -> s.length(); + } + + PureFunction pureLengthFunction() { + return s -> s.length(); + } + + // A lambda argument's body is checked directly. + + void lambdaArguments(String s) { + callee(t -> t.length(), s); + // :: error: [purity.assign.field] + callee(t -> count++, s); + // :: error: [purity.call] + callee(t -> impureLength(t), s); + callee( + t -> { + int local = 0; + local++; + return local; + }, + s); + } + + // The method that a method reference refers to must be @SideEffectFree. + + void methodReferenceArguments(String s) { + callee(this::pureLength, s); + // :: error: [purity.functional.argument] + callee(this::impureLength, s); + callee(String::length, s); + } + + // Each result expression of a conditional or switch expression is checked on its own. The type + // of such an expression is the parameter's type, which says nothing about the code that any + // result expression denotes. + + void conditionalArguments(boolean b, String s) { + callee(b ? t -> t.length() : t -> 0, s); + // :: error: [purity.assign.field] + callee(b ? t -> count++ : t -> 0, s); + callee(b ? this::pureLength : String::length, s); + // :: error: [purity.functional.argument] + callee(b ? this::pureLength : this::impureLength, s); + } + + void switchArguments(int i, String s) { + callee( + switch (i) { + case 1 -> t -> t.length(); + default -> t -> 0; + }, + s); + callee( + switch (i) { + // :: error: [purity.assign.field] + case 1 -> t -> count++; + default -> t -> 0; + }, + s); + callee( + switch (i) { + case 1 -> this::pureLength; + // :: error: [purity.functional.argument] + default -> this::impureLength; + }, + s); + } + + // A result expression may also be the value of a `yield` statement. + + void yieldArguments(int i, String s) { + callee( + switch (i) { + case 1: + { + // :: error: [purity.functional.argument] + yield this::impureLength; + } + default: + yield this::pureLength; + }, + s); + } + + // Parentheses do not change how an argument is checked. + + void parenthesizedArguments(boolean b, String s) { + callee((t -> t.length()), s); + // :: error: [purity.assign.field] + callee((t -> count++), s); + // :: error: [purity.functional.argument] + callee((this::impureLength), s); + // :: error: [purity.functional.argument] + callee(b ? (this::pureLength) : (this::impureLength), s); + } + + // A cast does not change the code that an argument denotes, so the operand is checked. + + void castArguments(String s) { + callee((Function) t -> t.length(), s); + // :: error: [purity.assign.field] + callee((Function) t -> count++, s); + callee((Function) this::pureLength, s); + // :: error: [purity.functional.argument] + callee((Function) this::impureLength, s); + // A cast of any other argument is checked against the functional method of the cast's type, + // just as an uncast argument is checked against the functional method of its own type. + // :: error: [purity.functional.argument] + callee((Function) annotatedVariable, s); + callee((PureFunction) unannotatedVariable, s); + } + + // The null literal denotes no code, so there is nothing to check. + + void nullArguments(boolean b, int i, String s) { + callee(null, s); + callee(b ? null : this::pureLength, s); + callee( + switch (i) { + case 1 -> null; + default -> this::pureLength; + }, + s); + } + + // Any other argument is checked against the functional method of its declared type. + + void otherArguments(String s) { + // :: error: [purity.functional.argument] + callee(unannotatedVariable, s); + callee(annotatedVariable, s); + // :: error: [purity.functional.argument] + callee(lengthFunction(), s); + callee(pureLengthFunction(), s); + } + + // A functional-interface parameter may be passed onward from a @SideEffectFree method, but not + // from a method that makes no promise about what it was given. + + @SideEffectFree + int passesParameterOnward(Function f, String s) { + return callee(f, s); + } + + int unannotatedMethodPassesParameterOnward(Function f, String s) { + // :: error: [purity.functional.argument] + return callee(f, s); + } + + // A method reference to the functional method of such a parameter denotes the same code, so it + // is passed onward in the same way. + + @SideEffectFree + int passesParameterMethodReference(Function f, String s) { + return callee(f::apply, s); + } + + int unannotatedMethodPassesParameterMethodReference(Function f, String s) { + // :: error: [purity.functional.argument] + return callee(f::apply, s); + } + + /** A default method is not the functional method, so the caller checked nothing about it. */ + @SideEffectFree + int passesParameterDefaultMethodReference(DefaultFunction f, String s) { + // :: error: [purity.functional.argument] + return callee(f::applyTwice, s); + } + + /** A functional interface with a default method of the same signature as its functional one. */ + @FunctionalInterface + interface DefaultFunction extends Function { + default Integer applyTwice(String s) { + return apply(s) + apply(s); + } + } + + // An unannotated callee imposes no obligation on its arguments. + + void unannotatedCalleeTakesAnything(String s) { + unannotatedCallee(t -> count++, s); + unannotatedCallee(this::impureLength, s); + unannotatedCallee(unannotatedVariable, s); + } + + // An argument that is not of functional-interface type is unaffected. + + @SideEffectFree + static int ordinaryParameters(String s, int i) { + return i; + } + + void ordinaryArguments(String s) { + ordinaryParameters(s, count); + } + + // An argument whose type is a class that implements the functional interface is checked against + // the class's implementation of the functional method, which is the code that will run. + + static class ImpureFunction implements Function { + int n = 0; + + @Override + public Integer apply(String s) { + return n++; + } + } + + static class SideEffectFreeFunction implements Function { + @Override + @SideEffectFree + public Integer apply(String s) { + return s.length(); + } + } + + void classTypedArguments(ImpureFunction impure, SideEffectFreeFunction pure, String s) { + // :: error: [purity.functional.argument] + callee(impure, s); + callee(pure, s); + } + + void anonymousClassArguments(String s) { + callee( + // :: error: [purity.functional.argument] + new Function() { + @Override + public Integer apply(String t) { + return count++; + } + }, + s); + callee( + new Function() { + @Override + @SideEffectFree + public Integer apply(String t) { + return t.length(); + } + }, + s); + } + + // A lambda's body may call a functional-interface parameter of the enclosing method, which holds + // a value that the caller of that method was required to check, whenever the lambda runs. + + @SideEffectFree + int lambdaCallsEnclosingParameter(Function f, String s) { + return callee(t -> f.apply(t), s); + } + + int unannotatedMethodsLambdaCallsParameter(Function f, String s) { + // :: error: [purity.call] + return callee(t -> f.apply(t), s); + } + + /** A lambda's own parameter is checked at no call site, so passing it on is not permitted. */ + @SideEffectFree + void lambdaParameterIsNotDischarged(Function f, String s) { + Consumer> c = + g -> { + // :: error: [purity.functional.argument] + callee(g, s); + }; + // :: error: [purity.call] + c.accept(f); + } + + // An array constructor cannot be annotated, and creating an array modifies nothing. + + @SideEffectFree + static String[] arrayCallee(IntFunction generator) { + return generator.apply(0); + } + + String[] arrayConstructorReference() { + return arrayCallee(String[]::new); + } + + // A method reference assigned to an annotated functional interface is checked against its + // functional method, wherever the assignment appears. + + void methodReferenceAgainstAnnotatedSam() { + PureFunction ok = this::pureLength; + // :: error: [purity.methodref] + PureFunction bad = this::impureLength; + } +} diff --git a/framework/tests/flow/PurityFunctionalArgumentJdk.java b/framework/tests/flow/PurityFunctionalArgumentJdk.java new file mode 100644 index 000000000000..6bcac8d32966 --- /dev/null +++ b/framework/tests/flow/PurityFunctionalArgumentJdk.java @@ -0,0 +1,56 @@ +// The requirement on a functional-interface argument applies to the annotated JDK's methods, which +// is where most calls to a side-effect-free method with a functional-interface parameter occur. +// +// The requirement holds at every call site, whatever the purity of the method that contains it. + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +public class PurityFunctionalArgumentJdk { + + int count = 0; + + // Optional.map is @SideEffectFree: "the mapper must not have side effects". + + void optionalMap(Optional o) { + o.map(s -> s.length()); + // :: error: [purity.assign.field] + o.map(s -> count++); + } + + // Collections.min is @Pure and List.sort is @SideEffectsOnly("this"), so each requires a + // side-effect-free comparator. Comparator.compare is @Pure, so a value of that type has one. + + void comparatorArguments(Collection c, List l, Comparator comparator) { + java.util.Collections.min(c, comparator); + l.sort(comparator); + } + + /** + * A value whose functional method promises no purity cannot be passed: Function.apply is not + * annotated. + */ + void unannotatedFunctionalValue(Optional o, Function f) { + // :: error: [purity.functional.argument] + o.map(f); + } + + /** + * A comparator written at the call site is checked directly, against the @Pure on + * Comparator.compare. + */ + void comparatorWrittenHere(List l) { + l.sort((a, b) -> a.length() - b.length()); + // :: error: [purity.assign.field] + l.sort((a, b) -> count++); + } + + /** An unannotated JDK method imposes nothing: Stream.map is not annotated. */ + void unannotatedJdkMethod(Stream s) { + s.map(t -> count++); + } +} diff --git a/framework/tests/flow/PurityFunctionalArgumentKinds.java b/framework/tests/flow/PurityFunctionalArgumentKinds.java new file mode 100644 index 000000000000..7cd8a76cdf43 --- /dev/null +++ b/framework/tests/flow/PurityFunctionalArgumentKinds.java @@ -0,0 +1,157 @@ +// The requirement on an argument passed to a functional-interface parameter matches the purity of +// the callee: a @Pure method's functional arguments must be @Pure, a @Deterministic method's must +// be @Deterministic, and a @SideEffectFree method's must be @SideEffectFree. +// +// Each call site is in an unannotated method, so the only diagnostic a call site can produce is the +// one under test. + +import java.util.function.Function; +import org.checkerframework.dataflow.qual.Deterministic; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalArgumentKinds { + + int count = 0; + + @Pure + static int pureCallee(Function f, String s) { + return 0; + } + + @Deterministic + static int deterministicCallee(Function f, String s) { + return 0; + } + + @SideEffectFree + static int sideEffectFreeCallee(Function f, String s) { + return 0; + } + + @Pure + int pureMethod(String s) { + return 1; + } + + @SideEffectFree + int sideEffectFreeOnly(String s) { + return count; + } + + @Deterministic + int deterministicOnly(String s) { + return 1; + } + + // A @Pure or @Deterministic method may call the functional method of its own + // functional-interface parameters. + + @Pure + int pureCallsParameter(Function f, String s) { + return f.apply(s); + } + + @Deterministic + int deterministicCallsParameter(Function f, String s) { + return f.apply(s); + } + + // A lambda argument's body is checked against the callee's purity. + + void lambdaArgumentsToPureCallee(String s) { + pureCallee(t -> 1, s); + // :: error: [purity.assign.field] + pureCallee(t -> count++, s); + // :: error: [purity.object.creation] + pureCallee(t -> new String("x").length(), s); + } + + /** A @SideEffectFree callee constrains side effects only. */ + void lambdaArgumentsToSideEffectFreeCallee(String s) { + sideEffectFreeCallee(t -> new String("x").length(), s); + // :: error: [purity.assign.field] + sideEffectFreeCallee(t -> count++, s); + } + + /** A @Deterministic callee constrains determinism only. */ + void lambdaArgumentsToDeterministicCallee(String s) { + deterministicCallee(t -> deterministicOnly(t), s); + // :: error: [purity.object.creation] + deterministicCallee(t -> new String("x").length(), s); + // The same lambda is not side-effect-free, so it is rejected for a @SideEffectFree callee. + // :: error: [purity.call] + sideEffectFreeCallee(t -> deterministicOnly(t), s); + } + + // A side-effect-free functional method that returns no value is deterministic, whatever + // implements it, so a @Pure callee requires only side-effect-freeness of it. + + @Pure + static int pureRunnableCallee(Runnable r) { + return 0; + } + + @SideEffectFree + void sideEffectFreeVoid() {} + + @SideEffectFree + int sideEffectFreeNotDeterministic() { + return count; + } + + void impureVoid() { + count++; + } + + void voidFunctionalMethod() { + pureRunnableCallee(() -> sideEffectFreeVoid()); + pureRunnableCallee(this::sideEffectFreeVoid); + // The lambda is not deterministic, but as a side-effect-free void callback it is. + pureRunnableCallee(() -> sideEffectFreeNotDeterministic()); + // :: error: [purity.call] + pureRunnableCallee(() -> impureVoid()); + // :: error: [purity.functional.argument] + pureRunnableCallee(this::impureVoid); + } + + // A @Deterministic callee does not require side-effect-freeness of its functional arguments, so + // determinism is still required of a void callback: the callback's side effects could make a + // later call to the callee compute a different value. + + @Deterministic + static int deterministicRunnableCallee(Runnable r) { + return 0; + } + + void voidFunctionalMethodDeterministicCallee() { + deterministicRunnableCallee(() -> sideEffectFreeVoid()); + deterministicRunnableCallee(this::sideEffectFreeVoid); + // :: error: [purity.call] + deterministicRunnableCallee(() -> impureVoid()); + // :: error: [purity.functional.argument] + deterministicRunnableCallee(this::impureVoid); + // :: error: [purity.assign.field] + deterministicRunnableCallee(() -> count++); + // Conservative: the callback discards the value, so a side-effect-free callback is + // deterministic, but the requirement is checked against the code that the argument denotes. + // :: error: [purity.call] + deterministicRunnableCallee(() -> sideEffectFreeNotDeterministic()); + // :: error: [purity.functional.argument] + deterministicRunnableCallee(this::sideEffectFreeNotDeterministic); + } + + // A method reference's annotations must cover the callee's. + + void methodReferenceArguments(String s) { + pureCallee(this::pureMethod, s); + // :: error: [purity.functional.argument] + pureCallee(this::sideEffectFreeOnly, s); + // :: error: [purity.functional.argument] + pureCallee(this::deterministicOnly, s); + sideEffectFreeCallee(this::sideEffectFreeOnly, s); + deterministicCallee(this::deterministicOnly, s); + // :: error: [purity.functional.argument] + deterministicCallee(this::sideEffectFreeOnly, s); + } +} diff --git a/framework/tests/flow/PurityFunctionalConstructor.java b/framework/tests/flow/PurityFunctionalConstructor.java new file mode 100644 index 000000000000..7824a5d63865 --- /dev/null +++ b/framework/tests/flow/PurityFunctionalConstructor.java @@ -0,0 +1,61 @@ +// A constructor may be annotated @SideEffectFree, so the obligation on an argument passed to a +// functional-interface parameter is checked at a `new` expression too. +// +// Each call site is in an unannotated method, so the only diagnostic a call site can produce is the +// one under test. + +import java.util.function.Function; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalConstructor { + + int count = 0; + + static class Holder { + Function held; + + @SideEffectFree + Holder(Function f) { + this.held = f; + } + } + + static class UnannotatedHolder { + UnannotatedHolder(Function f) {} + } + + void constructorCallSites() { + new Holder(t -> t.length()); + // :: error: [purity.assign.field] + new Holder(t -> count++); + new UnannotatedHolder(t -> count++); + } + + /** An anonymous class's arguments are passed to the super constructor. */ + void anonymousSubclass() { + new Holder(t -> t.length()) {}; + // :: error: [purity.assign.field] + new Holder(t -> count++) {}; + } + + static class VarargsHolder { + // The parameter type is not generic, to avoid javac's warnings about generic varargs. + @SideEffectFree + VarargsHolder(Runnable... rs) {} + } + + /** A varargs constructor checks each argument that the call passes to its array. */ + void varargsConstructorCallSites() { + new VarargsHolder(() -> {}); + // :: error: [purity.assign.field] + new VarargsHolder(() -> count++); + // :: error: [purity.assign.field] + new VarargsHolder(() -> {}, () -> count++) {}; + } + + /** A functional-interface parameter may be passed onward to a constructor. */ + @SideEffectFree + Holder passesParameterToConstructor(Function f) { + return new Holder(f); + } +} diff --git a/framework/tests/flow/PurityFunctionalInherited.java b/framework/tests/flow/PurityFunctionalInherited.java new file mode 100644 index 000000000000..8d9b95dadef5 --- /dev/null +++ b/framework/tests/flow/PurityFunctionalInherited.java @@ -0,0 +1,88 @@ +// A class can implement an interface method by inheriting a method from its superclass. A call +// through the interface checks its arguments against the interface method, but such a call does not +// rely on the inherited method's purity either, so the class is not an error. +// +// Each call site is in an unannotated method, so the only diagnostic a call site can produce is the +// one under test. + +import java.util.function.Supplier; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalInherited { + + int count = 0; + + static class Base { + /** The body assumes that {@code s} is side-effect-free. */ + @SideEffectFree + public int apply(Supplier s) { + return s.get(); + } + } + + /** The interface method imposes nothing on its argument. */ + interface Unannotated { + int apply(Supplier s); + } + + // Base.apply implements Unannotated.apply only in this class. A call through Unannotated may pass + // an argument with side effects, but the caller does not treat the call as side-effect-free. + static class Sub extends Base implements Unannotated {} + + void callsThroughInterface(Unannotated receiver) { + // This call reaches Base.apply when receiver is a Sub. + receiver.apply(() -> count++); + } + + void callsThroughBase(Base receiver) { + receiver.apply(() -> 0); + // :: error: [purity.assign.field] + receiver.apply(() -> count++); + } + + /** The interface method requires what Base.apply assumes. */ + interface Annotated { + @SideEffectFree + int apply(Supplier s); + } + + static class SubOfAnnotated extends Base implements Annotated {} + + void callsThroughAnnotatedInterface(Annotated receiver) { + receiver.apply(() -> 0); + // :: error: [purity.assign.field] + receiver.apply(() -> count++); + } + + /** + * An override, like an inherited method, may assume what its own annotation requires of its + * argument: a call through Unannotated does not rely on the override's purity. + */ + static class SubOverrides extends Base implements Unannotated { + @Override + @SideEffectFree + public int apply(Supplier s) { + return s.get(); + } + } + + /** The body relies on the purity of calls to u.apply, but those calls check no argument. */ + @SideEffectFree + static int runsUnannotated(Unannotated u) { + return u.apply(() -> 0); + } + + /** The body relies on the purity of calls to a.apply, and those calls check their argument. */ + @SideEffectFree + static int runsAnnotated(Annotated a) { + return a.apply(() -> 0); + } + + void passesImplementations() { + // :: error: [purity.functional.argument.parameter] + runsUnannotated(new Sub()); + // :: error: [purity.functional.argument.parameter] + runsUnannotated(new SubOverrides()); + runsAnnotated(new SubOfAnnotated()); + } +} diff --git a/framework/tests/flow/PurityFunctionalMethodRef.java b/framework/tests/flow/PurityFunctionalMethodRef.java new file mode 100644 index 000000000000..c085b2338b5e --- /dev/null +++ b/framework/tests/flow/PurityFunctionalMethodRef.java @@ -0,0 +1,67 @@ +// A method reference is checked like an override: a call through the functional method that relies +// on the referenced method's purity must check the arguments to the referenced method's +// functional-interface parameters at least as strictly as a call to the referenced method does. + +import java.util.function.Function; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalMethodRef { + + @SideEffectFree + static int applyStatic(Function f, String s) { + return f.apply(s); + } + + @SideEffectFree + int applyInstance(Function f, String s) { + return f.apply(s); + } + + interface Applier { + @SideEffectFree + int apply(Function f, String s); + } + + interface UnboundApplier { + @SideEffectFree + int apply(PurityFunctionalMethodRef receiver, Function f, String s); + } + + interface GenericApplier { + @SideEffectFree + int apply(T f, String s); + } + + interface UnannotatedApplier { + int apply(Function f, String s); + } + + void references() { + Applier a = PurityFunctionalMethodRef::applyStatic; + Applier b = this::applyInstance; + UnboundApplier c = PurityFunctionalMethodRef::applyInstance; + // :: error: [purity.methodref.functional.parameter] + GenericApplier> d = PurityFunctionalMethodRef::applyStatic; + // A call through UnannotatedApplier checks nothing, but it also does not rely on the purity of + // the referenced method. + UnannotatedApplier e = PurityFunctionalMethodRef::applyStatic; + } + + /** The body relies on the purity of calls to u.apply, but those calls check no argument. */ + @SideEffectFree + static int runsUnannotated(UnannotatedApplier u) { + return u.apply(s -> 0, ""); + } + + /** The body relies on the purity of calls to a.apply, and those calls check their argument. */ + @SideEffectFree + static int runsAnnotated(Applier a) { + return a.apply(s -> 0, ""); + } + + void passesReferences() { + // :: error: [purity.methodref.functional.parameter] + runsUnannotated(PurityFunctionalMethodRef::applyStatic); + runsAnnotated(PurityFunctionalMethodRef::applyStatic); + } +} diff --git a/framework/tests/flow/PurityFunctionalOverride.java b/framework/tests/flow/PurityFunctionalOverride.java new file mode 100644 index 000000000000..7acfda4d1ef9 --- /dev/null +++ b/framework/tests/flow/PurityFunctionalOverride.java @@ -0,0 +1,105 @@ +// The obligation on the arguments of a @SideEffectFree method with a functional-interface +// parameter is inherited by an override, and a call site is checked against the declaration of its +// static target. +// +// Each call site is in an unannotated method, so the only diagnostic a call site can produce is the +// one under test. + +import java.util.function.Function; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalOverride { + + int count = 0; + + static class Super { + @SideEffectFree + int m(Function f, String s) { + return 0; + } + } + + static class Sub extends Super { + /** + * @SideEffectFree is inherited, so this body is checked as side-effect-free. + */ + @Override + int m(Function f, String s) { + return f.apply(s); + } + } + + static class SubRepeatsAnnotation extends Super { + @Override + @SideEffectFree + int m(Function f, String s) { + return 0; + } + } + + void callsThroughSupertype(Super receiver, String s) { + receiver.m(t -> t.length(), s); + // :: error: [purity.assign.field] + receiver.m(t -> count++, s); + } + + void callsThroughSubtype(Sub receiver, String s) { + receiver.m(t -> t.length(), s); + // :: error: [purity.assign.field] + receiver.m(t -> count++, s); + } + + /** An unrelated unannotated method imposes nothing on its arguments. */ + static class Unrelated { + int m(Function f, String s) { + return 0; + } + } + + void callsUnannotated(Unrelated receiver, String s) { + receiver.m(t -> count++, s); + } + + static class GenericSuper { + @SideEffectFree + int m(T f, String s) { + return 0; + } + } + + /** + * A call through GenericSuper whose type argument is a type variable checks nothing about the + * argument, so this body cannot assume that the argument is side-effect-free. + */ + static class GenericSub extends GenericSuper> { + @Override + int m(Function f, String s) { + // :: error: [purity.call] + return f.apply(s); + } + } + + /** The argument is checked against the type of the parameter at the call site. */ + void callsThroughGenericSupertype(GenericSuper> receiver, String s) { + receiver.m(t -> t.length(), s); + // :: error: [purity.assign.field] + receiver.m(t -> count++, s); + } + + /** Here the type of the parameter at the call site is T, which is not a functional interface. */ + void callsThroughTypeVariable(GenericSuper receiver, T f, String s) { + receiver.m(f, s); + } + + /** + * A call through Unrelated checks nothing, but it also does not rely on the override's purity, so + * the override may assume what its own annotation requires. + */ + static class AnnotatedSubOfUnrelated extends Unrelated { + @Override + @SideEffectFree + int m(Function f, String s) { + return f.apply(s); + } + } +} diff --git a/framework/tests/flow/PurityFunctionalParameter.java b/framework/tests/flow/PurityFunctionalParameter.java new file mode 100644 index 000000000000..7bcc8c72c84e --- /dev/null +++ b/framework/tests/flow/PurityFunctionalParameter.java @@ -0,0 +1,131 @@ +// A @SideEffectFree method may call the functional method of one of its own functional-interface +// parameters; that call is side-effect-free. +// +// The receiver must be the parameter itself. A local that aliases the parameter, a field, or the +// result of a call does not get the assumption, and neither does a method of the parameter's type +// other than the functional method. + +import java.util.function.Function; +import java.util.function.Supplier; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalParameter { + + int count = 0; + + @FunctionalInterface + interface PureFunc { + @SideEffectFree + String get(); + } + + Function field = s -> s.length(); + + PureFunc pureField = () -> ""; + + @SideEffectFree + Function lengthFunction() { + return s -> s.length(); + } + + // The receiver is one of this method's own parameters. + + @SideEffectFree + int callsParameter(Function f, String s) { + return f.apply(s); + } + + @SideEffectFree + int callsTwoParameters(Function f, Function g, String s) { + int a = f.apply(s); + int b = g.apply(s); + return a + b; + } + + @SideEffectFree + String callsSupplierParameter(Supplier supplier) { + return supplier.get(); + } + + /** A parameter whose type is a type variable bounded by a functional interface. */ + @SideEffectFree + > int callsTypeVariableParameter(T f, String s) { + return f.apply(s); + } + + // Only the parameter itself is a legal receiver. + + @SideEffectFree + int callsField(String s) { + // :: error: [purity.call] + return field.apply(s); + } + + @SideEffectFree + int callsLocalAlias(Function f, String s) { + Function g = f; + // :: error: [purity.call] + return g.apply(s); + } + + @SideEffectFree + int callsCallResult(String s) { + // :: error: [purity.call] + return lengthFunction().apply(s); + } + + // A functional interface whose functional method is annotated is side-effect-free wherever it is + // called, so these need no assumption about the receiver. + + @SideEffectFree + String annotatedSamField() { + return pureField.get(); + } + + @SideEffectFree + String annotatedSamLocalAlias(PureFunc p) { + PureFunc q = p; + return q.get(); + } + + @SideEffectFree + String annotatedSamTypeVariable(T t) { + return t.get(); + } + + // Only the functional method is assumed side-effect-free. + + @SideEffectFree + Function callsNonFunctionalMethod( + Function f, Function g) { + // :: error: [purity.call] + return f.andThen(g); + } + + /** The rest of the body is checked as usual. */ + @SideEffectFree + int otherEffectsAreReported(Function f, String s) { + // :: error: [purity.assign.field] + count++; + return f.apply(s); + } + + // A lambda's body may call a functional-interface parameter of the enclosing method, which + // holds a value that the caller of that method was required to check, whenever the lambda runs. + + @SideEffectFree + PureFunc lambdaCallsParameter(Function f) { + return () -> "" + f.apply("x"); + } + + PureFunc unannotatedMethodsLambdaCallsParameter(Function f) { + // :: error: [purity.call] + return () -> "" + f.apply("x"); + } + + /** An unannotated method promises nothing, so nothing in its body is checked. */ + int unannotatedEnclosingMethod(Function f, String s) { + count++; + return f.apply(s); + } +} diff --git a/framework/tests/flow/PurityFunctionalSubinterface.java b/framework/tests/flow/PurityFunctionalSubinterface.java new file mode 100644 index 000000000000..e7d38290243b --- /dev/null +++ b/framework/tests/flow/PurityFunctionalSubinterface.java @@ -0,0 +1,110 @@ +// A functional subinterface may implement a functional method of its superinterface with a default +// method, and declare an unrelated abstract method as its own functional method. An argument of +// the subinterface's type runs the default method, so it is checked against that method. + +import java.util.function.Function; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityFunctionalSubinterface { + + static int count = 0; + + /** G's functional method is other, which is unrelated to Function.apply. */ + interface G extends Function { + @Override + default Integer apply(String s) { + return count++; + } + + @SideEffectFree + int other(); + } + + /** Like G, but its implementation of Function.apply is side-effect-free. */ + interface PureApply extends Function { + @Override + @SideEffectFree + default Integer apply(String s) { + return 0; + } + + int other(); + } + + @SideEffectFree + static int m(Function f, String s) { + return f.apply(s); + } + + void passes(G g, PureApply p) { + // :: error: [purity.functional.argument] + m(g, ""); + m(p, ""); + } + + /** The caller of forwards checked the code that g.other runs, not the code that g.apply runs. */ + @SideEffectFree + static int forwards(G g, String s) { + // :: error: [purity.functional.argument] + return m(g, s); + } + + /** R's functional method is go, which is unrelated to Runnable.run. */ + interface R extends Runnable { + @Override + default void run() { + count++; + } + + @SideEffectFree + void go(); + } + + /** Like R, but its implementation of Runnable.run is side-effect-free. */ + interface PureRun extends Runnable { + @Override + @SideEffectFree + default void run() {} + + void go(); + } + + @SideEffectFree + static void takesRunnable(Runnable r) {} + + /** A lambda cast to R implements R.go, but a call to r.run() runs R.run. */ + void castsCode() { + // :: error: [purity.functional.argument] + takesRunnable((R) () -> {}); + // :: error: [purity.functional.argument] + takesRunnable((R) PurityFunctionalSubinterface::pureStatic); + takesRunnable((PureRun) () -> {}); + } + + @SideEffectFree + static void pureStatic() {} + + interface Takes { + @SideEffectFree + void take(F f); + } + + /** + * A call through Takes checks its argument against Runnable.run, not against PureRun.go, so the + * body may not assume that p.go() is side-effect-free. + */ + static class TakesPureRun implements Takes { + @Override + @SideEffectFree + public void take(PureRun p) { + // :: error: [purity.call] + p.go(); + } + } + + /** A lambda passed to Takes.take implements R.go, but the callee runs R.run. */ + void passesToInstantiation(Takes t) { + // :: error: [purity.functional.argument] + t.take(() -> {}); + } +} diff --git a/framework/tests/flow/PurityInheritedImplementationGap.java b/framework/tests/flow/PurityInheritedImplementationGap.java new file mode 100644 index 000000000000..dd6ca09e39f7 --- /dev/null +++ b/framework/tests/flow/PurityInheritedImplementationGap.java @@ -0,0 +1,36 @@ +// An inherited implementation that assumes the purity of a functional-interface argument, paired +// with an interface method that only a subclass implements with it, is checked at a call whose +// parameter type is a type variable of the callee. + +import java.util.function.Supplier; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityInheritedImplementationGap { + static int count; + + static class Base { + @SideEffectFree + public int apply(Supplier s) { + return s.get(); + } + } + + interface I { + @SideEffectFree + int apply(T t); + } + + /** Base.apply implements I>.apply, but only in Sub. */ + static class Sub extends Base implements I> {} + + /** The parameter type at this call is T, which is inferred to be Supplier. */ + @SideEffectFree + static int callThrough(I i, T t) { + return i.apply(t); + } + + static int unannotated() { + // :: error: [purity.assign.field] + return callThrough(new Sub(), () -> count++); + } +} diff --git a/framework/tests/flow/PurityVarargsArgument.java b/framework/tests/flow/PurityVarargsArgument.java new file mode 100644 index 000000000000..70045a47be12 --- /dev/null +++ b/framework/tests/flow/PurityVarargsArgument.java @@ -0,0 +1,77 @@ +// At a call to a @SideEffectFree method, an argument passed to a varargs functional-interface +// parameter is checked like an argument passed to any other functional-interface parameter. +// +// In the expanded form of such a call, each argument from the varargs parameter onward is an +// element of its array, and is checked against the array's component type. In the array form, +// the argument is the array itself: each initializer of an array literal is checked, and the +// elements of any other array are unknown and therefore unchecked. + +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PurityVarargsArgument { + + int count = 0; + + @SideEffectFree + static int callee(Runnable... rs) { + return 0; + } + + @SideEffectFree + static int calleeWithLeadingParameter(String s, Runnable... rs) { + return 0; + } + + @SideEffectFree + static int calleeWithoutFunctionalParameter(String... ss) { + return 0; + } + + void impureVoid() { + count++; + } + + @SideEffectFree + void pureVoid() {} + + // The expanded form: each argument is one element of the array. + + void expandedForm(String s) { + callee(() -> {}); + callee(this::pureVoid); + // :: error: [purity.assign.field] + callee(() -> count++); + // :: error: [purity.functional.argument] + callee(this::impureVoid); + callee(this::pureVoid, this::pureVoid); + // :: error: [purity.functional.argument] + callee(this::pureVoid, this::impureVoid); + // :: error: [purity.functional.argument] + calleeWithLeadingParameter(s, this::impureVoid); + } + + void zeroVarargsArguments() { + callee(); + } + + // The array form: the argument is the array that the call passes. + + void arrayForm(Runnable[] rs) { + callee(new Runnable[] {this::pureVoid}); + // :: error: [purity.functional.argument] + callee(new Runnable[] {this::impureVoid}); + // :: error: [purity.assign.field] + callee(new Runnable[] {() -> count++}); + // An array that the call receives from elsewhere says nothing about its elements. + callee(rs); + // Neither does an array with no initializer. + callee(new Runnable[10]); + } + + // A varargs parameter whose component type is not a functional interface is unaffected. + + void notFunctional() { + calleeWithoutFunctionalParameter("a", "b"); + calleeWithoutFunctionalParameter(new String[] {"a"}); + } +} diff --git a/framework/tests/purity-assumptions/AssumeSideEffectFreeArguments.java b/framework/tests/purity-assumptions/AssumeSideEffectFreeArguments.java new file mode 100644 index 000000000000..2641afaf0731 --- /dev/null +++ b/framework/tests/purity-assumptions/AssumeSideEffectFreeArguments.java @@ -0,0 +1,65 @@ +// Under -AassumeSideEffectFree, an argument to a functional-interface parameter is checked with +// the same assumption whatever its form: a lambda body, a method reference, or the functional +// method of the argument's type. The assumption applies to every method, including one with no +// purity annotation, which is the case the option exists for: an unannotated library. + +import java.util.function.Function; +import org.checkerframework.dataflow.qual.Deterministic; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class AssumeSideEffectFreeArguments { + + int count = 0; + + @SideEffectFree + static int callee(Function f, String s) { + return 0; + } + + @Deterministic + int deterministicLength(String s) { + // The method's own body is still checked against its own annotation. + // :: error: [purity.assign.field] + count++; + return s.length(); + } + + int unannotatedLength(String s) { + count++; + return s.length(); + } + + /** A functional interface whose functional method promises determinism. */ + @FunctionalInterface + interface DeterministicFunction extends Function { + @Override + @Deterministic + Integer apply(String s); + } + + DeterministicFunction deterministicVariable = s -> s.length(); + + /** The side effect of a method that promises determinism is assumed away. */ + void annotatedMethods(String s) { + callee(t -> deterministicLength(t), s); + callee(this::deterministicLength, s); + callee(deterministicVariable, s); + } + + /** A method with no purity annotation gets the assumption too, in either form. */ + void unannotatedMethods(String s) { + callee(t -> unannotatedLength(t), s); + callee(this::unannotatedLength, s); + } + + /** A value of an unannotated functional-interface type gets the assumption too. */ + void unannotatedVariable(Function f, String s) { + callee(f, s); + } + + /** The assumption is that a called method has no side effect, not that this code has none. */ + void theAssumptionIsAboutCalls(String s) { + // :: error: [purity.assign.field] + callee(t -> count++, s); + } +}