From dddb8a39518b2e8e2b22c2f2c4dbd3b3937191e8 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 8 Jan 2024 21:28:12 -0800 Subject: [PATCH 001/110] Add start of manual for Non-Empty Checker --- docs/manual/introduction.tex | 3 ++ docs/manual/manual.tex | 1 + docs/manual/non-empty-checker.tex | 71 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 docs/manual/non-empty-checker.tex diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index e034e8f5b98..1201717fb86 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -48,6 +48,9 @@ \item \ahrefloc{index-checker}{Index Checker} for array accesses (see \chapterpageref{index-checker}) +\item + \ahrefloc{non-empty-checker}{Non-Empty Checker} to determine whether a + collection, iterator, iterable, or map is non-empty (see \chapterpageref{non-empty-checker}) \item \ahrefloc{regex-checker}{Regex Checker} to prevent use of syntactically invalid regular expressions (see \chapterpageref{regex-checker}) diff --git a/docs/manual/manual.tex b/docs/manual/manual.tex index 4a57d5c5214..80f6f0f7a37 100644 --- a/docs/manual/manual.tex +++ b/docs/manual/manual.tex @@ -58,6 +58,7 @@ \input{tainting-checker.tex} \input{lock-checker.tex} \input{index-checker.tex} +\input{non-empty-checker.tex} % These are focused on strings: \input{regex-checker.tex} diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex new file mode 100644 index 00000000000..27ef6a5bd56 --- /dev/null +++ b/docs/manual/non-empty-checker.tex @@ -0,0 +1,71 @@ +\htmlhr +\chapterAndLabel{Non-Empty Checker for collections, iterators, iterables, and maps}{non-empty-checker} + +The Non-Empty Checker warns about operations that depend on whether a +collection, iterator, iterable, or map is non-empty. + +To run the Non-Empty Checker, run either of these commands: + +\begin{alltt} + javac -processor nonempty \emph{MyJavaFile}.java + javac -processor org.checkerframework.checker.nonempty.NonEmptyChecker \emph{MyJavaFile}.java +\end{alltt} + +\sectionAndLabel{Non-Empty annotations}{non-empty-annotations} + +These qualifiers make up the Non-Empty type system: + +\begin{description} + +\item[\refqualclass{checker/nonempty/qual}{UnknownNonEmpty}] + The annotated collection, iterator, iterable, or map may or may not be empty. + This is the top type; programmers need not explicitly write it. + +\item[\refqualclass{checker/nonempty/qual}{NonEmpty}] + The annotated collection, iterator, iterable, or map is \emph{definitely} + non-empty. + +\item[\refqualclass{checker/nonempty/qual}{PolyNonEmpty}] + indicates qualifier polymorphism. + For a description of qualifier polymorphism, see + Section~\ref{method-qualifier-polymorphism}. + +\end{description} + +\sectionAndLabel{Annotating your code with \<@NonEmpty>}{annotating-with-non-empty} + +The default annotation for collections, iterators, iterables, and maps is +\<@UnknownNonEmpty>. +Refinement to the \<@NonEmpty> type occurs in certain cases, such as after +conditional checks for empty/non-emptiness (see~\ref{type-refinement} for +more details): + +\begin{Verbatim} + public List getSessionIds() { ... } + ... + List sessionIds = getSessionIds(); // sessionIds has type @UnknownNonEmpty + ... + if (!sessionIds.isEmpty()) { + List firstId = sessionIds.get(0); // OK, sessionIds has type @NonEmpty + ... + } +\end{Verbatim} + +Or on the result of a method that returns a non-empty collection: + +\begin{Verbatim} + List countryCodes1; // Has default type @UnknownNonEmpty + List countryCodes1 = List.of("CA", "US"); // Has type @NonEmpty +\end{Verbatim} + +A programmer can manually annotate code in cases where a collection, +iterator, iterable, or map is always known to be non-empty, but that fact is +unable to be inferred by the type system: + +\begin{Verbatim} + // This call always returns a non-empty map; there is always at least one user in the store + public @NonEmpty Map getUserMapping() { ... } + ... + Map users = getUserMapping(); // users has type @NonEmpty +\end{Verbatim} + From f5b118842da8d7895323c139acc29c6f5b5db728 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 9 Jan 2024 09:47:17 -0800 Subject: [PATCH 002/110] Add section on Non-Empty Checker checks --- docs/manual/non-empty-checker.tex | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index 27ef6a5bd56..69222aad8a3 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -69,3 +69,22 @@ Map users = getUserMapping(); // users has type @NonEmpty \end{Verbatim} +\sectionAndLabel{What the Non-Empty Checker checks}{non-empty-checker-checks} + +The Non-Empty Checker ensures that collections, iterators, iterables, or maps +are non-empty at certain points in a program. +If a program type-checks cleanly under the Non-Empty Checker (i.e., no errors +are issued by the checker), then the program is certified with a compile-time +guarantee of the absence of errors rooted in the use of operations that +rely on whether a collection is non-empty. +For example, calling \ on an iterator that is known to be \<@NonEmpty> +should never fail, or, getting the first element of a \<@NonEmpty> list should +not throw an exception. + +The Non-Empty Checker does \emph{not} provide guarantees about the fixed +length or size of collections, iterators, iterables, or maps, beyond whether +it has a length of at least 1 (i.e., it is non-empty). +The Index Checker~(See Chapter~\ref{index-checker}) is a checker that analyzes +array bounds and indices and warns about potential +\s. + From d7e07e77616f93b0902779e5cf6f42cc85e3a2c2 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 9 Jan 2024 15:20:21 -0800 Subject: [PATCH 003/110] Adding type hierarchy image --- docs/manual/figures/nonempty-subtyping.svg | 157 +++++++++++++++++++++ docs/manual/non-empty-checker.tex | 48 ++++++- 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 docs/manual/figures/nonempty-subtyping.svg diff --git a/docs/manual/figures/nonempty-subtyping.svg b/docs/manual/figures/nonempty-subtyping.svg new file mode 100644 index 00000000000..84314d3700c --- /dev/null +++ b/docs/manual/figures/nonempty-subtyping.svg @@ -0,0 +1,157 @@ + + + + + + image/svg+xml + + + + + + + + + + @UnknownNonEmpty + + + + + @NonEmpty + + + + + + + + + diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index 69222aad8a3..9d7c13290ba 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -32,6 +32,12 @@ \end{description} +\begin{figure} +\includeimage{nonempty-subtyping}{3.75cm} +\caption{The subtyping relationship of the Non-Empty Checker's qualifiers.} +\label{fig-nonempty-hierarchy} +\end{figure} + \sectionAndLabel{Annotating your code with \<@NonEmpty>}{annotating-with-non-empty} The default annotation for collections, iterators, iterables, and maps is @@ -83,8 +89,48 @@ The Non-Empty Checker does \emph{not} provide guarantees about the fixed length or size of collections, iterators, iterables, or maps, beyond whether -it has a length of at least 1 (i.e., it is non-empty). +it has a length or size of at least 1 (i.e., it is non-empty). The Index Checker~(See Chapter~\ref{index-checker}) is a checker that analyzes array bounds and indices and warns about potential \s. +\sectionAndLabel{Suppressing non-empty warnings}{suppressing-non-empty-warnings} + +Like any sound static analysis tool, the Non-Empty Checker may issue a warning +for code that is correct. +It is often best to change your code or annotations in this case. +Alternatively, you may choose to suppress the warning. +This does not change the code, but prevents the warning from being presented to +you. +The Checker Framework supplies several mechanisms to suppress warnings. +See Chapter~\ref{suppressing-warnings} for additional usages. +The \<@SuppressWarnings("nonempty")> annotation is specific to warnings raised +by the Non-Empty Checker: + +\begin{Verbatim} + // This method might return an empty list, depending on the argument + List getRegionIds(String region) { ... } + + void parseRegions() { + @SuppressWarnings("nonempty") // A non-empty list is returned when getRegionIds is invoked with argument x + @NonEmpty List regionIds = getRegionIds(x); + } +\end{Verbatim} + +\subsectionAndLabel{Suppressing warnings with assertions}{suppressing-warnings-with-assertions} + +Occasionally, it is inconvenient or verbose to use the \<@SuppressWarnings> +annotation. +For example, Java does not permit annotations such as \<@SuppressWarnings> to +appear on expressions, static initializers, etc. +Here are two ways to suppress a warning in such cases: + +\begin{itemize} +\item + Create a local variable to hold a subexpression, and + suppress a warning on the local variable declaration. +\item + Use the \<@AssumeAssertion> string in + an \ message (see Section~\ref{assumeassertion}). +\end{itemize} + From ce3cde344ce0e2a3a949298fed5004c045a26b8f Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 9 Jan 2024 19:20:57 -0800 Subject: [PATCH 004/110] Add qualifiers and barebones Non-Empty Checker --- .../checker/nonempty/qual/NonEmpty.java | 12 ++++++++++++ .../nonempty/qual/UnknownNonEmpty.java | 14 ++++++++++++++ .../checker/nonempty/NonEmptyChecker.java | 5 +++++ .../checker/test/junit/NonEmptyTest.java | 19 +++++++++++++++++++ .../tests/nonempty/NonEmptyHierarchyTest.java | 13 +++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java create mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java create mode 100644 checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java create mode 100644 checker/tests/nonempty/NonEmptyHierarchyTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java new file mode 100644 index 00000000000..d59ca5ebe5b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java @@ -0,0 +1,12 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) +@SubtypeOf(UnknownNonEmpty.class) +public @interface NonEmpty {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java new file mode 100644 index 00000000000..54bd4f7fd7a --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java @@ -0,0 +1,14 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) +@DefaultQualifierInHierarchy +@SubtypeOf({}) +public @interface UnknownNonEmpty {} diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java new file mode 100644 index 00000000000..e0570476961 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java @@ -0,0 +1,5 @@ +package org.checkerframework.checker.nonempty; + +import org.checkerframework.common.basetype.BaseTypeChecker; + +public class NonEmptyChecker extends BaseTypeChecker {} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java new file mode 100644 index 00000000000..30c87bf882b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +/** JUnit tests for the Non-Empty Checker */ +public class NonEmptyTest extends CheckerFrameworkPerDirectoryTest { + + public NonEmptyTest(List testFiles) { + super(testFiles, org.checkerframework.checker.nonempty.NonEmptyChecker.class, "nonempty"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nonempty"}; + } +} diff --git a/checker/tests/nonempty/NonEmptyHierarchyTest.java b/checker/tests/nonempty/NonEmptyHierarchyTest.java new file mode 100644 index 00000000000..226976ad347 --- /dev/null +++ b/checker/tests/nonempty/NonEmptyHierarchyTest.java @@ -0,0 +1,13 @@ +import java.util.List; +import org.checkerframework.checker.nonempty.qual.NonEmpty; +import org.checkerframework.checker.nonempty.qual.UnknownNonEmpty; + +class NonEmptyHierarchyTest { + + void testAssignments(@NonEmpty List l1, @UnknownNonEmpty List l2) { + @NonEmpty List l3 = l1; // OK, both are @NonEmpty + + // :: error: (assignment) + @NonEmpty List l4 = l2; // Error for this line + } +} From 61ce73325162c4ad9259b070dd23073d0fac6bbb Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 9 Jan 2024 19:34:28 -0800 Subject: [PATCH 005/110] Add documentation for quals --- .../checkerframework/checker/nonempty/qual/NonEmpty.java | 6 ++++++ .../checker/nonempty/qual/UnknownNonEmpty.java | 6 ++++++ .../checkerframework/checker/test/junit/NonEmptyTest.java | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java index d59ca5ebe5b..5928e166023 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java @@ -6,6 +6,12 @@ import java.lang.annotation.Target; import org.checkerframework.framework.qual.SubtypeOf; +/** + * The {@link java.util.Collection Collection}, {@link java.util.Iterator Iterator}, {@link + * java.lang.Iterable Iterable}, or {@link java.util.Map Map} is definitely non-empty. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @SubtypeOf(UnknownNonEmpty.class) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java index 54bd4f7fd7a..a6364c06330 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java @@ -7,6 +7,12 @@ import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.SubtypeOf; +/** + * The {@link java.util.Collection Collection}, {@link java.util.Iterator Iterator}, {@link + * java.lang.Iterable Iterable}, or {@link java.util.Map Map} may or may not be empty. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @DefaultQualifierInHierarchy diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java index 30c87bf882b..fa061b632e7 100644 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java @@ -8,6 +8,11 @@ /** JUnit tests for the Non-Empty Checker */ public class NonEmptyTest extends CheckerFrameworkPerDirectoryTest { + /** + * Create a NonEmptyTest. + * + * @param testFiles the files containing test code to be type-checked + */ public NonEmptyTest(List testFiles) { super(testFiles, org.checkerframework.checker.nonempty.NonEmptyChecker.class, "nonempty"); } From e20d5afdef31fb6a02d2f21f83312b8773cb1d0e Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 10 Jan 2024 12:58:58 -0800 Subject: [PATCH 006/110] Add postcondition annotations for the Non-Empty Checker --- .../nonempty/qual/EnsuresNonEmpty.java | 50 ++++++++++ .../nonempty/qual/EnsuresNonEmptyIf.java | 91 +++++++++++++++++++ .../checker/nonempty/qual/NonEmpty.java | 2 + .../nonempty/qual/UnknownNonEmpty.java | 2 + 4 files changed, 145 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java new file mode 100644 index 00000000000..a4d324b4163 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java @@ -0,0 +1,50 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.PostconditionAnnotation; + +/** + * Indicates that the expression evaluates to a non-empty collection, iterator, iterable, or map, if + * the method terminates successfully. + * + *

This postcondition annotation is useful for methods that construct a non-empty collection, + * iterator, iterable, or map: + * + *


+ *   {@literal @}EnsuresNonEmpty("ids")
+ *   void addId(String id) {
+ *     ids.add(id);
+ *   }
+ * 
+ * + * It can also be used for a method that fails if a given collection, iterator, iterable, or map is + * empty, indicating that the argument is non-empty if the method returns normally: + * + *

+ *   /** Throws an exception if the argument is empty. */
+ *   {@literal @}EnsuresNonEmpty("#1")
+ *   void useTheMap(Map<T, U> arg) { ... }
+ * 
+ * + * @see NonEmpty + * @see org.checkerframework.checker.nonempty.NonEmptyChecker + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@PostconditionAnnotation(qualifier = NonEmpty.class) +@InheritedAnnotation +public @interface EnsuresNonEmpty { + /** + * The expression (a collection, iterator, iterable, or map) that is non-empty, if the method + * returns normally. + * + * @return the expression (a collection, iterator, iterable, or map) that is non-empty, if the + * method returns normally + */ + String[] value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java new file mode 100644 index 00000000000..267d35cf809 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java @@ -0,0 +1,91 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; + +/** + * Indicates that the given expressions which may be {@link java.util.Collection collections}, + * {@link java.util.Iterator iterators}, {@link java.lang.Iterable iterables}, or {@link + * java.util.Map maps} are non-empty, if the method returns the given result (either true or false). + * + *

Here are ways this conditional postcondition annotation can be used: + * + *

Method parameters: Suppose that a method has a parameter that is a list, and returns + * true if the length of the list is non-zero. You could annotate the method as follows: + * + *

 @EnsuresNonEmptyIf(result = true, expression = "#1")
+ *  public <T> boolean isLengthGreaterThanZero(List<T> items) { ... }
+ * 
+ * + * because, if {@code isLengthGreaterThanZero} returns true, then {@code items} was non-empty. Note + * that you can write more than one {@code @EnsuresNonEmptyIf} annotations on a single method. + * + *

Fields: The value expression can refer to fields, even private ones. For example: + * + *

 @EnsuresNonEmptyIf(result = true, expression = "this.orders")
+ *  public <T> boolean areOrdersActive() {
+ *    return this.orders != null && this.orders.size() > 0;
+ * }
+ * + * An {@code @EnsuresNonEmptyIf} annotation that refers to a private field is useful for verifying + * that a method establishes a property, even though client code cannot directly affect the field. + * + *

Method postconditions: Suppose that if a method {@code areOrdersActive()} returns + * true,p then {@code getOrders()} will return a non-empty Map. You can express this relationship + * as: + * + *

 @EnsuresNonEmptyIf(result = true, expression = "this.getOrders()")
+ *  public <T> boolean areOrdersActive() {
+ *    return this.orders != null && this.orders.size() > 0;
+ * }
+ * + * @see NonEmpty + * @see EnsuresNonEmpty + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@ConditionalPostconditionAnnotation(qualifier = NonEmpty.class) +@InheritedAnnotation +public @interface EnsuresNonEmptyIf { + + /** + * Returns the return value of the method for which the postcondition holds. + * + * @return the return value of the method for which the postcondition holds + */ + boolean result(); + + /** + * Returns the Java expressions which may be {@link java.util.Collection collections}, {@link + * java.util.Iterator iterators}, {@link java.lang.Iterable iterables}, or {@link java.util.Map + * maps} that are non-empty after the method returns the given result. + * + * @return the Java expressions that are non-empty after the method returns the given result + */ + String[] expression(); + + /** + * A wrapper annotation that makes the {@link EnsuresNonEmptyIf} annotation repeatable. + * + *

Programmers generally do not need to write ths. It is created by Java when a programmer + * writes more than one {@link EnsuresNonEmptyIf} annotation at the same location. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @ConditionalPostconditionAnnotation(qualifier = NonEmpty.class) + @interface List { + /** + * Returns the repeatable annotations. + * + * @return the repeatable annotations + */ + EnsuresNonEmptyIf[] value(); + } +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java index 5928e166023..d5a7f8c15d5 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.nonempty.qual; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,6 +13,7 @@ * * @checker_framework.manual #non-empty-checker Non-Empty Checker */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @SubtypeOf(UnknownNonEmpty.class) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java index a6364c06330..65900f7fa12 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.nonempty.qual; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -13,6 +14,7 @@ * * @checker_framework.manual #non-empty-checker Non-Empty Checker */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @DefaultQualifierInHierarchy From 433a993a858ed7fa4bf7d9bf741db7ea4fccfb61 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 10 Jan 2024 15:37:45 -0800 Subject: [PATCH 007/110] Add postcondition annotations and tests --- .../nonempty/qual/EnsuresNonEmpty.java | 5 +- .../nonempty/qual/RequiresNonEmpty.java | 103 ++++++++++++++++++ .../tests/nonempty/NonEmptyHierarchyTest.java | 4 +- .../tests/nonempty/RequiresNonEmptyTest.java | 49 +++++++++ docs/manual/non-empty-checker.tex | 23 ++++ 5 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java create mode 100644 checker/tests/nonempty/RequiresNonEmptyTest.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java index a4d324b4163..a5754473767 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java @@ -8,8 +8,9 @@ import org.checkerframework.framework.qual.PostconditionAnnotation; /** - * Indicates that the expression evaluates to a non-empty collection, iterator, iterable, or map, if - * the method terminates successfully. + * Indicates that the expression evaluates to a non-empty {@link java.util.Collection collection}, + * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map + * map}, if the method terminates successfully. * *

This postcondition annotation is useful for methods that construct a non-empty collection, * iterator, iterable, or map: diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java new file mode 100644 index 00000000000..f98e12f01de --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java @@ -0,0 +1,103 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.PreconditionAnnotation; + +/** + * Indicates a method precondition: the specified expressions that may be a {@link + * java.util.Collection collection}, {@link java.util.Iterator iterator}, {@link java.lang.Iterable + * iterable}, or {@link java.util.Map map} must be non-empty when the annotated method is invoked. + * + *

For example: + * + *

+ * import java.util.LinkedList;
+ * import java.util.List;
+ * import org.checkerframework.checker.nonempty.qual.NonEmpty;
+ * import org.checkerframework.checker.nonempty.qual.RequiresNonEmpty;
+ * import org.checkerframework.dataflow.qual.Pure;
+ *
+ * class MyClass {
+ *
+ *   List<String> list1 = new LinkedList<>();
+ *   List<String> list2;
+ *
+ *     @RequiresNonEmpty("list1")
+ *     @Pure
+ *   void m1() {}
+ *
+ *     @RequiresNonEmpty({"list1", "list2"})
+ *     @Pure
+ *   void m2() {}
+ *
+ *     @RequiresNonEmpty({"list1", "list2"})
+ *   void m3() {}
+ *
+ *   void m4() {}
+ *
+ *   void test(@NonEmpty List<String> l1, @NonEmpty List<String> l2) {
+ *     MyClass testClass = new MyClass();
+ *
+ *     // At this point, we should have an error since m1 requires that list1 is @NonEmpty, which is
+ *     // not the case here
+ *     // :: error: (contracts.precondition)
+ *     testClass.m1();
+ *
+ *     testClass.list1 = l1;
+ *     testClass.m1(); // OK
+ *
+ *     // A call to m2 is stil illegal here, since list2 is still @UnknownNonEmpty
+ *     // :: error: (contracts.precondition)
+ *     testClass.m2();
+ *
+ *     testClass.list2 = l2;
+ *     testClass.m2(); // OK
+ *
+ *     testClass.m4();
+ *
+ *     // No longer OK to call m2, no guarantee that m4() was pure
+ *     // :: error: (contracts.precondition)
+ *     testClass.m2();
+ *   }
+ * }
+ * 
+ * + * This annotation should not be used for formal parameters (instead, give them a {@code @NonEmpty} + * type). The {@code @RequiresNonEmpty} annotation is intended for non-parameter expressions, such + * as field accesses or method calls. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.PARAMETER}) +@PreconditionAnnotation(qualifier = NonEmpty.class) +public @interface RequiresNonEmpty { + + /** + * The Java {@link java.util.Collection collection}, + * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map + * map} that must be non-empty. + * + * @return the Java {@link java.util.Collection collection}, + * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map map + */ + String[] value(); + + /** + * A wrapper annotation that makes the {@link RequiresNonEmpty} annotation repeatable. + * + *

Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link RequiresNonEmpty} annotation at the same location. + */ + @interface List { + /** + * Returns the repeatable annotations. + * + * @return the repeatable annotations + */ + RequiresNonEmpty[] value(); + } +} diff --git a/checker/tests/nonempty/NonEmptyHierarchyTest.java b/checker/tests/nonempty/NonEmptyHierarchyTest.java index 226976ad347..a68659d84d7 100644 --- a/checker/tests/nonempty/NonEmptyHierarchyTest.java +++ b/checker/tests/nonempty/NonEmptyHierarchyTest.java @@ -8,6 +8,8 @@ void testAssignments(@NonEmpty List l1, @UnknownNonEmpty List l2 @NonEmpty List l3 = l1; // OK, both are @NonEmpty // :: error: (assignment) - @NonEmpty List l4 = l2; // Error for this line + @NonEmpty List l4 = l2; + + List l5 = l1; // l5 implicitly has type @UnknownNonEmpty, assigning l1 to it is legal } } diff --git a/checker/tests/nonempty/RequiresNonEmptyTest.java b/checker/tests/nonempty/RequiresNonEmptyTest.java new file mode 100644 index 00000000000..b1fad95613b --- /dev/null +++ b/checker/tests/nonempty/RequiresNonEmptyTest.java @@ -0,0 +1,49 @@ +import java.util.LinkedList; +import java.util.List; +import org.checkerframework.checker.nonempty.qual.NonEmpty; +import org.checkerframework.checker.nonempty.qual.RequiresNonEmpty; +import org.checkerframework.dataflow.qual.Pure; + +class MyClass { + + List list1 = new LinkedList<>(); + List list2; + + @RequiresNonEmpty("list1") + @Pure + void m1() {} + + @RequiresNonEmpty({"list1", "list2"}) + @Pure + void m2() {} + + @RequiresNonEmpty({"list1", "list2"}) + void m3() {} + + void m4() {} + + void test(@NonEmpty List l1, @NonEmpty List l2) { + MyClass testClass = new MyClass(); + + // At this point, we should have an error since m1 requires that list1 is @NonEmpty, which is + // not the case here + // :: error: (contracts.precondition) + testClass.m1(); + + testClass.list1 = l1; + testClass.m1(); // OK + + // A call to m2 is stil illegal here, since list2 is still @UnknownNonEmpty + // :: error: (contracts.precondition) + testClass.m2(); + + testClass.list2 = l2; + testClass.m2(); // OK + + testClass.m4(); + + // No longer OK to call m2, no guarantee that m4() was pure + // :: error: (contracts.precondition) + testClass.m2(); + } +} diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index 9d7c13290ba..eccf80fbc77 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -38,6 +38,29 @@ \label{fig-nonempty-hierarchy} \end{figure} +\subsectionAndLabel{Non-Empty method annotations}{non-empty-method-annotations} + +The Non-Empty Checker supports several annotations that specify method +behavior. These are declaration annotations, not type annotations; they +apply to the annotated method itself rather than to some particular type. + +\begin{description} + +\item[\refqualclass{checker/nonempty/qual}{RequiresNonEmpty}] + indicates a method precondition: The annotated method expects the + specified expresssion to be non-empty when the + method is invoked. \<@RequiresNonEmpty> may be appropriate for + a field that may not always be non-empty, but the annotated method requires + the field to be non-empty. + +\item[\refqualclass{checker/nonempty/qual}{EnsuresNonEmpty}] + Todo + +\item[\refqualclass{checker/nonempty/qual}{EnsuresNonEmptyIf}] + Todo + +\end{description} + \sectionAndLabel{Annotating your code with \<@NonEmpty>}{annotating-with-non-empty} The default annotation for collections, iterators, iterables, and maps is From 134aef7b79ba48eae91e7d2fa3cb8363c2b54ba2 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 10 Jan 2024 15:56:11 -0800 Subject: [PATCH 008/110] Fix Javadoc build errors --- .../checker/nonempty/qual/EnsuresNonEmptyIf.java | 4 ++-- .../checker/nonempty/qual/RequiresNonEmpty.java | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java index 267d35cf809..c761c93feb2 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java @@ -29,7 +29,7 @@ * *

 @EnsuresNonEmptyIf(result = true, expression = "this.orders")
  *  public <T> boolean areOrdersActive() {
- *    return this.orders != null && this.orders.size() > 0;
+ *    return this.orders != null && this.orders.size() > 0;
  * }
* * An {@code @EnsuresNonEmptyIf} annotation that refers to a private field is useful for verifying @@ -41,7 +41,7 @@ * *
 @EnsuresNonEmptyIf(result = true, expression = "this.getOrders()")
  *  public <T> boolean areOrdersActive() {
- *    return this.orders != null && this.orders.size() > 0;
+ *    return this.orders != null && this.orders.size() > 0;
  * }
* * @see NonEmpty diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java index f98e12f01de..7800cd6df1a 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.nonempty.qual; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -22,7 +23,7 @@ * * class MyClass { * - * List<String> list1 = new LinkedList<>(); + * List<String> list1 = new LinkedList<>(); * List<String> list2; * *   @RequiresNonEmpty("list1") @@ -71,6 +72,7 @@ * * @checker_framework.manual #non-empty-checker Non-Empty Checker */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.PARAMETER}) @PreconditionAnnotation(qualifier = NonEmpty.class) @@ -82,7 +84,7 @@ * map} that must be non-empty. * * @return the Java {@link java.util.Collection collection}, - * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map map + * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map map} */ String[] value(); From cdf043dc5007532d8722e77b38093a68628a26d2 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 10 Jan 2024 17:03:41 -0800 Subject: [PATCH 009/110] Add formatting updates --- .../checker/nonempty/qual/RequiresNonEmpty.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java index 7800cd6df1a..832f21ccaa9 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java @@ -79,12 +79,11 @@ public @interface RequiresNonEmpty { /** - * The Java {@link java.util.Collection collection}, - * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map - * map} that must be non-empty. + * The Java {@link java.util.Collection collection}, {@link java.util.Iterator iterator}, {@link + * java.lang.Iterable iterable}, or {@link java.util.Map map} that must be non-empty. * - * @return the Java {@link java.util.Collection collection}, - * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map map} + * @return the Java {@link java.util.Collection collection}, {@link java.util.Iterator iterator}, + * {@link java.lang.Iterable iterable}, or {@link java.util.Map map} */ String[] value(); From eda22bbf77eb3fe1c39e597cfaea2e3a39d0104f Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 10 Jan 2024 17:16:51 -0800 Subject: [PATCH 010/110] Adding descriptions for `@EnsuresNonEmpty` and `@EnsuresNonEmptyIf` --- docs/manual/non-empty-checker.tex | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index eccf80fbc77..fa19ea97406 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -47,17 +47,23 @@ \begin{description} \item[\refqualclass{checker/nonempty/qual}{RequiresNonEmpty}] - indicates a method precondition: The annotated method expects the + indicates a method precondition. The annotated method expects the specified expresssion to be non-empty when the method is invoked. \<@RequiresNonEmpty> may be appropriate for a field that may not always be non-empty, but the annotated method requires the field to be non-empty. \item[\refqualclass{checker/nonempty/qual}{EnsuresNonEmpty}] - Todo + indicates a method postcondition. The successful return (i.e., a + non-exceptional return) of the annotated method results in the given + expression being non-empty. See the Javadoc for examples of its use. \item[\refqualclass{checker/nonempty/qual}{EnsuresNonEmptyIf}] - Todo + indicates a method postcondition. With \<@EnsuresNonEmpty>, the given + expression is non-empty after the method returns normally. With + \<@EnsuresNonEmptyIf>, if the annotated + method returns the given boolean value (true or false), then the given + expression is non-empty. See the Javadoc for examples of their use. \end{description} From 8824de7a7a2542c4a3945fda648a9ed33ca57418 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 10 Jan 2024 20:50:59 -0800 Subject: [PATCH 011/110] Adding tests for `@EnsuresNonEmpty` and `@EnsuresNonEmptyIf` --- .../tests/nonempty/EnsuresNonEmptyIfTest.java | 35 +++++++++++++++++++ .../tests/nonempty/EnsuresNonEmptyTest.java | 24 +++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 checker/tests/nonempty/EnsuresNonEmptyIfTest.java create mode 100644 checker/tests/nonempty/EnsuresNonEmptyTest.java diff --git a/checker/tests/nonempty/EnsuresNonEmptyIfTest.java b/checker/tests/nonempty/EnsuresNonEmptyIfTest.java new file mode 100644 index 00000000000..03598ff0d0c --- /dev/null +++ b/checker/tests/nonempty/EnsuresNonEmptyIfTest.java @@ -0,0 +1,35 @@ +import java.util.ArrayList; +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +// @skip-test until JDK is annotated with Non-Empty type qualifiers + +class EnsuresNonEmptyIfTest { + + @EnsuresNonEmptyIf(result = true, expression = "#1") + boolean m1(ArrayList l1) { + try { + l1.add("foo"); + return true; + } catch (Exception e) { + // As per the JDK documentation for Collections, an exception is thrown when adding to a + // collection fails + return false; + } + } + + void m2(@NonEmpty ArrayList l1) {} + + void test(ArrayList l1) { + // m2 requires a @NonEmpty collection, l1 has type @UnknownNonEmpty + // :: error: (argument) + m2(l1); + + if (!m1(l1)) { + // :: error: (argument) + m2(l1); + } else { + m2(l1); // OK + } + } +} diff --git a/checker/tests/nonempty/EnsuresNonEmptyTest.java b/checker/tests/nonempty/EnsuresNonEmptyTest.java new file mode 100644 index 00000000000..060e6472eda --- /dev/null +++ b/checker/tests/nonempty/EnsuresNonEmptyTest.java @@ -0,0 +1,24 @@ +import java.util.ArrayList; +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +// @skip-test until JDK is annotated with Non-Empty type qualifiers + +class EnsuresNonEmptyTest { + + @EnsuresNonEmpty("#1") + void m1(ArrayList l1) { + l1.add("foo"); + } + + void m2(@NonEmpty ArrayList l1) {} + + void test(ArrayList l1) { + // m2 requires a @NonEmpty collection, l1 has type @UnknownNonEmpty + // :: error: (argument) + m2(l1); + + m1(l1); + m2(l1); // OK + } +} From 5aeea646d0e0113a5ac60742fb14b81a01c78c00 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 11 Jan 2024 15:31:10 -0800 Subject: [PATCH 012/110] Tweaks to manual --- docs/manual/advanced-features.tex | 5 ++++- docs/manual/introduction.tex | 3 ++- docs/manual/non-empty-checker.tex | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 87430d02453..fee42c4c563 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -899,7 +899,7 @@ representation of a number and determine whether it is intended to represent kilometers or miles. -% Keep these lists in sync with the list in introduction.tex +% Keep these lists in sync with the list in introduction.tex . Type systems that support a run-time test are: \begin{itemize} @@ -919,6 +919,9 @@ \item \ahrefloc{index-checker}{Index Checker} for array accesses (see \chapterpageref{index-checker}) +\item + \ahrefloc{non-empty-checker}{Non-Empty Checker} to determine whether a + collection, iterator, iterable, or map is non-empty (see \chapterpageref{non-empty-checker}) \item \ahrefloc{resource-leak-checker}{Resource Leak Checker} for ensuring that resources are disposed of properly (see \chapterpageref{resource-leak-checker}) diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 1201717fb86..5cbb381343d 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -11,7 +11,8 @@ The Checker Framework comes with checkers for specific types of errors: \begin{enumerate} -% If you update this list, also update the list in advanced-features.tex . +% If you update this list, also update the list in advanced-features.tex , +% near the text "introduction.tex". \item \ahrefloc{nullness-checker}{Nullness Checker} for null pointer errors diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index fa19ea97406..13299690392 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -1,8 +1,18 @@ \htmlhr -\chapterAndLabel{Non-Empty Checker for collections, iterators, iterables, and maps}{non-empty-checker} +\chapterAndLabel{Non-Empty Checker for container classes}{non-empty-checker} + +The Non-Empty Checker tracks whether a container is possibly-empty or is +definitely non-empty. It works on containers such as +\s, \s, \s, and \s. + +If the Non-Empty Checker issues no warnings, then your program does not +throw \ as a result of calling a method such as + +Deque.getFirst +Deque.removeFirst +Queue.remove +Queue.element -The Non-Empty Checker warns about operations that depend on whether a -collection, iterator, iterable, or map is non-empty. To run the Non-Empty Checker, run either of these commands: From 500c249a396ee4ecd4727d278fc32f31dcc11abc Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 11 Jan 2024 18:43:31 -0800 Subject: [PATCH 013/110] Update formatting and code example --- docs/manual/non-empty-checker.tex | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index 13299690392..39840e414dd 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -6,13 +6,9 @@ \s, \s, \s, and \s. If the Non-Empty Checker issues no warnings, then your program does not -throw \ as a result of calling a method such as - -Deque.getFirst -Deque.removeFirst -Queue.remove -Queue.element - +throw \ as a result of calling methods such as +\, \, \, or +\. To run the Non-Empty Checker, run either of these commands: @@ -99,8 +95,8 @@ Or on the result of a method that returns a non-empty collection: \begin{Verbatim} - List countryCodes1; // Has default type @UnknownNonEmpty - List countryCodes1 = List.of("CA", "US"); // Has type @NonEmpty + List countryCodes; // Has default type @UnknownNonEmpty + List countryCodes = List.of("CA", "US"); // Has type @NonEmpty \end{Verbatim} A programmer can manually annotate code in cases where a collection, From c1703556a3ae744657b6aa14177c55002dccc75a Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 12 Jan 2024 16:18:25 -0800 Subject: [PATCH 014/110] Add `NonEmptyBottom` and `PolyNonEmpty` to Non-Empty type system --- .../checker/nonempty/qual/NonEmptyBottom.java | 22 +++++++++++++++++++ .../checker/nonempty/qual/PolyNonEmpty.java | 20 +++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java new file mode 100644 index 00000000000..c2721796780 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +/** + * The bottom type for the Non-Empty type system. Programmers should never have to write this type. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@SubtypeOf({NonEmpty.class}) +public @interface NonEmptyBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java new file mode 100644 index 00000000000..dd538314595 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.PolymorphicQualifier; + +/** + * A polymorphic qualifier for the Non-Empty type system. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(UnknownNonEmpty.class) +public @interface PolyNonEmpty {} From 1e6667643726efdbf4404c0abbf8daf4fe2536d7 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 16 Jan 2024 19:30:17 -0800 Subject: [PATCH 015/110] Re-organize tests --- .../checker/nonempty/qual/NonEmptyBottom.java | 22 -------------- .../list/ImmutableNonEmptyListTest.java | 20 +++++++++++++ .../tests/nonempty/list/ListOperations.java | 29 +++++++++++++++++++ .../EnsuresNonEmptyIfTest.java | 0 .../EnsuresNonEmptyTest.java | 0 .../RequiresNonEmptyTest.java | 0 6 files changed, 49 insertions(+), 22 deletions(-) delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java create mode 100644 checker/tests/nonempty/list/ImmutableNonEmptyListTest.java create mode 100644 checker/tests/nonempty/list/ListOperations.java rename checker/tests/nonempty/{ => postconditions}/EnsuresNonEmptyIfTest.java (100%) rename checker/tests/nonempty/{ => postconditions}/EnsuresNonEmptyTest.java (100%) rename checker/tests/nonempty/{ => preconditions}/RequiresNonEmptyTest.java (100%) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java deleted file mode 100644 index c2721796780..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmptyBottom.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; - -/** - * The bottom type for the Non-Empty type system. Programmers should never have to write this type. - * - * @checker_framework.manual #non-empty-checker Non-Empty Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) -@SubtypeOf({NonEmpty.class}) -public @interface NonEmptyBottom {} diff --git a/checker/tests/nonempty/list/ImmutableNonEmptyListTest.java b/checker/tests/nonempty/list/ImmutableNonEmptyListTest.java new file mode 100644 index 00000000000..133f6f382a5 --- /dev/null +++ b/checker/tests/nonempty/list/ImmutableNonEmptyListTest.java @@ -0,0 +1,20 @@ +import java.util.List; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +// @skip-test until JDK is annotated with Non-Empty type qualifiers + +class ImmutableNonEmptyListTest { + + void testCreateEmptyImmutableList() { + List emptyInts = List.of(); + // Creating a copy of an empty list should also yield an empty list + // :: error: (assignment) + @NonEmpty List copyOfEmptyInts = List.copyOf(emptyInts); + } + + void testCreateNonEmptyImmutableList() { + List nonEmptyInts = List.of(1, 2, 3); + // Creating a copy of an empty list should also yield a non-empty list + @NonEmpty List copyOfNonEmptyInts = List.copyOf(nonEmptyInts); // OK + } +} diff --git a/checker/tests/nonempty/list/ListOperations.java b/checker/tests/nonempty/list/ListOperations.java new file mode 100644 index 00000000000..eaf4b557c6f --- /dev/null +++ b/checker/tests/nonempty/list/ListOperations.java @@ -0,0 +1,29 @@ +import java.util.ArrayList; +import java.util.List; + +// @skip-test until JDK is annotated with Non-Empty type qualifiers + +class ListOperations { + + void testGetOnEmptyList(List strs) { + // :: error: (method.invocation) + strs.get(0); + } + + void testGetOnNonEmptyList(List strs) { + if (strs.isEmpty()) { + // :: error: (method.invocation) + strs.get(0); + } else { + strs.get(0); // OK + } + } + + void testAddToEmptyListAndGet() { + List nums = new ArrayList<>(); + nums.add(1); // nums has type @NonEmpty after this line + nums.get(0); // OK + } + + // TODO: consider other sequences (e.g., calling get(int) after clear()) +} diff --git a/checker/tests/nonempty/EnsuresNonEmptyIfTest.java b/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java similarity index 100% rename from checker/tests/nonempty/EnsuresNonEmptyIfTest.java rename to checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java diff --git a/checker/tests/nonempty/EnsuresNonEmptyTest.java b/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java similarity index 100% rename from checker/tests/nonempty/EnsuresNonEmptyTest.java rename to checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java diff --git a/checker/tests/nonempty/RequiresNonEmptyTest.java b/checker/tests/nonempty/preconditions/RequiresNonEmptyTest.java similarity index 100% rename from checker/tests/nonempty/RequiresNonEmptyTest.java rename to checker/tests/nonempty/preconditions/RequiresNonEmptyTest.java From fc61d3d6b1a91ca8a49675f9876adc3a02b1c68f Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 16 Jan 2024 20:34:38 -0800 Subject: [PATCH 016/110] Adding additional tests for `Set` --- ...Test.java => ImmutableListOperations.java} | 4 +- .../tests/nonempty/list/ListOperations.java | 24 +++++++++- .../nonempty/set/ImmutableSetOperations.java | 18 ++++++++ checker/tests/nonempty/set/SetOperations.java | 45 +++++++++++++++++++ 4 files changed, 87 insertions(+), 4 deletions(-) rename checker/tests/nonempty/list/{ImmutableNonEmptyListTest.java => ImmutableListOperations.java} (84%) create mode 100644 checker/tests/nonempty/set/ImmutableSetOperations.java create mode 100644 checker/tests/nonempty/set/SetOperations.java diff --git a/checker/tests/nonempty/list/ImmutableNonEmptyListTest.java b/checker/tests/nonempty/list/ImmutableListOperations.java similarity index 84% rename from checker/tests/nonempty/list/ImmutableNonEmptyListTest.java rename to checker/tests/nonempty/list/ImmutableListOperations.java index 133f6f382a5..a251cead6cb 100644 --- a/checker/tests/nonempty/list/ImmutableNonEmptyListTest.java +++ b/checker/tests/nonempty/list/ImmutableListOperations.java @@ -3,7 +3,7 @@ // @skip-test until JDK is annotated with Non-Empty type qualifiers -class ImmutableNonEmptyListTest { +class ImmutableListOperations { void testCreateEmptyImmutableList() { List emptyInts = List.of(); @@ -14,7 +14,7 @@ void testCreateEmptyImmutableList() { void testCreateNonEmptyImmutableList() { List nonEmptyInts = List.of(1, 2, 3); - // Creating a copy of an empty list should also yield a non-empty list + // Creating a copy of a non-empty list should also yield a non-empty list @NonEmpty List copyOfNonEmptyInts = List.copyOf(nonEmptyInts); // OK } } diff --git a/checker/tests/nonempty/list/ListOperations.java b/checker/tests/nonempty/list/ListOperations.java index eaf4b557c6f..8bee1e7bb55 100644 --- a/checker/tests/nonempty/list/ListOperations.java +++ b/checker/tests/nonempty/list/ListOperations.java @@ -1,7 +1,6 @@ import java.util.ArrayList; import java.util.List; - -// @skip-test until JDK is annotated with Non-Empty type qualifiers +import org.checkerframework.checker.nonempty.qual.NonEmpty; class ListOperations { @@ -25,5 +24,26 @@ void testAddToEmptyListAndGet() { nums.get(0); // OK } + void testAddAllWithEmptyList() { + List nums = new ArrayList<>(); + nums.addAll(List.of()); + // :: error: (assignment) + @NonEmpty List nums2 = nums; + } + + void testAddAllWithNonEmptyList() { + List nums = new ArrayList<>(); + if (nums.addAll(List.of(1, 2, 3))) { + @NonEmpty List nums2 = nums; // OK + } + } + + void testContains(List nums) { + if (nums.contains(11)) { + @NonEmpty List nums2 = nums; // OK + } + // :: error: (assignment) + @NonEmpty List nums2 = nums; + } // TODO: consider other sequences (e.g., calling get(int) after clear()) } diff --git a/checker/tests/nonempty/set/ImmutableSetOperations.java b/checker/tests/nonempty/set/ImmutableSetOperations.java new file mode 100644 index 00000000000..02c002cae50 --- /dev/null +++ b/checker/tests/nonempty/set/ImmutableSetOperations.java @@ -0,0 +1,18 @@ +import java.util.Set; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +class ImmutableSetOperations { + + void testCreateEmptyImmutableSet() { + Set emptyInts = Set.of(); + // Creating a copy of an empty set should also yield an empty set + // :: error: (assignment) + @NonEmpty Set copyOfEmptyInts = Set.copyOf(emptyInts); + } + + void testCreateNonEmptyImmutableSet() { + Set nonEmptyInts = Set.of(1, 2, 3); + // Creating a copy of a non-empty set should also yield a non-empty set + @NonEmpty Set copyOfNonEmptyInts = Set.copyOf(nonEmptyInts); + } +} diff --git a/checker/tests/nonempty/set/SetOperations.java b/checker/tests/nonempty/set/SetOperations.java new file mode 100644 index 00000000000..8df64de3a9e --- /dev/null +++ b/checker/tests/nonempty/set/SetOperations.java @@ -0,0 +1,45 @@ +import java.util.HashSet; +import java.util.Set; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +class SetOperations { + + void testIsEmpty(Set nums) { + if (nums.isEmpty()) { + // :: error: (assignment) + @NonEmpty Set nums2 = nums; + } else { + @NonEmpty Set nums3 = nums; // OK + } + } + + void testContains(Set nums) { + if (nums.contains(1)) { + @NonEmpty Set nums2 = nums; + } else { + // :: error: (assignment) + @NonEmpty Set nums3 = nums; + } + } + + void testAdd(Set nums) { + // :: error: (assignment) + @NonEmpty Set nums2 = nums; // No guarantee that the set is non-empty here + if (nums.add(1)) { + @NonEmpty Set nums3 = nums; + } + } + + void testAddAllEmptySet() { + Set nums = new HashSet<>(); + // :: error: (assignment) + @NonEmpty Set nums2 = nums; + if (nums.addAll(Set.of())) { + // Adding an empty set will always return false, this is effectively dead code + @NonEmpty Set nums3 = nums; + } else { + // :: error: (assignment) + @NonEmpty Set nums3 = nums; + } + } +} From e91c373addf0b71a587c6f4d203bd16a977d7126 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 16 Jan 2024 20:35:49 -0800 Subject: [PATCH 017/110] Add temporary `@skip-test` --- checker/tests/nonempty/list/ListOperations.java | 2 ++ checker/tests/nonempty/set/ImmutableSetOperations.java | 2 ++ checker/tests/nonempty/set/SetOperations.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/checker/tests/nonempty/list/ListOperations.java b/checker/tests/nonempty/list/ListOperations.java index 8bee1e7bb55..af2948ac270 100644 --- a/checker/tests/nonempty/list/ListOperations.java +++ b/checker/tests/nonempty/list/ListOperations.java @@ -2,6 +2,8 @@ import java.util.List; import org.checkerframework.checker.nonempty.qual.NonEmpty; +// @skip-test until JDK is annotated with Non-Empty type qualifiers + class ListOperations { void testGetOnEmptyList(List strs) { diff --git a/checker/tests/nonempty/set/ImmutableSetOperations.java b/checker/tests/nonempty/set/ImmutableSetOperations.java index 02c002cae50..c7215a00cfa 100644 --- a/checker/tests/nonempty/set/ImmutableSetOperations.java +++ b/checker/tests/nonempty/set/ImmutableSetOperations.java @@ -1,6 +1,8 @@ import java.util.Set; import org.checkerframework.checker.nonempty.qual.NonEmpty; +// @skip-test until JDK is annotated with Non-Empty type qualifiers + class ImmutableSetOperations { void testCreateEmptyImmutableSet() { diff --git a/checker/tests/nonempty/set/SetOperations.java b/checker/tests/nonempty/set/SetOperations.java index 8df64de3a9e..246d88af035 100644 --- a/checker/tests/nonempty/set/SetOperations.java +++ b/checker/tests/nonempty/set/SetOperations.java @@ -2,6 +2,8 @@ import java.util.Set; import org.checkerframework.checker.nonempty.qual.NonEmpty; +// @skip-test until JDK is annotated with Non-Empty type qualifiers + class SetOperations { void testIsEmpty(Set nums) { From 446785468f7135800653bff5276f0904a83ad44b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 21 Jan 2024 22:29:01 -0800 Subject: [PATCH 018/110] Set `this.sideEffectsUnrefineAliases` in NonEmptyAnnotatedTypeFactory --- .../NonEmptyAnnotatedTypeFactory.java | 20 +++++++ checker/tests/nonempty/Issue6407.java | 57 +++++++++++++++++++ docs/manual/creating-a-checker.tex | 21 +++++-- 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java create mode 100644 checker/tests/nonempty/Issue6407.java diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java new file mode 100644 index 00000000000..62c8faa2b0d --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.nonempty; + +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; + +public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** + * Creates a new {@link NonEmptyAnnotatedTypeFactory} that operates on a particular AST. + * + * @param checker the checker to use + */ + public NonEmptyAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + + this.sideEffectsUnrefineAliases = true; + + this.postInit(); + } +} diff --git a/checker/tests/nonempty/Issue6407.java b/checker/tests/nonempty/Issue6407.java new file mode 100644 index 00000000000..0455121e4ed --- /dev/null +++ b/checker/tests/nonempty/Issue6407.java @@ -0,0 +1,57 @@ +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; +import org.checkerframework.checker.nonempty.qual.NonEmpty; +import org.checkerframework.checker.nonempty.qual.UnknownNonEmpty; + +class Issue6407 { + // void usesJdk() { + // // items initially has the type @UnknownNonEmpty + // List items = new LinkedList<>(); + // items.add("hello"); + // @NonEmpty List bar = items; // OK + // items.remove("hello"); + // @NonEmpty List baz = items; // I expect an error here + // } + + static class MyList { + @SuppressWarnings("contracts.postcondition") // nonfunctional class + @EnsuresNonEmpty("this") + boolean add(E e) { + return true; + } + + boolean remove(@NonEmpty MyList this, E e) { + return true; + } + } + + boolean removeIt(@NonEmpty MyList myl, T e) { + return true; + } + + void noJdk() { + // items initially has the type @UnknownNonEmpty + @UnknownNonEmpty MyList items = new MyList<>(); + items.add("hello"); + @NonEmpty MyList bar = items; // OK + items.remove("hello"); + // :: error: (assignment) + @NonEmpty MyList baz = items; + } + + void noJdk2() { + // items initially has the type @UnknownNonEmpty + @UnknownNonEmpty MyList items = new MyList<>(); + items.add("hello"); + @NonEmpty MyList bar = items; // OK + removeIt(items, "hello"); + // :: error: (assignment) + @NonEmpty MyList baz = items; + } + + // void initialRemoval() { + // // items initially has the type @UnknownNonEmpty + // @UnknownNonEmpty MyList items = new MyList<>(); + // // :: error: (method.invocation) + // items.remove("hello"); + // } +} diff --git a/docs/manual/creating-a-checker.tex b/docs/manual/creating-a-checker.tex index 65a31e1f05f..29989ce74da 100644 --- a/docs/manual/creating-a-checker.tex +++ b/docs/manual/creating-a-checker.tex @@ -1840,10 +1840,23 @@ \end{enumerate} -\subsectionAndLabel{Disabling flow-sensitive inference}{creating-dataflow-disable} - -In the uncommon case that you wish to disable the Checker Framework's -built-in flow inference in your checker (this is different than choosing +\label{creating-dataflow-disable} % temporary; remove in January 2025 +\subsectionAndLabel{Further customizing flow-sensitive inference}{creating-dataflow-customization} + +By default, the Checker Framework assumes that modifications to an object's +fields do not change its type qualifiers. This is true for all +provenance-based type systems (see +Section~\ref{type-refinement-runtime-tests}), for all value-based type +systems over immutable values, and for some other type systems. To +indicate that \textbf{side effects can change a value's type qualifiers}, +write \ in the type factory +constructor, before the call to \. Note that this +setting may lead to large numbers of false positive warnings. One way to +reduce the number of false positive warnings would be to improve the side +effect annotations to be more precise. + +In the uncommon case that you wish to \textbf{disable the Checker Framework's +built-in flow inference} in your checker (this is different than choosing not to extend it as described in Section~\ref{creating-dataflow}), put the following two lines at the beginning of the constructor for your subtype of \refclass{common/basetype}{BaseAnnotatedTypeFactory}: From db6dab95bc7d34f84923c9bbb2fe7e6f56abc2d5 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 22 Jan 2024 14:34:20 -0800 Subject: [PATCH 019/110] Add test for `Set.remove(E)` --- checker/tests/nonempty/Issue6407.java | 32 +++++++++++-------- checker/tests/nonempty/set/SetOperations.java | 10 ++++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/checker/tests/nonempty/Issue6407.java b/checker/tests/nonempty/Issue6407.java index 0455121e4ed..c52697ae253 100644 --- a/checker/tests/nonempty/Issue6407.java +++ b/checker/tests/nonempty/Issue6407.java @@ -1,16 +1,20 @@ +import java.util.LinkedList; +import java.util.List; import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.checker.nonempty.qual.UnknownNonEmpty; class Issue6407 { - // void usesJdk() { - // // items initially has the type @UnknownNonEmpty - // List items = new LinkedList<>(); - // items.add("hello"); - // @NonEmpty List bar = items; // OK - // items.remove("hello"); - // @NonEmpty List baz = items; // I expect an error here - // } + + void usesJdk() { + // items initially has the type @UnknownNonEmpty + List items = new LinkedList<>(); + items.add("hello"); + @NonEmpty List bar = items; // OK + items.remove("hello"); + // :: error: (assignment) + @NonEmpty List baz = items; // I expect an error here + } static class MyList { @SuppressWarnings("contracts.postcondition") // nonfunctional class @@ -48,10 +52,10 @@ void noJdk2() { @NonEmpty MyList baz = items; } - // void initialRemoval() { - // // items initially has the type @UnknownNonEmpty - // @UnknownNonEmpty MyList items = new MyList<>(); - // // :: error: (method.invocation) - // items.remove("hello"); - // } + void initialRemoval() { + // items initially has the type @UnknownNonEmpty + MyList items = new MyList<>(); + // :: error: (method.invocation) + items.remove("hello"); + } } diff --git a/checker/tests/nonempty/set/SetOperations.java b/checker/tests/nonempty/set/SetOperations.java index 246d88af035..0e84cdbddab 100644 --- a/checker/tests/nonempty/set/SetOperations.java +++ b/checker/tests/nonempty/set/SetOperations.java @@ -44,4 +44,14 @@ void testAddAllEmptySet() { @NonEmpty Set nums3 = nums; } } + + void testRemove() { + Set nums = new HashSet<>(); + nums.add(1); + @NonEmpty Set nums2 = nums; + nums.remove(1); + + // :: error: (assignment) + @NonEmpty Set nums3 = nums; + } } From 89b4beed2151051e4efc6665ccb700326e4b1324 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 22 Jan 2024 20:52:01 -0800 Subject: [PATCH 020/110] Add tests for immutable map operations --- .../nonempty/map/ImmutableMapOperations.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 checker/tests/nonempty/map/ImmutableMapOperations.java diff --git a/checker/tests/nonempty/map/ImmutableMapOperations.java b/checker/tests/nonempty/map/ImmutableMapOperations.java new file mode 100644 index 00000000000..f097a4589f1 --- /dev/null +++ b/checker/tests/nonempty/map/ImmutableMapOperations.java @@ -0,0 +1,29 @@ +import java.util.Map; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +// @skip-test until JDK is annotated with Non-Empty type qualifiers + +class ImmutableMapOperations { + + void emptyImmutableMap() { + Map emptyMap = Map.of(); + // :: error: (assignment) + @NonEmpty Map nonEmptyMap = emptyMap; + } + + void nonEmptyImmutableMap() { + Map nonEmptyMap = Map.of("Hello", 1); + @NonEmpty Map m1 = nonEmptyMap; + } + + void immutableCopyEmptyMap() { + Map emptyMap = Map.of(); + // :: error: (assignment) + @NonEmpty Map nonEmptyMap = Map.copyOf(emptyMap); + } + + void immutableCopyNonEmptyMap() { + Map nonEmptyMap = Map.of("Hello", 1, "World", 2); + @NonEmpty Map m2 = Map.copyOf(nonEmptyMap); + } +} From 19a20429aa5a60912ac3a105a6aaf71cb6fe1fb7 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 23 Jan 2024 14:59:42 -0800 Subject: [PATCH 021/110] Enable JDK tests --- .../list/ImmutableListOperations.java | 2 - .../tests/nonempty/list/ListOperations.java | 2 - checker/tests/nonempty/map/MapOperations.java | 43 +++++++++++++++++++ .../postconditions/EnsuresNonEmptyIfTest.java | 2 - .../postconditions/EnsuresNonEmptyTest.java | 2 - .../nonempty/set/ImmutableSetOperations.java | 2 - checker/tests/nonempty/set/SetOperations.java | 2 - 7 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 checker/tests/nonempty/map/MapOperations.java diff --git a/checker/tests/nonempty/list/ImmutableListOperations.java b/checker/tests/nonempty/list/ImmutableListOperations.java index a251cead6cb..1fdb87ad008 100644 --- a/checker/tests/nonempty/list/ImmutableListOperations.java +++ b/checker/tests/nonempty/list/ImmutableListOperations.java @@ -1,8 +1,6 @@ import java.util.List; import org.checkerframework.checker.nonempty.qual.NonEmpty; -// @skip-test until JDK is annotated with Non-Empty type qualifiers - class ImmutableListOperations { void testCreateEmptyImmutableList() { diff --git a/checker/tests/nonempty/list/ListOperations.java b/checker/tests/nonempty/list/ListOperations.java index af2948ac270..8bee1e7bb55 100644 --- a/checker/tests/nonempty/list/ListOperations.java +++ b/checker/tests/nonempty/list/ListOperations.java @@ -2,8 +2,6 @@ import java.util.List; import org.checkerframework.checker.nonempty.qual.NonEmpty; -// @skip-test until JDK is annotated with Non-Empty type qualifiers - class ListOperations { void testGetOnEmptyList(List strs) { diff --git a/checker/tests/nonempty/map/MapOperations.java b/checker/tests/nonempty/map/MapOperations.java new file mode 100644 index 00000000000..820854ffd8b --- /dev/null +++ b/checker/tests/nonempty/map/MapOperations.java @@ -0,0 +1,43 @@ +import java.util.Map; + +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +class MapOperations { + + void addToMapParam(Map m) { + // :: error: (method.invocation) + m.get("hello"); + + m.put("hello", 1); + + @NonEmpty Map m2 = m; // OK + m.get("hello"); // OK + } + + void clearMap(Map m) { + m.put("hello", 1); + m.get("hello"); // OK + + m.clear(); + // :: error: (method.invocation) + m.get("hello"); + } + + void containsKeyRefinement(Map m, String key) { + if (m.containsKey(key)) { + @NonEmpty Map m2 = m; // OK + } else { + // :: error: (assignment) + @NonEmpty Map m2 = m; // OK + } + } + + void containsValueRefinement(Map m, Integer value) { + if (m.containsValue(value)) { + @NonEmpty Map m2 = m; + } else { + // :: error: (assignment) + @NonEmpty Map m2 = m; + } + } +} diff --git a/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java b/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java index 03598ff0d0c..f1669b8b90e 100644 --- a/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java +++ b/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java @@ -2,8 +2,6 @@ import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; import org.checkerframework.checker.nonempty.qual.NonEmpty; -// @skip-test until JDK is annotated with Non-Empty type qualifiers - class EnsuresNonEmptyIfTest { @EnsuresNonEmptyIf(result = true, expression = "#1") diff --git a/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java b/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java index 060e6472eda..1c60cae2576 100644 --- a/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java +++ b/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java @@ -2,8 +2,6 @@ import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; import org.checkerframework.checker.nonempty.qual.NonEmpty; -// @skip-test until JDK is annotated with Non-Empty type qualifiers - class EnsuresNonEmptyTest { @EnsuresNonEmpty("#1") diff --git a/checker/tests/nonempty/set/ImmutableSetOperations.java b/checker/tests/nonempty/set/ImmutableSetOperations.java index c7215a00cfa..02c002cae50 100644 --- a/checker/tests/nonempty/set/ImmutableSetOperations.java +++ b/checker/tests/nonempty/set/ImmutableSetOperations.java @@ -1,8 +1,6 @@ import java.util.Set; import org.checkerframework.checker.nonempty.qual.NonEmpty; -// @skip-test until JDK is annotated with Non-Empty type qualifiers - class ImmutableSetOperations { void testCreateEmptyImmutableSet() { diff --git a/checker/tests/nonempty/set/SetOperations.java b/checker/tests/nonempty/set/SetOperations.java index 0e84cdbddab..ca8af22822c 100644 --- a/checker/tests/nonempty/set/SetOperations.java +++ b/checker/tests/nonempty/set/SetOperations.java @@ -2,8 +2,6 @@ import java.util.Set; import org.checkerframework.checker.nonempty.qual.NonEmpty; -// @skip-test until JDK is annotated with Non-Empty type qualifiers - class SetOperations { void testIsEmpty(Set nums) { From a0da89d3a3b58eff2ebf15e72a5c7d7734828a14 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 23 Jan 2024 16:08:09 -0800 Subject: [PATCH 022/110] Apply Spotless changes --- checker/tests/nonempty/map/MapOperations.java | 1 - 1 file changed, 1 deletion(-) diff --git a/checker/tests/nonempty/map/MapOperations.java b/checker/tests/nonempty/map/MapOperations.java index 820854ffd8b..1e9b1de6682 100644 --- a/checker/tests/nonempty/map/MapOperations.java +++ b/checker/tests/nonempty/map/MapOperations.java @@ -1,5 +1,4 @@ import java.util.Map; - import org.checkerframework.checker.nonempty.qual.NonEmpty; class MapOperations { From a53b9ba0632ef8d45b06f8a19a3fb7c3bd59b700 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 26 Jan 2024 16:24:25 -0800 Subject: [PATCH 023/110] Adding tests for iterator operations --- .../nonempty/iterator/IteratorOperations.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 checker/tests/nonempty/iterator/IteratorOperations.java diff --git a/checker/tests/nonempty/iterator/IteratorOperations.java b/checker/tests/nonempty/iterator/IteratorOperations.java new file mode 100644 index 00000000000..7534893a6bc --- /dev/null +++ b/checker/tests/nonempty/iterator/IteratorOperations.java @@ -0,0 +1,19 @@ +import java.util.Iterator; +import java.util.List; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +class IteratorOperations { + + void testPolyNonEmptyIterator(List nums) { + // :: error: (method.invocation) + nums.iterator().next(); + + if (!nums.isEmpty()) { + @NonEmpty Iterator nonEmptyIterator = nums.iterator(); + nonEmptyIterator.next(); + } else { + // :: error: (assignment) + @NonEmpty Iterator unknownEmptyIterator = nums.iterator(); + } + } +} From 28859fba2b1079a10a3555fabddd5f07716f8216 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 29 Jan 2024 12:54:02 -0800 Subject: [PATCH 024/110] Start implementing transfer fn. for Non-Empty Checker --- .../NonEmptyAnnotatedTypeFactory.java | 25 +++++++- .../checker/nonempty/NonEmptyTransfer.java | 59 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 62c8faa2b0d..029c731aac2 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -1,10 +1,27 @@ package org.checkerframework.checker.nonempty; +import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.nonempty.qual.NonEmpty; +import org.checkerframework.checker.nonempty.qual.UnknownNonEmpty; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.AnnotationBuilder; public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + /** The @{@link UnknownNonEmpty} annotation * */ + @SuppressWarnings("UnusedVariable") + private final AnnotationMirror UNKNOWN_NON_EMPTY = + AnnotationBuilder.fromClass(elements, UnknownNonEmpty.class); + + /** The @{@link NonEmpty} annotation * */ + @SuppressWarnings("UnusedVariable") + private final AnnotationMirror NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); + /** * Creates a new {@link NonEmptyAnnotatedTypeFactory} that operates on a particular AST. * @@ -12,9 +29,13 @@ public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { */ public NonEmptyAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - this.sideEffectsUnrefineAliases = true; - this.postInit(); } + + @Override + public CFTransfer createFlowTransferFunction( + CFAbstractAnalysis analysis) { + return new NonEmptyTransfer(analysis); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java new file mode 100644 index 00000000000..0a0c854a84d --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -0,0 +1,59 @@ +package org.checkerframework.checker.nonempty; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.GreaterThanNode; +import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.util.NodeUtils; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.TreeUtils; + +public class NonEmptyTransfer extends CFTransfer { + + private final ExecutableElement collectionSize; + private final ProcessingEnvironment env; + + public NonEmptyTransfer(CFAbstractAnalysis analysis) { + super(analysis); + + this.env = analysis.getTypeFactory().getProcessingEnv(); + this.collectionSize = TreeUtils.getMethod("java.util.Collection", "size", 0, this.env); + } + + @Override + public TransferResult visitGreaterThan( + GreaterThanNode n, TransferInput in) { + TransferResult result = super.visitGreaterThan(n, in); + handleContainerSizeComparison(n.getLeftOperand(), n.getRightOperand(), result); + return result; // stub + } + + private TransferResult handleContainerSizeComparison( + Node possibleCollectionSize, Node possibleConstant, TransferResult in) { + if (!(possibleCollectionSize instanceof MethodInvocationNode)) { + return in; + } + if (!(possibleConstant instanceof IntegerLiteralNode)) { + return in; + } + + if (isSizeAccess(possibleCollectionSize)) { + IntegerLiteralNode comparedValue = (IntegerLiteralNode) possibleConstant; + if (comparedValue.getValue() > 0) { + // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; + } + } + return in; + } + + private boolean isSizeAccess(Node possibleSizeAccess) { + return NodeUtils.isMethodInvocation(possibleSizeAccess, collectionSize, env); + } +} From 266af5f34fc824970d5f6aa3e903734f975459d6 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 29 Jan 2024 14:44:36 -0800 Subject: [PATCH 025/110] Implement `NonEmptyTransfer.visitNotEqual` --- .../NonEmptyAnnotatedTypeFactory.java | 2 +- .../checker/nonempty/NonEmptyTransfer.java | 71 +++++++++++++++++-- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 029c731aac2..0a6f9fe2ff3 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -20,7 +20,7 @@ public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The @{@link NonEmpty} annotation * */ @SuppressWarnings("UnusedVariable") - private final AnnotationMirror NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); + public final AnnotationMirror NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); /** * Creates a new {@link NonEmptyAnnotatedTypeFactory} that operates on a particular AST. diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 0a0c854a84d..1835668aba2 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -5,9 +5,14 @@ import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.GreaterThanNode; +import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode; import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; +import org.checkerframework.dataflow.cfg.node.LessThanNode; +import org.checkerframework.dataflow.cfg.node.MethodAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.NotEqualNode; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; @@ -19,14 +24,25 @@ public class NonEmptyTransfer extends CFTransfer { private final ExecutableElement collectionSize; private final ProcessingEnvironment env; + private final NonEmptyAnnotatedTypeFactory aTypeFactory; public NonEmptyTransfer(CFAbstractAnalysis analysis) { super(analysis); this.env = analysis.getTypeFactory().getProcessingEnv(); + this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); this.collectionSize = TreeUtils.getMethod("java.util.Collection", "size", 0, this.env); } + @Override + public TransferResult visitNotEqual( + NotEqualNode n, TransferInput in) { + TransferResult result = super.visitNotEqual(n, in); + refineNotEqual(n.getLeftOperand(), n.getRightOperand(), result); + refineNotEqual(n.getRightOperand(), n.getLeftOperand(), result); + return result; + } + @Override public TransferResult visitGreaterThan( GreaterThanNode n, TransferInput in) { @@ -35,25 +51,68 @@ public TransferResult visitGreaterThan( return result; // stub } + @Override + public TransferResult visitGreaterThanOrEqual( + GreaterThanOrEqualNode n, TransferInput in) { + // TODO: implement me + TransferResult result = super.visitGreaterThan(n, in); + return result; // stub + } + + private TransferResult refineNotEqual( + Node lhs, Node rhs, TransferResult in) { + if (!isSizeAccess(lhs)) { + return in; + } + if (!(rhs instanceof IntegerLiteralNode)) { + return in; + } + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + if (integerLiteralNode.getValue() == 0) { + // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; + JavaExpression receiver = getReceiver(lhs); + in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); + } + return in; + } + + @Override + public TransferResult visitLessThan( + LessThanNode n, TransferInput cfValueCFStoreTransferInput) { + return super.visitLessThan(n, cfValueCFStoreTransferInput); + } + private TransferResult handleContainerSizeComparison( Node possibleCollectionSize, Node possibleConstant, TransferResult in) { - if (!(possibleCollectionSize instanceof MethodInvocationNode)) { + if (!isSizeAccess(possibleCollectionSize)) { return in; } if (!(possibleConstant instanceof IntegerLiteralNode)) { return in; } - if (isSizeAccess(possibleCollectionSize)) { - IntegerLiteralNode comparedValue = (IntegerLiteralNode) possibleConstant; - if (comparedValue.getValue() > 0) { - // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; - } + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleConstant; + if (integerLiteralNode.getValue() >= 0) { + // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; + JavaExpression receiver = getReceiver(possibleCollectionSize); + in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); } return in; } + /** + * Given a node that is a possible call to Collection.size(), return true if and only if this is + * the case. + * + * @param possibleSizeAccess a node that may be a method call to Collection.size() + * @return true iff the node is a method call to Collection.size() + */ private boolean isSizeAccess(Node possibleSizeAccess) { return NodeUtils.isMethodInvocation(possibleSizeAccess, collectionSize, env); } + + private JavaExpression getReceiver(Node sizeAccessNode) { + MethodAccessNode methodAccessNode = ((MethodInvocationNode) sizeAccessNode).getTarget(); + return JavaExpression.fromNode(methodAccessNode.getReceiver()); + } } From 16d8aa1febe3201d395a6d3cdf34e098a9749f2d Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 29 Jan 2024 14:45:33 -0800 Subject: [PATCH 026/110] Pass proper arg --- .../org/checkerframework/checker/nonempty/NonEmptyTransfer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 1835668aba2..ce7deed7cd2 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -55,7 +55,7 @@ public TransferResult visitGreaterThan( public TransferResult visitGreaterThanOrEqual( GreaterThanOrEqualNode n, TransferInput in) { // TODO: implement me - TransferResult result = super.visitGreaterThan(n, in); + TransferResult result = super.visitGreaterThanOrEqual(n, in); return result; // stub } From aec43174890c8321df4968796e87fb8cc5872aed Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 29 Jan 2024 21:56:01 -0800 Subject: [PATCH 027/110] Complete initial impl. of transfer function for Non-Empty Checker --- .../checker/nonempty/NonEmptyTransfer.java | 59 +++++++++---- checker/tests/nonempty/list/Comparisons.java | 88 +++++++++++++++++++ 2 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 checker/tests/nonempty/list/Comparisons.java diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index ce7deed7cd2..576f29b3585 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -8,6 +8,7 @@ import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode; import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; import org.checkerframework.dataflow.cfg.node.LessThanNode; +import org.checkerframework.dataflow.cfg.node.LessThanOrEqualNode; import org.checkerframework.dataflow.cfg.node.MethodAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -43,20 +44,36 @@ public TransferResult visitNotEqual( return result; } + @Override + public TransferResult visitLessThan( + LessThanNode n, TransferInput in) { + TransferResult result = super.visitLessThan(n, in); + refineGT(n.getRightOperand(), n.getLeftOperand(), result); + return result; + } + + @Override + public TransferResult visitLessThanOrEqual( + LessThanOrEqualNode n, TransferInput in) { + TransferResult result = super.visitLessThanOrEqual(n, in); + refineGTE(n.getRightOperand(), n.getLeftOperand(), result); + return result; + } + @Override public TransferResult visitGreaterThan( GreaterThanNode n, TransferInput in) { TransferResult result = super.visitGreaterThan(n, in); - handleContainerSizeComparison(n.getLeftOperand(), n.getRightOperand(), result); - return result; // stub + refineGT(n.getLeftOperand(), n.getRightOperand(), result); + return result; } @Override public TransferResult visitGreaterThanOrEqual( GreaterThanOrEqualNode n, TransferInput in) { - // TODO: implement me TransferResult result = super.visitGreaterThanOrEqual(n, in); - return result; // stub + refineGTE(n.getLeftOperand(), n.getRightOperand(), result); + return result; } private TransferResult refineNotEqual( @@ -76,25 +93,37 @@ private TransferResult refineNotEqual( return in; } - @Override - public TransferResult visitLessThan( - LessThanNode n, TransferInput cfValueCFStoreTransferInput) { - return super.visitLessThan(n, cfValueCFStoreTransferInput); + private TransferResult refineGT( + Node lhs, Node rhs, TransferResult in) { + if (!isSizeAccess(lhs)) { + return in; + } + if (!(rhs instanceof IntegerLiteralNode)) { + return in; + } + + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + if (integerLiteralNode.getValue() >= 0) { + // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; + JavaExpression receiver = getReceiver(lhs); + in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); + } + return in; } - private TransferResult handleContainerSizeComparison( - Node possibleCollectionSize, Node possibleConstant, TransferResult in) { - if (!isSizeAccess(possibleCollectionSize)) { + private TransferResult refineGTE( + Node lhs, Node rhs, TransferResult in) { + if (!isSizeAccess(lhs)) { return in; } - if (!(possibleConstant instanceof IntegerLiteralNode)) { + if (!(rhs instanceof IntegerLiteralNode)) { return in; } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleConstant; - if (integerLiteralNode.getValue() >= 0) { + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + if (integerLiteralNode.getValue() > 0) { // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; - JavaExpression receiver = getReceiver(possibleCollectionSize); + JavaExpression receiver = getReceiver(lhs); in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); } return in; diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java new file mode 100644 index 00000000000..4f463efe5c2 --- /dev/null +++ b/checker/tests/nonempty/list/Comparisons.java @@ -0,0 +1,88 @@ +import java.util.List; + +class Comparisons { + + /**** Tests for GT ****/ + void t1(List strs) { + if (strs.size() > 0) { + strs.iterator().next(); + } else if (0 > strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } else if (100 > strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + + void t2(List strs) { + if (strs.size() > -1) { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + + /**** Tests for GTE ****/ + void t3(List strs) { + if (strs.size() >= 0) { + // :: error: (method.invocation) + strs.iterator().next(); + } else if (strs.size() >= 1) { + strs.iterator().next(); + } + } + + void t4(List strs) { + if (0 >= strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + + /**** Tests for LT ****/ + void t5(List strs) { + if (strs.size() < 10) { + // :: error: (method.invocation) + strs.iterator().next(); + } else if (strs.size() < 1) { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + + void t6(List strs) { + if (0 < strs.size()) { + strs.iterator().next(); + } + if (10 < strs.size()) { + strs.iterator().next(); + } + if (-1 < strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + + /**** Tests for LTE ****/ + void t7(List strs) { + if (strs.size() <= 2) { + // :: error: (method.invocation) + strs.iterator().next(); + } + if (strs.size() <= 0) { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + + void t8(List strs) { + if (0 <= strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } else if (1 <= strs.size()) { + strs.iterator().next(); + } else if (10 <= strs.size()) { + strs.iterator().next(); + } + } +} From 3fefdb4b5c8fdafc347e766a046a33c3ba491898 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 29 Jan 2024 22:30:37 -0800 Subject: [PATCH 028/110] Adding failing tests for LTE --- checker/tests/nonempty/list/Comparisons.java | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java index 4f463efe5c2..ad326f2bdda 100644 --- a/checker/tests/nonempty/list/Comparisons.java +++ b/checker/tests/nonempty/list/Comparisons.java @@ -2,6 +2,20 @@ class Comparisons { + /**** Tests for NE ****/ + void t0(List strs) { + if (strs.size() != 0) { + strs.iterator().next(); + } + if (0 != strs.size()) { + strs.iterator().next(); + } + if (1 != strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + /**** Tests for GT ****/ void t1(List strs) { if (strs.size() > 0) { @@ -13,6 +27,12 @@ void t1(List strs) { // :: error: (method.invocation) strs.iterator().next(); } + if (strs.size() > 0) { + strs.iterator().next(); + } else { + // :: error: (method.invocation) + strs.iterator().next(); + } } void t2(List strs) { @@ -72,6 +92,8 @@ void t7(List strs) { if (strs.size() <= 0) { // :: error: (method.invocation) strs.iterator().next(); + } else { + strs.iterator().next(); // OK, since strs must be non-empty } } @@ -84,5 +106,11 @@ void t8(List strs) { } else if (10 <= strs.size()) { strs.iterator().next(); } + if (0 <= strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } else { + strs.iterator().next(); // OK, since strs must be non-empty + } } } From e44ecb8541f7f3862bc70c5d2463b5d6f25c9606 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 30 Jan 2024 12:50:37 -0800 Subject: [PATCH 029/110] Comparison refinement working, need to add documentation --- .../checker/nonempty/NonEmptyTransfer.java | 61 ++++++++----------- checker/tests/nonempty/list/Comparisons.java | 41 +++++++++---- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 576f29b3585..73c80cbb7e2 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -45,34 +45,36 @@ public TransferResult visitNotEqual( } @Override - public TransferResult visitLessThan( - LessThanNode n, TransferInput in) { - TransferResult result = super.visitLessThan(n, in); - refineGT(n.getRightOperand(), n.getLeftOperand(), result); + public TransferResult visitGreaterThan( + GreaterThanNode n, TransferInput in) { + TransferResult result = super.visitGreaterThan(n, in); + refineGT(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); return result; } @Override - public TransferResult visitLessThanOrEqual( - LessThanOrEqualNode n, TransferInput in) { - TransferResult result = super.visitLessThanOrEqual(n, in); - refineGTE(n.getRightOperand(), n.getLeftOperand(), result); + public TransferResult visitGreaterThanOrEqual( + GreaterThanOrEqualNode n, TransferInput in) { + TransferResult result = super.visitGreaterThanOrEqual(n, in); + refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); return result; } @Override - public TransferResult visitGreaterThan( - GreaterThanNode n, TransferInput in) { - TransferResult result = super.visitGreaterThan(n, in); - refineGT(n.getLeftOperand(), n.getRightOperand(), result); + public TransferResult visitLessThan( + LessThanNode n, TransferInput in) { + TransferResult result = super.visitLessThan(n, in); + refineGT(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); + refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); return result; } @Override - public TransferResult visitGreaterThanOrEqual( - GreaterThanOrEqualNode n, TransferInput in) { - TransferResult result = super.visitGreaterThanOrEqual(n, in); - refineGTE(n.getLeftOperand(), n.getRightOperand(), result); + public TransferResult visitLessThanOrEqual( + LessThanOrEqualNode n, TransferInput in) { + TransferResult result = super.visitLessThanOrEqual(n, in); + refineGTE(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); + refineGT(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); return result; } @@ -93,40 +95,29 @@ private TransferResult refineNotEqual( return in; } - private TransferResult refineGT( - Node lhs, Node rhs, TransferResult in) { - if (!isSizeAccess(lhs)) { - return in; + private void refineGT(Node lhs, Node rhs, CFStore store) { + if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { + return; } - if (!(rhs instanceof IntegerLiteralNode)) { - return in; - } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; if (integerLiteralNode.getValue() >= 0) { // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; JavaExpression receiver = getReceiver(lhs); - in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); + store.insertValue(receiver, aTypeFactory.NON_EMPTY); } - return in; } - private TransferResult refineGTE( - Node lhs, Node rhs, TransferResult in) { - if (!isSizeAccess(lhs)) { - return in; - } - if (!(rhs instanceof IntegerLiteralNode)) { - return in; + private void refineGTE(Node lhs, Node rhs, CFStore store) { + if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { + return; } IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; if (integerLiteralNode.getValue() > 0) { // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; JavaExpression receiver = getReceiver(lhs); - in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); + store.insertValue(receiver, aTypeFactory.NON_EMPTY); } - return in; } /** diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java index ad326f2bdda..b1edeac0031 100644 --- a/checker/tests/nonempty/list/Comparisons.java +++ b/checker/tests/nonempty/list/Comparisons.java @@ -18,7 +18,7 @@ void t0(List strs) { /**** Tests for GT ****/ void t1(List strs) { - if (strs.size() > 0) { + if (strs.size() > 10) { strs.iterator().next(); } else if (0 > strs.size()) { // :: error: (method.invocation) @@ -33,6 +33,14 @@ void t1(List strs) { // :: error: (method.invocation) strs.iterator().next(); } + + if (0 > strs.size()) { + // :: error: (method.invocation) + strs.iterator().next(); + } else { + // :: error: (method.invocation) + strs.iterator().next(); + } } void t2(List strs) { @@ -64,22 +72,29 @@ void t5(List strs) { if (strs.size() < 10) { // :: error: (method.invocation) strs.iterator().next(); - } else if (strs.size() < 1) { + } + if (strs.size() < 1) { // :: error: (method.invocation) strs.iterator().next(); + } else { + strs.iterator().next(); // OK } } void t6(List strs) { if (0 < strs.size()) { - strs.iterator().next(); - } - if (10 < strs.size()) { - strs.iterator().next(); + strs.iterator().next(); // Equiv. to strs.size() > 0 + } else { + // :: error: (method.invocation) + strs.iterator().next(); // Equiv. to strs.size() <= 0 } - if (-1 < strs.size()) { + + if (strs.size() < 10) { + // Doesn't tell us a useful fact // :: error: (method.invocation) strs.iterator().next(); + } else { + strs.iterator().next(); } } @@ -98,19 +113,19 @@ void t7(List strs) { } void t8(List strs) { - if (0 <= strs.size()) { - // :: error: (method.invocation) + if (1 <= strs.size()) { strs.iterator().next(); - } else if (1 <= strs.size()) { - strs.iterator().next(); - } else if (10 <= strs.size()) { + } else { + // :: error: (method.invocation) strs.iterator().next(); } + if (0 <= strs.size()) { // :: error: (method.invocation) strs.iterator().next(); } else { - strs.iterator().next(); // OK, since strs must be non-empty + // :: error: (method.invocation) + strs.iterator().next(); } } } From fefadec9d2a7d3718022c91c4652c32ec113d4cc Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 30 Jan 2024 14:53:04 -0800 Subject: [PATCH 030/110] Document `NonEmptyTransfer` class --- .../NonEmptyAnnotatedTypeFactory.java | 7 -- .../checker/nonempty/NonEmptyChecker.java | 6 + .../checker/nonempty/NonEmptyTransfer.java | 113 +++++++++++++----- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 0a6f9fe2ff3..1e789954f0a 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -2,7 +2,6 @@ import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.checker.nonempty.qual.UnknownNonEmpty; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; @@ -13,13 +12,7 @@ public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - /** The @{@link UnknownNonEmpty} annotation * */ - @SuppressWarnings("UnusedVariable") - private final AnnotationMirror UNKNOWN_NON_EMPTY = - AnnotationBuilder.fromClass(elements, UnknownNonEmpty.class); - /** The @{@link NonEmpty} annotation * */ - @SuppressWarnings("UnusedVariable") public final AnnotationMirror NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); /** diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java index e0570476961..ca4054c68e3 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java @@ -2,4 +2,10 @@ import org.checkerframework.common.basetype.BaseTypeChecker; +/** + * A type-checker that prevents {@link java.util.NoSuchElementException} in the use of container + * classes. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ public class NonEmptyChecker extends BaseTypeChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 73c80cbb7e2..d6f1e6480cf 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -21,18 +21,27 @@ import org.checkerframework.framework.flow.CFValue; import org.checkerframework.javacutil.TreeUtils; +/** + * This class provides methods used by the Non-Empty Checker as transfer functions for type rules + * that cannot be expressed via simple pre- or post-conditional annotations. + */ public class NonEmptyTransfer extends CFTransfer { - private final ExecutableElement collectionSize; + /** A {@link ProcessingEnvironment} instance. */ private final ProcessingEnvironment env; + + /** The {@code size()} method of the {@link java.util.Collection} interface. */ + private final ExecutableElement collectionSize; + + /** A {@link NonEmptyAnnotatedTypeFactory} instance. */ private final NonEmptyAnnotatedTypeFactory aTypeFactory; public NonEmptyTransfer(CFAbstractAnalysis analysis) { super(analysis); this.env = analysis.getTypeFactory().getProcessingEnv(); - this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); this.collectionSize = TreeUtils.getMethod("java.util.Collection", "size", 0, this.env); + this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); } @Override @@ -44,27 +53,14 @@ public TransferResult visitNotEqual( return result; } - @Override - public TransferResult visitGreaterThan( - GreaterThanNode n, TransferInput in) { - TransferResult result = super.visitGreaterThan(n, in); - refineGT(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); - return result; - } - - @Override - public TransferResult visitGreaterThanOrEqual( - GreaterThanOrEqualNode n, TransferInput in) { - TransferResult result = super.visitGreaterThanOrEqual(n, in); - refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); - return result; - } - @Override public TransferResult visitLessThan( LessThanNode n, TransferInput in) { TransferResult result = super.visitLessThan(n, in); + + // A < B is equivalent to B > A refineGT(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); + // This handles the case where n < container.size() refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); return result; } @@ -73,18 +69,45 @@ public TransferResult visitLessThan( public TransferResult visitLessThanOrEqual( LessThanOrEqualNode n, TransferInput in) { TransferResult result = super.visitLessThanOrEqual(n, in); - refineGTE(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); + + // A <= B is equivalent to B > A refineGT(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); + // This handles the case where n <= container.size() + refineGTE(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); return result; } - private TransferResult refineNotEqual( - Node lhs, Node rhs, TransferResult in) { - if (!isSizeAccess(lhs)) { - return in; - } - if (!(rhs instanceof IntegerLiteralNode)) { - return in; + @Override + public TransferResult visitGreaterThan( + GreaterThanNode n, TransferInput in) { + TransferResult result = super.visitGreaterThan(n, in); + refineGT(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); + return result; + } + + @Override + public TransferResult visitGreaterThanOrEqual( + GreaterThanOrEqualNode n, TransferInput in) { + TransferResult result = super.visitGreaterThanOrEqual(n, in); + refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); + return result; + } + + /** + * Updates the transfer result's store with information from the Non-Empty type system for + * expressions of the form {@code container.size() != n} and {@code container.size() != n}. + * + *

For example, the type of {@code container} in the "then" branch of a conditional statement + * with the test {@code container.size() != n} where {@code n} is 0 should refine to + * {@code @NonEmpty}. + * + * @param lhs the right-hand side of a not equal operator + * @param rhs the left-hand side of a not equals operator + * @param in the initial transfer result before refinement + */ + private void refineNotEqual(Node lhs, Node rhs, TransferResult in) { + if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { + return; } IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; if (integerLiteralNode.getValue() == 0) { @@ -92,9 +115,20 @@ private TransferResult refineNotEqual( JavaExpression receiver = getReceiver(lhs); in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); } - return in; } + /** + * Updates the transfer result's store with information from the Non-Empty type system for + * expressions of the form {@code container.size() > n}. + * + *

For example, the type of {@code container} in the "then" branch of a conditional statement + * with the test {@code container.size() > n} where {@code n >= 0} should be refined to + * {@code @NonEmpty}. + * + * @param lhs the left-hand side of a greater-than operation + * @param rhs the right-hand side of a greater-than operation + * @param store the abstract store to update + */ private void refineGT(Node lhs, Node rhs, CFStore store) { if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { return; @@ -107,6 +141,18 @@ private void refineGT(Node lhs, Node rhs, CFStore store) { } } + /** + * Updates the transfer result's store with information from the Non-Empty type system for + * expressions of the form {@code container.size() >= n}. + * + *

For example, the type of {@code container} in the "then" branch of a conditional statement + * with the test {@code container.size() >= n} where {@code n > 0} should be refined to + * {@code @NonEmpty}. + * + * @param lhs the left-hand side of a greater-than-or-equal operation + * @param rhs the right-hand side of a greater-than-or-equal operation + * @param store the abstract store to update + */ private void refineGTE(Node lhs, Node rhs, CFStore store) { if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { return; @@ -124,15 +170,22 @@ private void refineGTE(Node lhs, Node rhs, CFStore store) { * Given a node that is a possible call to Collection.size(), return true if and only if this is * the case. * - * @param possibleSizeAccess a node that may be a method call to Collection.size() + * @param possibleSizeAccess a node that may be a method call to the {@code size()} method in the * @return true iff the node is a method call to Collection.size() */ private boolean isSizeAccess(Node possibleSizeAccess) { return NodeUtils.isMethodInvocation(possibleSizeAccess, collectionSize, env); } - private JavaExpression getReceiver(Node sizeAccessNode) { - MethodAccessNode methodAccessNode = ((MethodInvocationNode) sizeAccessNode).getTarget(); + /** + * Given a node that is an instance of a method access, return the receiver as a {@link + * JavaExpression}. + * + * @param node an instance of a method access + * @return the receiver as a {@link JavaExpression} + */ + private JavaExpression getReceiver(Node node) { + MethodAccessNode methodAccessNode = ((MethodInvocationNode) node).getTarget(); return JavaExpression.fromNode(methodAccessNode.getReceiver()); } } From feb14e27ab7cef2ea6d9d4161fe2b195782e273f Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 30 Jan 2024 21:17:22 -0800 Subject: [PATCH 031/110] Invert conditionals and comment-out tests --- .../checker/nonempty/NonEmptyTransfer.java | 52 ++++++++----------- .../tests/nonempty/list/ListOperations.java | 26 +++++----- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index d6f1e6480cf..4e5ab62fbb3 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -106,14 +106,12 @@ public TransferResult visitGreaterThanOrEqual( * @param in the initial transfer result before refinement */ private void refineNotEqual(Node lhs, Node rhs, TransferResult in) { - if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { - return; - } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; - if (integerLiteralNode.getValue() == 0) { - // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; - JavaExpression receiver = getReceiver(lhs); - in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); + if (isSizeAccess(lhs) && rhs instanceof IntegerLiteralNode) { + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + if (integerLiteralNode.getValue() == 0) { + JavaExpression receiver = getReceiver(lhs); + in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); + } } } @@ -130,14 +128,12 @@ private void refineNotEqual(Node lhs, Node rhs, TransferResult * @param store the abstract store to update */ private void refineGT(Node lhs, Node rhs, CFStore store) { - if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { - return; - } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; - if (integerLiteralNode.getValue() >= 0) { - // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; - JavaExpression receiver = getReceiver(lhs); - store.insertValue(receiver, aTypeFactory.NON_EMPTY); + if (isSizeAccess(lhs) && rhs instanceof IntegerLiteralNode) { + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + if (integerLiteralNode.getValue() >= 0) { + JavaExpression receiver = getReceiver(lhs); + store.insertValue(receiver, aTypeFactory.NON_EMPTY); + } } } @@ -154,32 +150,28 @@ private void refineGT(Node lhs, Node rhs, CFStore store) { * @param store the abstract store to update */ private void refineGTE(Node lhs, Node rhs, CFStore store) { - if (!isSizeAccess(lhs) || !(rhs instanceof IntegerLiteralNode)) { - return; - } - - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; - if (integerLiteralNode.getValue() > 0) { - // Update the `then` store to have @NonEmpty for the receiver of java.util.Collection.size; - JavaExpression receiver = getReceiver(lhs); - store.insertValue(receiver, aTypeFactory.NON_EMPTY); + if (isSizeAccess(lhs) && rhs instanceof IntegerLiteralNode) { + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + if (integerLiteralNode.getValue() > 0) { + JavaExpression receiver = getReceiver(lhs); + store.insertValue(receiver, aTypeFactory.NON_EMPTY); + } } } /** - * Given a node that is a possible call to Collection.size(), return true if and only if this is - * the case. + * Return true if the given node is an instance of a method invocation node for {@code + * Collection.size()}. * * @param possibleSizeAccess a node that may be a method call to the {@code size()} method in the - * @return true iff the node is a method call to Collection.size() + * @return true if the node is a method call to Collection.size() */ private boolean isSizeAccess(Node possibleSizeAccess) { return NodeUtils.isMethodInvocation(possibleSizeAccess, collectionSize, env); } /** - * Given a node that is an instance of a method access, return the receiver as a {@link - * JavaExpression}. + * Return the receiver as a {@link JavaExpression} given a method invocation node. * * @param node an instance of a method access * @return the receiver as a {@link JavaExpression} diff --git a/checker/tests/nonempty/list/ListOperations.java b/checker/tests/nonempty/list/ListOperations.java index 8bee1e7bb55..fe0956fec12 100644 --- a/checker/tests/nonempty/list/ListOperations.java +++ b/checker/tests/nonempty/list/ListOperations.java @@ -4,19 +4,21 @@ class ListOperations { - void testGetOnEmptyList(List strs) { - // :: error: (method.invocation) - strs.get(0); - } + // Skip test until we decide whether to handle accesses on empty containers + // void testGetOnEmptyList(List strs) { + // // :: error: (method.invocation) + // strs.get(0); + // } - void testGetOnNonEmptyList(List strs) { - if (strs.isEmpty()) { - // :: error: (method.invocation) - strs.get(0); - } else { - strs.get(0); // OK - } - } + // Skip test until we decide whether to handle accesses on empty containers + // void testGetOnNonEmptyList(List strs) { + // if (strs.isEmpty()) { + // // :: error: (method.invocation) + // strs.get(0); + // } else { + // strs.get(0); // OK + // } + // } void testAddToEmptyListAndGet() { List nums = new ArrayList<>(); From 10ddf6246af95d00d29eb3be17d0b6e11eb69019 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 31 Jan 2024 14:40:01 -0800 Subject: [PATCH 032/110] Update Map.java annotation tests --- checker/tests/nonempty/map/MapOperations.java | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/checker/tests/nonempty/map/MapOperations.java b/checker/tests/nonempty/map/MapOperations.java index 1e9b1de6682..63073baaf6a 100644 --- a/checker/tests/nonempty/map/MapOperations.java +++ b/checker/tests/nonempty/map/MapOperations.java @@ -3,24 +3,26 @@ class MapOperations { - void addToMapParam(Map m) { - // :: error: (method.invocation) - m.get("hello"); - - m.put("hello", 1); - - @NonEmpty Map m2 = m; // OK - m.get("hello"); // OK - } - - void clearMap(Map m) { - m.put("hello", 1); - m.get("hello"); // OK - - m.clear(); - // :: error: (method.invocation) - m.get("hello"); - } + // Skip test until we decide whether to handle accesses on empty containers + // void addToMapParam(Map m) { + // // :: error: (method.invocation) + // m.get("hello"); + + // m.put("hello", 1); + + // @NonEmpty Map m2 = m; // OK + // m.get("hello"); // OK + // } + + // Skip test until we decide whether to handle accesses on empty containers + // void clearMap(Map m) { + // m.put("hello", 1); + // m.get("hello"); // OK + + // m.clear(); + // // :: error: (method.invocation) + // m.get("hello"); + // } void containsKeyRefinement(Map m, String key) { if (m.containsKey(key)) { From 92dee7b11f7dcbb37b0f7a6eed9d87636f0ac206 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 31 Jan 2024 17:14:01 -0800 Subject: [PATCH 033/110] Add support for `Map.size` in `NonEmptyTransfer` --- .../NonEmptyAnnotatedTypeFactory.java | 2 +- .../checker/nonempty/NonEmptyTransfer.java | 57 ++++++++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 1e789954f0a..21cd6d6367b 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -12,7 +12,7 @@ public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - /** The @{@link NonEmpty} annotation * */ + /** The @{@link NonEmpty} annotation. */ public final AnnotationMirror NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); /** diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 4e5ab62fbb3..c7a40483a66 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -1,5 +1,7 @@ package org.checkerframework.checker.nonempty; +import java.util.Arrays; +import java.util.List; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; import org.checkerframework.dataflow.analysis.TransferInput; @@ -33,6 +35,9 @@ public class NonEmptyTransfer extends CFTransfer { /** The {@code size()} method of the {@link java.util.Collection} interface. */ private final ExecutableElement collectionSize; + /** The {@code size()} method of the {@link java.util.Map} class. */ + private final ExecutableElement mapSize; + /** A {@link NonEmptyAnnotatedTypeFactory} instance. */ private final NonEmptyAnnotatedTypeFactory aTypeFactory; @@ -41,6 +46,7 @@ public NonEmptyTransfer(CFAbstractAnalysis analysi this.env = analysis.getTypeFactory().getProcessingEnv(); this.collectionSize = TreeUtils.getMethod("java.util.Collection", "size", 0, this.env); + this.mapSize = TreeUtils.getMethod("java.util.Map", "size", 0, this.env); this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); } @@ -101,15 +107,17 @@ public TransferResult visitGreaterThanOrEqual( * with the test {@code container.size() != n} where {@code n} is 0 should refine to * {@code @NonEmpty}. * - * @param lhs the right-hand side of a not equal operator - * @param rhs the left-hand side of a not equals operator + * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} + * or {@code Map.size()} + * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} * @param in the initial transfer result before refinement */ - private void refineNotEqual(Node lhs, Node rhs, TransferResult in) { - if (isSizeAccess(lhs) && rhs instanceof IntegerLiteralNode) { - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + private void refineNotEqual( + Node possibleSizeAccess, Node possibleIntegerLiteral, TransferResult in) { + if (isSizeAccess(possibleSizeAccess) && possibleIntegerLiteral instanceof IntegerLiteralNode) { + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; if (integerLiteralNode.getValue() == 0) { - JavaExpression receiver = getReceiver(lhs); + JavaExpression receiver = getReceiver(possibleSizeAccess); in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); } } @@ -123,15 +131,16 @@ private void refineNotEqual(Node lhs, Node rhs, TransferResult * with the test {@code container.size() > n} where {@code n >= 0} should be refined to * {@code @NonEmpty}. * - * @param lhs the left-hand side of a greater-than operation - * @param rhs the right-hand side of a greater-than operation + * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} + * or {@code Map.size()} + * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} * @param store the abstract store to update */ - private void refineGT(Node lhs, Node rhs, CFStore store) { - if (isSizeAccess(lhs) && rhs instanceof IntegerLiteralNode) { - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + private void refineGT(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { + if (isSizeAccess(possibleSizeAccess) && possibleIntegerLiteral instanceof IntegerLiteralNode) { + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; if (integerLiteralNode.getValue() >= 0) { - JavaExpression receiver = getReceiver(lhs); + JavaExpression receiver = getReceiver(possibleSizeAccess); store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } @@ -145,15 +154,15 @@ private void refineGT(Node lhs, Node rhs, CFStore store) { * with the test {@code container.size() >= n} where {@code n > 0} should be refined to * {@code @NonEmpty}. * - * @param lhs the left-hand side of a greater-than-or-equal operation - * @param rhs the right-hand side of a greater-than-or-equal operation + * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} + * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} * @param store the abstract store to update */ - private void refineGTE(Node lhs, Node rhs, CFStore store) { - if (isSizeAccess(lhs) && rhs instanceof IntegerLiteralNode) { - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) rhs; + private void refineGTE(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { + if (isSizeAccess(possibleSizeAccess) && possibleIntegerLiteral instanceof IntegerLiteralNode) { + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; if (integerLiteralNode.getValue() > 0) { - JavaExpression receiver = getReceiver(lhs); + JavaExpression receiver = getReceiver(possibleSizeAccess); store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } @@ -161,13 +170,19 @@ private void refineGTE(Node lhs, Node rhs, CFStore store) { /** * Return true if the given node is an instance of a method invocation node for {@code - * Collection.size()}. + * Collection.size()} or {@code Map.size()}. * * @param possibleSizeAccess a node that may be a method call to the {@code size()} method in the - * @return true if the node is a method call to Collection.size() + * {@link java.util.List} or {@link java.util.Map} types + * @return true if the node is a method call to size() */ private boolean isSizeAccess(Node possibleSizeAccess) { - return NodeUtils.isMethodInvocation(possibleSizeAccess, collectionSize, env); + // In Java 9+, use `List.of()` + List sizeAccessMethods = Arrays.asList(collectionSize, mapSize); + return sizeAccessMethods.stream() + .anyMatch( + sizeAccessMethod -> + NodeUtils.isMethodInvocation(possibleSizeAccess, sizeAccessMethod, env)); } /** From a9b7069ddab2adb6d096827efa8f608cf17ef1e0 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 1 Feb 2024 11:50:11 -0800 Subject: [PATCH 034/110] Add switch test case --- .../checker/nonempty/NonEmptyTransfer.java | 65 ++++++++++++++----- .../nonempty/iterator/IteratorOperations.java | 12 ++++ 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index c7a40483a66..c61dd75f256 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -6,6 +6,8 @@ import javax.lang.model.element.ExecutableElement; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.CaseNode; import org.checkerframework.dataflow.cfg.node.GreaterThanNode; import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode; import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; @@ -99,6 +101,27 @@ public TransferResult visitGreaterThanOrEqual( return result; } + @Override + public TransferResult visitCase( + CaseNode n, TransferInput in) { + TransferResult result = super.visitCase(n, in); + List caseOperands = n.getCaseOperands(); + AssignmentNode assign = n.getSwitchOperand(); + Node switchNode = assign.getExpression(); + if (!isSizeAccess(switchNode)) { + return result; + } + for (Node caseOperand : caseOperands) { + if (!(caseOperand instanceof IntegerLiteralNode)) { + continue; + } + if (((IntegerLiteralNode) caseOperand).getValue() > 0) { + result.getThenStore().insertValue(getReceiver(switchNode), aTypeFactory.NON_EMPTY); + } + } + return result; + } + /** * Updates the transfer result's store with information from the Non-Empty type system for * expressions of the form {@code container.size() != n} and {@code container.size() != n}. @@ -114,12 +137,14 @@ public TransferResult visitGreaterThanOrEqual( */ private void refineNotEqual( Node possibleSizeAccess, Node possibleIntegerLiteral, TransferResult in) { - if (isSizeAccess(possibleSizeAccess) && possibleIntegerLiteral instanceof IntegerLiteralNode) { - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; - if (integerLiteralNode.getValue() == 0) { - JavaExpression receiver = getReceiver(possibleSizeAccess); - in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); - } + if (!isSizeAccess(possibleSizeAccess) + || !(possibleIntegerLiteral instanceof IntegerLiteralNode)) { + return; + } + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; + if (integerLiteralNode.getValue() == 0) { + JavaExpression receiver = getReceiver(possibleSizeAccess); + in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); } } @@ -137,12 +162,14 @@ private void refineNotEqual( * @param store the abstract store to update */ private void refineGT(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { - if (isSizeAccess(possibleSizeAccess) && possibleIntegerLiteral instanceof IntegerLiteralNode) { - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; - if (integerLiteralNode.getValue() >= 0) { - JavaExpression receiver = getReceiver(possibleSizeAccess); - store.insertValue(receiver, aTypeFactory.NON_EMPTY); - } + if (!isSizeAccess(possibleSizeAccess) + || !(possibleIntegerLiteral instanceof IntegerLiteralNode)) { + return; + } + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; + if (integerLiteralNode.getValue() >= 0) { + JavaExpression receiver = getReceiver(possibleSizeAccess); + store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } @@ -159,12 +186,14 @@ private void refineGT(Node possibleSizeAccess, Node possibleIntegerLiteral, CFSt * @param store the abstract store to update */ private void refineGTE(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { - if (isSizeAccess(possibleSizeAccess) && possibleIntegerLiteral instanceof IntegerLiteralNode) { - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; - if (integerLiteralNode.getValue() > 0) { - JavaExpression receiver = getReceiver(possibleSizeAccess); - store.insertValue(receiver, aTypeFactory.NON_EMPTY); - } + if (!isSizeAccess(possibleSizeAccess) + || !(possibleIntegerLiteral instanceof IntegerLiteralNode)) { + return; + } + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; + if (integerLiteralNode.getValue() > 0) { + JavaExpression receiver = getReceiver(possibleSizeAccess); + store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } diff --git a/checker/tests/nonempty/iterator/IteratorOperations.java b/checker/tests/nonempty/iterator/IteratorOperations.java index 7534893a6bc..40c056e8093 100644 --- a/checker/tests/nonempty/iterator/IteratorOperations.java +++ b/checker/tests/nonempty/iterator/IteratorOperations.java @@ -16,4 +16,16 @@ void testPolyNonEmptyIterator(List nums) { @NonEmpty Iterator unknownEmptyIterator = nums.iterator(); } } + + void testSwitchRefinement(List nums) { + switch (nums.size()) { + case 0: + // :: error: (method.invocation) + nums.iterator().next(); + case 1: + @NonEmpty List nums2 = nums; // OK + default: + // Nothing + } + } } From cd8d5e1dd8db9deab4dec1608efcc908c048be92 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 1 Feb 2024 20:42:39 -0800 Subject: [PATCH 035/110] Add transfer function for switch statments --- .../checker/nonempty/NonEmptyTransfer.java | 45 ++++++++++++++----- .../nonempty/iterator/IteratorOperations.java | 31 ++++++++++++- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index c61dd75f256..89f0708fa42 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -108,17 +108,7 @@ public TransferResult visitCase( List caseOperands = n.getCaseOperands(); AssignmentNode assign = n.getSwitchOperand(); Node switchNode = assign.getExpression(); - if (!isSizeAccess(switchNode)) { - return result; - } - for (Node caseOperand : caseOperands) { - if (!(caseOperand instanceof IntegerLiteralNode)) { - continue; - } - if (((IntegerLiteralNode) caseOperand).getValue() > 0) { - result.getThenStore().insertValue(getReceiver(switchNode), aTypeFactory.NON_EMPTY); - } - } + refineSwitchStatement(switchNode, caseOperands, result.getThenStore(), result.getElseStore()); return result; } @@ -182,6 +172,7 @@ private void refineGT(Node possibleSizeAccess, Node possibleIntegerLiteral, CFSt * {@code @NonEmpty}. * * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} + * or {@code Map.size()} * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} * @param store the abstract store to update */ @@ -197,6 +188,38 @@ private void refineGTE(Node possibleSizeAccess, Node possibleIntegerLiteral, CFS } } + /** + * Updates the transfer result's store with information from the Non-Empty type system for switch + * statements, where the test expression is of the form {@code container.size()}. + * + *

For example, the "then" store of any case node with an integer value greater than 0 should + * refine the type of {@code container} to {@code @NonEmpty}. + * + * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} + * or {@code Map.size()} + * @param caseOperands the operands within each case label + * @param thenStore the "then" store + * @param elseStore the "else" store, corresponding to the "default" case label + */ + private void refineSwitchStatement( + Node possibleSizeAccess, List caseOperands, CFStore thenStore, CFStore elseStore) { + if (!isSizeAccess(possibleSizeAccess)) { + return; + } + for (Node caseOperand : caseOperands) { + if (!(caseOperand instanceof IntegerLiteralNode)) { + continue; + } + IntegerLiteralNode caseIntegerLiteral = (IntegerLiteralNode) caseOperand; + JavaExpression receiver = getReceiver(possibleSizeAccess); + // If a value is encountered that is <= 0, the type of the container in the "else" store + // (i.e., the + // default case) is refined to @NonEmpty + CFStore storeToUpdate = caseIntegerLiteral.getValue() > 0 ? thenStore : elseStore; + storeToUpdate.insertValue(receiver, aTypeFactory.NON_EMPTY); + } + } + /** * Return true if the given node is an instance of a method invocation node for {@code * Collection.size()} or {@code Map.size()}. diff --git a/checker/tests/nonempty/iterator/IteratorOperations.java b/checker/tests/nonempty/iterator/IteratorOperations.java index 40c056e8093..87b54d4d357 100644 --- a/checker/tests/nonempty/iterator/IteratorOperations.java +++ b/checker/tests/nonempty/iterator/IteratorOperations.java @@ -17,15 +17,42 @@ void testPolyNonEmptyIterator(List nums) { } } - void testSwitchRefinement(List nums) { + void testSwitchRefinementNoFallthrough(List nums) { switch (nums.size()) { case 0: // :: error: (method.invocation) nums.iterator().next(); + break; case 1: @NonEmpty List nums2 = nums; // OK + break; default: - // Nothing + @NonEmpty List nums3 = nums; // OK + } + } + + void testSwitchRefinementWithFallthrough(List nums) { + switch (nums.size()) { + case 0: + // :: error: (method.invocation) + nums.iterator().next(); + case 1: + // :: error: (assignment) + @NonEmpty List nums2 = nums; + default: + // :: error: (assignment) + @NonEmpty List nums3 = nums; + } + } + + void testSwitchRefinementNoZero(List nums) { + switch (nums.size()) { + case 1: + nums.iterator().next(); + break; + default: + // :: error: (assignment) + @NonEmpty List nums3 = nums; } } } From 5d5cf3aa370db9b3331c2e672d0746b776094a1f Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 2 Feb 2024 12:06:03 -0800 Subject: [PATCH 036/110] Support refinement for equality comparisons --- .../checker/nonempty/NonEmptyTransfer.java | 44 +++++++++++-------- checker/tests/nonempty/list/Comparisons.java | 28 ++++++++++++ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 89f0708fa42..739d945582e 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -6,17 +6,7 @@ import javax.lang.model.element.ExecutableElement; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; -import org.checkerframework.dataflow.cfg.node.AssignmentNode; -import org.checkerframework.dataflow.cfg.node.CaseNode; -import org.checkerframework.dataflow.cfg.node.GreaterThanNode; -import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode; -import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; -import org.checkerframework.dataflow.cfg.node.LessThanNode; -import org.checkerframework.dataflow.cfg.node.LessThanOrEqualNode; -import org.checkerframework.dataflow.cfg.node.MethodAccessNode; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.cfg.node.NotEqualNode; +import org.checkerframework.dataflow.cfg.node.*; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; @@ -52,12 +42,25 @@ public NonEmptyTransfer(CFAbstractAnalysis analysi this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); } + @Override + public TransferResult visitEqualTo( + EqualToNode n, TransferInput in) { + TransferResult result = super.visitEqualTo(n, in); + // Account for the case where size is checked against a non-zero integer + refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); + refineGTE(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); + // A == 0 is the inversion of A != 0 + refineNotEqual(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); + refineNotEqual(n.getRightOperand(), n.getLeftOperand(), result.getElseStore()); + return result; + } + @Override public TransferResult visitNotEqual( NotEqualNode n, TransferInput in) { TransferResult result = super.visitNotEqual(n, in); - refineNotEqual(n.getLeftOperand(), n.getRightOperand(), result); - refineNotEqual(n.getRightOperand(), n.getLeftOperand(), result); + refineNotEqual(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); + refineNotEqual(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); return result; } @@ -114,19 +117,21 @@ public TransferResult visitCase( /** * Updates the transfer result's store with information from the Non-Empty type system for - * expressions of the form {@code container.size() != n} and {@code container.size() != n}. + * expressions of the form {@code container.size() != n} and {@code n != container.size()}. * *

For example, the type of {@code container} in the "then" branch of a conditional statement * with the test {@code container.size() != n} where {@code n} is 0 should refine to * {@code @NonEmpty}. * + *

This method is also used to refine the "else" store of an equality comparison where {@code + * container.size()} is compared against 0. + * * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} * or {@code Map.size()} * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} - * @param in the initial transfer result before refinement + * @param store the abstract store to update */ - private void refineNotEqual( - Node possibleSizeAccess, Node possibleIntegerLiteral, TransferResult in) { + private void refineNotEqual(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { if (!isSizeAccess(possibleSizeAccess) || !(possibleIntegerLiteral instanceof IntegerLiteralNode)) { return; @@ -134,7 +139,7 @@ private void refineNotEqual( IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; if (integerLiteralNode.getValue() == 0) { JavaExpression receiver = getReceiver(possibleSizeAccess); - in.getThenStore().insertValue(receiver, aTypeFactory.NON_EMPTY); + store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } @@ -171,6 +176,9 @@ private void refineGT(Node possibleSizeAccess, Node possibleIntegerLiteral, CFSt * with the test {@code container.size() >= n} where {@code n > 0} should be refined to * {@code @NonEmpty}. * + *

This method is also used to refine the "then" branch of an equality comparison where {@code + * container.size()} is compared against a non-zero value. + * * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} * or {@code Map.size()} * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java index b1edeac0031..a9b1578ba02 100644 --- a/checker/tests/nonempty/list/Comparisons.java +++ b/checker/tests/nonempty/list/Comparisons.java @@ -2,6 +2,34 @@ class Comparisons { + /**** Tests for EQ ****/ + void testEqZeroWithReturn(List strs) { + if (strs.size() == 0) { + // :: error: (method.invocation) + strs.iterator().next(); + return; + } + strs.iterator().next(); // OK + } + + void testEqZeroFallthrough(List strs) { + if (strs.size() == 0) { + // :: error: (method.invocation) + strs.iterator().next(); + } + // :: error: (method.invocation) + strs.iterator().next(); + } + + void testEqNonZero(List strs) { + if (1 == strs.size()) { + strs.iterator().next(); + } else { + // :: error: (method.invocation) + strs.iterator().next(); + } + } + /**** Tests for NE ****/ void t0(List strs) { if (strs.size() != 0) { From a107fb2e5b4d6c2481a8434d6d7ca6a5d98fafd1 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 5 Feb 2024 11:47:40 -0800 Subject: [PATCH 037/110] Implement implicit Non-Empty refinement for Equals --- .../checker/nonempty/NonEmptyTransfer.java | 32 +++++++++++++++++++ checker/tests/nonempty/list/Comparisons.java | 12 +++++++ 2 files changed, 44 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 739d945582e..38f5d8d22ac 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -3,7 +3,10 @@ import java.util.Arrays; import java.util.List; import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.nonempty.qual.NonEmpty; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.*; @@ -46,6 +49,8 @@ public NonEmptyTransfer(CFAbstractAnalysis analysi public TransferResult visitEqualTo( EqualToNode n, TransferInput in) { TransferResult result = super.visitEqualTo(n, in); + // Account for the case where the sizes of two containers are compared + strengthenAnnotationSizeEquals(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); // Account for the case where size is checked against a non-zero integer refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); refineGTE(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); @@ -115,6 +120,33 @@ public TransferResult visitCase( return result; } + /** + * Refine the transfer result's "then" store, given the left- and right-hand side of an equals + * expression comparing container sizes. + * + * @param lhs a node that may be a method invocation for {@code Collection.size()} or {@code + * Map.size()} + * @param rhs a node that may be a method invocation for {@code Collection.size()} or {@code + * Map.size()} + * @param store the "then" store of the comparison operation + */ + private void strengthenAnnotationSizeEquals(Node lhs, Node rhs, CFStore store) { + if (!isSizeAccess(lhs) || !isSizeAccess(rhs)) { + return; + } + @Nullable AnnotationMirror lhsNonEmptyAnno = + aTypeFactory.getAnnotationFromJavaExpression( + getReceiver(lhs), lhs.getTree(), NonEmpty.class); + @Nullable AnnotationMirror rhsNonEmptyAnno = + aTypeFactory.getAnnotationFromJavaExpression( + getReceiver(rhs), rhs.getTree(), NonEmpty.class); + if (lhsNonEmptyAnno != null) { + store.insertValue(getReceiver(rhs), aTypeFactory.NON_EMPTY); + } else if (rhsNonEmptyAnno != null) { + store.insertValue(getReceiver(lhs), aTypeFactory.NON_EMPTY); + } + } + /** * Updates the transfer result's store with information from the Non-Empty type system for * expressions of the form {@code container.size() != n} and {@code n != container.size()}. diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java index a9b1578ba02..d7ccddf01c6 100644 --- a/checker/tests/nonempty/list/Comparisons.java +++ b/checker/tests/nonempty/list/Comparisons.java @@ -1,4 +1,5 @@ import java.util.List; +import org.checkerframework.checker.nonempty.qual.NonEmpty; class Comparisons { @@ -30,6 +31,17 @@ void testEqNonZero(List strs) { } } + void testImplicitNonZero(List strs1, List strs2) { + if (strs1.isEmpty()) { + return; + } + if (strs1.size() == strs2.size()) { + @NonEmpty List strs3 = strs2; // OK + } + // :: error: (assignment) + @NonEmpty List strs4 = strs2; + } + /**** Tests for NE ****/ void t0(List strs) { if (strs.size() != 0) { From 6fc90730e2e6f753cab9ee9200f4b22e2b52ee76 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 5 Feb 2024 14:39:58 -0800 Subject: [PATCH 038/110] Implement implicit refinement for `NotEqualTo` expressions --- .../checker/nonempty/NonEmptyTransfer.java | 5 +++-- checker/tests/nonempty/list/Comparisons.java | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 38f5d8d22ac..0074c86aae7 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -64,6 +64,7 @@ public TransferResult visitEqualTo( public TransferResult visitNotEqual( NotEqualNode n, TransferInput in) { TransferResult result = super.visitNotEqual(n, in); + strengthenAnnotationSizeEquals(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); refineNotEqual(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); refineNotEqual(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); return result; @@ -121,8 +122,8 @@ public TransferResult visitCase( } /** - * Refine the transfer result's "then" store, given the left- and right-hand side of an equals - * expression comparing container sizes. + * Refine the transfer result's store, given the left- and right-hand side of an equality check + * comparing container sizes. * * @param lhs a node that may be a method invocation for {@code Collection.size()} or {@code * Map.size()} diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java index d7ccddf01c6..f5f6ede8c55 100644 --- a/checker/tests/nonempty/list/Comparisons.java +++ b/checker/tests/nonempty/list/Comparisons.java @@ -56,6 +56,19 @@ void t0(List strs) { } } + void testNotEqualsRefineElse(List strs1, List strs2) { + if (strs1.size() <= 0) { + return; + } + if (strs1.size() != strs2.size()) { + // :: error: (assignment) + @NonEmpty List strs3 = strs2; + } else { + @NonEmpty List strs4 = strs1; + @NonEmpty List strs5 = strs2; + } + } + /**** Tests for GT ****/ void t1(List strs) { if (strs.size() > 10) { From f45e8356ab5f159b663f353c364237a6ececad99 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 5 Feb 2024 21:16:58 -0800 Subject: [PATCH 039/110] Add todo for GLB in strengthening annos --- .../org/checkerframework/checker/nonempty/NonEmptyTransfer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 0074c86aae7..2a5db7268bc 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -141,6 +141,7 @@ private void strengthenAnnotationSizeEquals(Node lhs, Node rhs, CFStore store) { @Nullable AnnotationMirror rhsNonEmptyAnno = aTypeFactory.getAnnotationFromJavaExpression( getReceiver(rhs), rhs.getTree(), NonEmpty.class); + // TODO: use aTypeFactory.getQualifierHierarchy().greatestLowerBoundQualifiersOnly() ? if (lhsNonEmptyAnno != null) { store.insertValue(getReceiver(rhs), aTypeFactory.NON_EMPTY); } else if (rhsNonEmptyAnno != null) { From 1c37e41b511f096e7611f8645326c92c44a9dd6d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Feb 2024 08:51:31 -0800 Subject: [PATCH 040/110] Test that `Predicate.test` is pure --- checker/tests/nonempty/PredicateTestMethod.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 checker/tests/nonempty/PredicateTestMethod.java diff --git a/checker/tests/nonempty/PredicateTestMethod.java b/checker/tests/nonempty/PredicateTestMethod.java new file mode 100644 index 00000000000..620edc7f260 --- /dev/null +++ b/checker/tests/nonempty/PredicateTestMethod.java @@ -0,0 +1,17 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +class PredicateTestMethod { + + public static List filter1(Collection coll, Predicate filter) { + List result = new ArrayList<>(); + for (T elt : coll) { + if (filter.test(elt)) { + result.add(elt); + } + } + return result; + } +} From ed46d3067751b8b30aaa06ccd66f558ef84a191b Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 12 Feb 2024 10:43:37 -0800 Subject: [PATCH 041/110] Instantiate `NonEmptyTransfer` via reflection --- .../checker/nonempty/NonEmptyAnnotatedTypeFactory.java | 10 ---------- .../checker/nonempty/NonEmptyTransfer.java | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 21cd6d6367b..07604333bcf 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -4,10 +4,6 @@ import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFStore; -import org.checkerframework.framework.flow.CFTransfer; -import org.checkerframework.framework.flow.CFValue; import org.checkerframework.javacutil.AnnotationBuilder; public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -25,10 +21,4 @@ public NonEmptyAnnotatedTypeFactory(BaseTypeChecker checker) { this.sideEffectsUnrefineAliases = true; this.postInit(); } - - @Override - public CFTransfer createFlowTransferFunction( - CFAbstractAnalysis analysis) { - return new NonEmptyTransfer(analysis); - } } diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 2a5db7268bc..53eea343720 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -12,7 +12,7 @@ import org.checkerframework.dataflow.cfg.node.*; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.util.NodeUtils; -import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; @@ -36,7 +36,7 @@ public class NonEmptyTransfer extends CFTransfer { /** A {@link NonEmptyAnnotatedTypeFactory} instance. */ private final NonEmptyAnnotatedTypeFactory aTypeFactory; - public NonEmptyTransfer(CFAbstractAnalysis analysis) { + public NonEmptyTransfer(CFAnalysis analysis) { super(analysis); this.env = analysis.getTypeFactory().getProcessingEnv(); From d72f6bdf4f1717ff8f3c664eb2b86b8fd4c857ba Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 12 Feb 2024 20:47:19 -0800 Subject: [PATCH 042/110] Implement refinement for `java.util.List indexOf(Object)` --- .../checker/nonempty/NonEmptyTransfer.java | 121 ++++++++++++------ checker/tests/nonempty/list/Comparisons.java | 42 ++++++ 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 53eea343720..b2d502573d8 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -6,7 +6,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.*; @@ -33,6 +32,9 @@ public class NonEmptyTransfer extends CFTransfer { /** The {@code size()} method of the {@link java.util.Map} class. */ private final ExecutableElement mapSize; + /** The {@code indexOf(Object)} method of the {@link java.util.List} class. */ + private final ExecutableElement indexOf; + /** A {@link NonEmptyAnnotatedTypeFactory} instance. */ private final NonEmptyAnnotatedTypeFactory aTypeFactory; @@ -42,6 +44,7 @@ public NonEmptyTransfer(CFAnalysis analysis) { this.env = analysis.getTypeFactory().getProcessingEnv(); this.collectionSize = TreeUtils.getMethod("java.util.Collection", "size", 0, this.env); this.mapSize = TreeUtils.getMethod("java.util.Map", "size", 0, this.env); + this.indexOf = TreeUtils.getMethod("java.util.List", "indexOf", 1, this.env); this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); } @@ -125,20 +128,20 @@ public TransferResult visitCase( * Refine the transfer result's store, given the left- and right-hand side of an equality check * comparing container sizes. * - * @param lhs a node that may be a method invocation for {@code Collection.size()} or {@code - * Map.size()} - * @param rhs a node that may be a method invocation for {@code Collection.size()} or {@code - * Map.size()} + * @param lhs a node that may be a method invocation for {@link java.util.Collection size()} or + * {@link java.util.Map size()} + * @param rhs a node that may be a method invocation for {@link java.util.Collection size()} or + * {@link java.util.Map size()} * @param store the "then" store of the comparison operation */ private void strengthenAnnotationSizeEquals(Node lhs, Node rhs, CFStore store) { if (!isSizeAccess(lhs) || !isSizeAccess(rhs)) { return; } - @Nullable AnnotationMirror lhsNonEmptyAnno = + AnnotationMirror lhsNonEmptyAnno = aTypeFactory.getAnnotationFromJavaExpression( getReceiver(lhs), lhs.getTree(), NonEmpty.class); - @Nullable AnnotationMirror rhsNonEmptyAnno = + AnnotationMirror rhsNonEmptyAnno = aTypeFactory.getAnnotationFromJavaExpression( getReceiver(rhs), rhs.getTree(), NonEmpty.class); // TODO: use aTypeFactory.getQualifierHierarchy().greatestLowerBoundQualifiersOnly() ? @@ -151,7 +154,8 @@ private void strengthenAnnotationSizeEquals(Node lhs, Node rhs, CFStore store) { /** * Updates the transfer result's store with information from the Non-Empty type system for - * expressions of the form {@code container.size() != n} and {@code n != container.size()}. + * expressions of the form {@code container.size() != n}, {@code n != container.size()}, or {@code + * container.indexOf(Object) != n}. * *

For example, the type of {@code container} in the "then" branch of a conditional statement * with the test {@code container.size() != n} where {@code n} is 0 should refine to @@ -160,51 +164,58 @@ private void strengthenAnnotationSizeEquals(Node lhs, Node rhs, CFStore store) { *

This method is also used to refine the "else" store of an equality comparison where {@code * container.size()} is compared against 0. * - * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} - * or {@code Map.size()} - * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} + * @param left the left operand of a binary operation + * @param right the right operand of a binary operation * @param store the abstract store to update */ - private void refineNotEqual(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { - if (!isSizeAccess(possibleSizeAccess) - || !(possibleIntegerLiteral instanceof IntegerLiteralNode)) { + private void refineNotEqual(Node left, Node right, CFStore store) { + boolean isSizeComparison = isSizeComparison(left, right); + boolean isIndexOfComparison = isIndexOfComparison(left, right); + if (!isSizeComparison && !isIndexOfComparison) { return; } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; - if (integerLiteralNode.getValue() == 0) { - JavaExpression receiver = getReceiver(possibleSizeAccess); + // In case of a size() comparison, refine the store if the value is 0 + // In case of a indexOf(Object) check, refine the store if the value is -1 + int threshold = isSizeComparison ? 0 : -1; + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) right; + if (integerLiteralNode.getValue() == threshold) { + JavaExpression receiver = getReceiver(left); store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } /** * Updates the transfer result's store with information from the Non-Empty type system for - * expressions of the form {@code container.size() > n}. + * expressions of the form {@code container.size() > n} or {@code container.indexOf(Object) > n}. * *

For example, the type of {@code container} in the "then" branch of a conditional statement * with the test {@code container.size() > n} where {@code n >= 0} should be refined to * {@code @NonEmpty}. * - * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} - * or {@code Map.size()} - * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} + * @param left the left operand of a binary operation + * @param right the right operand of a binary operation * @param store the abstract store to update */ - private void refineGT(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { - if (!isSizeAccess(possibleSizeAccess) - || !(possibleIntegerLiteral instanceof IntegerLiteralNode)) { + private void refineGT(Node left, Node right, CFStore store) { + boolean isSizeComparison = isSizeComparison(left, right); + boolean isIndexOfComparison = isIndexOfComparison(left, right); + if (!isSizeComparison && !isIndexOfComparison) { return; } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; - if (integerLiteralNode.getValue() >= 0) { - JavaExpression receiver = getReceiver(possibleSizeAccess); + // In case of a size() comparison, refine the store if the value is 0 + // In case of a indexOf(Object) check, refine the store if the value is -1 + int threshold = isSizeComparison ? 0 : -1; + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) right; + if (integerLiteralNode.getValue() >= threshold) { + JavaExpression receiver = getReceiver(left); store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } /** * Updates the transfer result's store with information from the Non-Empty type system for - * expressions of the form {@code container.size() >= n}. + * expressions of the form {@code container.size() >= n} or {@code container.indexOf(Object) >= + * n}. * *

For example, the type of {@code container} in the "then" branch of a conditional statement * with the test {@code container.size() >= n} where {@code n > 0} should be refined to @@ -213,19 +224,25 @@ private void refineGT(Node possibleSizeAccess, Node possibleIntegerLiteral, CFSt *

This method is also used to refine the "then" branch of an equality comparison where {@code * container.size()} is compared against a non-zero value. * - * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} - * or {@code Map.size()} - * @param possibleIntegerLiteral a node that may be an {@link IntegerLiteralNode} + * @param left the left operand of a binary operation + * @param right the right operand of a binary operation * @param store the abstract store to update */ - private void refineGTE(Node possibleSizeAccess, Node possibleIntegerLiteral, CFStore store) { - if (!isSizeAccess(possibleSizeAccess) - || !(possibleIntegerLiteral instanceof IntegerLiteralNode)) { + private void refineGTE(Node left, Node right, CFStore store) { + boolean isSizeComparison = isSizeComparison(left, right); + boolean isIndexOfComparison = isIndexOfComparison(left, right); + if (!isSizeComparison && !isIndexOfComparison) { + return; + } + IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) right; + JavaExpression receiver = getReceiver(left); + // In an indexOf(Object) comparison, if the index is GTE 0, then the object is within the + // container + if (isIndexOfComparison && integerLiteralNode.getValue() >= 0) { + store.insertValue(receiver, aTypeFactory.NON_EMPTY); return; } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) possibleIntegerLiteral; if (integerLiteralNode.getValue() > 0) { - JavaExpression receiver = getReceiver(possibleSizeAccess); store.insertValue(receiver, aTypeFactory.NON_EMPTY); } } @@ -263,11 +280,25 @@ private void refineSwitchStatement( } /** - * Return true if the given node is an instance of a method invocation node for {@code - * Collection.size()} or {@code Map.size()}. + * Check whether a given binary operation corresponds to a {@link java.util.List size()} or {@link + * java.util.Map size()}comparison. + * + * @param left the left operand of a binary operation + * @param right the right operand of a binary operation + * @return true if the operands correspond to a {@link java.util.List size()} or {@link + * java.util.Map size()} comparison + */ + private boolean isSizeComparison(Node left, Node right) { + // Use `List.of()` in Java 9+ + return isSizeAccess(left) && right instanceof IntegerLiteralNode; + } + + /** + * Return true if the given node is an instance of a method invocation node for {@link + * java.util.Collection size()} or {@link java.util.Map size()}. * * @param possibleSizeAccess a node that may be a method call to the {@code size()} method in the - * {@link java.util.List} or {@link java.util.Map} types + * {@link java.util.Collection} or {@link java.util.Map} types * @return true if the node is a method call to size() */ private boolean isSizeAccess(Node possibleSizeAccess) { @@ -279,6 +310,18 @@ private boolean isSizeAccess(Node possibleSizeAccess) { NodeUtils.isMethodInvocation(possibleSizeAccess, sizeAccessMethod, env)); } + /** + * Check whether a given binary operation corresponds to a {@link java.util.List indexOf(Object)} + * comparison. + * + * @param left the left operand of a binary operation + * @param right the right operand of a binary operation + * @return true if the operands correspond to a {@link java.util.List indexOf(Object)} comparison. + */ + private boolean isIndexOfComparison(Node left, Node right) { + return NodeUtils.isMethodInvocation(left, indexOf, env) && right instanceof IntegerLiteralNode; + } + /** * Return the receiver as a {@link JavaExpression} given a method invocation node. * diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java index f5f6ede8c55..9ad02ae857c 100644 --- a/checker/tests/nonempty/list/Comparisons.java +++ b/checker/tests/nonempty/list/Comparisons.java @@ -42,6 +42,15 @@ void testImplicitNonZero(List strs1, List strs2) { @NonEmpty List strs4 = strs2; } + void testEqualIndexOfRefinement(List objs, Object obj) { + if (objs.indexOf(obj) == -1) { + // :: error: (assignment) + @NonEmpty List objs2 = objs; + } else { + objs.iterator().next(); + } + } + /**** Tests for NE ****/ void t0(List strs) { if (strs.size() != 0) { @@ -69,6 +78,21 @@ void testNotEqualsRefineElse(List strs1, List strs2) { } } + void testNotEqualsRefineIndexOf(List objs, Object obj) { + if (objs.indexOf(obj) != -1) { + @NonEmpty List objs2 = objs; + } else { + // :: error: (method.invocation) + objs.iterator().next(); + } + if (-1 != objs.indexOf(obj)) { + @NonEmpty List objs2 = objs; + } else { + // :: error: (method.invocation) + objs.iterator().next(); + } + } + /**** Tests for GT ****/ void t1(List strs) { if (strs.size() > 10) { @@ -103,6 +127,15 @@ void t2(List strs) { } } + void testRefineIndexOfGT(List objs, Object obj) { + if (objs.indexOf(obj) > -1) { + @NonEmpty List objs2 = objs; + } else { + // :: error: (method.invocation) + objs.iterator().next(); + } + } + /**** Tests for GTE ****/ void t3(List strs) { if (strs.size() >= 0) { @@ -120,6 +153,15 @@ void t4(List strs) { } } + void testRefineGTEIndexOf(List strs, String s) { + if (strs.indexOf(s) >= 0) { + strs.iterator().next(); + } else { + // :: error: (assignment) + @NonEmpty List strs2 = strs; + } + } + /**** Tests for LT ****/ void t5(List strs) { if (strs.size() < 10) { From 06601887bc61397b487a0f86e716629f586e7f72 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 12 Feb 2024 21:27:41 -0800 Subject: [PATCH 043/110] Implement `indexOf(Object)` refinement for `switch` statements --- .../checker/nonempty/NonEmptyTransfer.java | 26 +++++++++++-------- .../nonempty/iterator/IteratorOperations.java | 17 ++++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index b2d502573d8..a5f8d765cb0 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -249,20 +249,22 @@ private void refineGTE(Node left, Node right, CFStore store) { /** * Updates the transfer result's store with information from the Non-Empty type system for switch - * statements, where the test expression is of the form {@code container.size()}. + * statements, where the test expression is of the form {@code container.size()} or {@code + * container.indexOf(Object)}. * - *

For example, the "then" store of any case node with an integer value greater than 0 should + *

For example, the "then" store of any case node with an integer value greater than 0 (or -1, + * in the case of the test expression being a call to {@code container.indexOf(Object)}) should * refine the type of {@code container} to {@code @NonEmpty}. * - * @param possibleSizeAccess a node that may be a method invocation for {@code Collection.size()} - * or {@code Map.size()} + * @param testNode a node that is the test expression for a {@code switch} statement * @param caseOperands the operands within each case label * @param thenStore the "then" store * @param elseStore the "else" store, corresponding to the "default" case label */ private void refineSwitchStatement( - Node possibleSizeAccess, List caseOperands, CFStore thenStore, CFStore elseStore) { - if (!isSizeAccess(possibleSizeAccess)) { + Node testNode, List caseOperands, CFStore thenStore, CFStore elseStore) { + boolean isIndexOfAccess = NodeUtils.isMethodInvocation(testNode, indexOf, env); + if (!isSizeAccess(testNode) && !isIndexOfAccess) { return; } for (Node caseOperand : caseOperands) { @@ -270,11 +272,13 @@ private void refineSwitchStatement( continue; } IntegerLiteralNode caseIntegerLiteral = (IntegerLiteralNode) caseOperand; - JavaExpression receiver = getReceiver(possibleSizeAccess); - // If a value is encountered that is <= 0, the type of the container in the "else" store - // (i.e., the - // default case) is refined to @NonEmpty - CFStore storeToUpdate = caseIntegerLiteral.getValue() > 0 ? thenStore : elseStore; + JavaExpression receiver = getReceiver(testNode); + CFStore storeToUpdate; + if (isIndexOfAccess) { + storeToUpdate = caseIntegerLiteral.getValue() >= 0 ? thenStore : elseStore; + } else { + storeToUpdate = caseIntegerLiteral.getValue() > 0 ? thenStore : elseStore; + } storeToUpdate.insertValue(receiver, aTypeFactory.NON_EMPTY); } } diff --git a/checker/tests/nonempty/iterator/IteratorOperations.java b/checker/tests/nonempty/iterator/IteratorOperations.java index 87b54d4d357..a862cbf959c 100644 --- a/checker/tests/nonempty/iterator/IteratorOperations.java +++ b/checker/tests/nonempty/iterator/IteratorOperations.java @@ -55,4 +55,21 @@ void testSwitchRefinementNoZero(List nums) { @NonEmpty List nums3 = nums; } } + + void testSwitchRefinementIndexOf(List strs, String s) { + switch (strs.indexOf(s)) { + case -1: + // :: error: (method.invocation) + strs.iterator().next(); + break; + case 0: + @NonEmpty List strs2 = strs; + case 2: + case 3: + strs.iterator().next(); + break; + default: + @NonEmpty List strs3 = strs; + } + } } From 692b0c4af72de4a94b025db83665c9de45a5c8ba Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 13 Feb 2024 09:43:10 -0800 Subject: [PATCH 044/110] Add test case --- .../tests/nonempty/IndexOfNonNegative.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 checker/tests/nonempty/IndexOfNonNegative.java diff --git a/checker/tests/nonempty/IndexOfNonNegative.java b/checker/tests/nonempty/IndexOfNonNegative.java new file mode 100644 index 00000000000..d111a82af7e --- /dev/null +++ b/checker/tests/nonempty/IndexOfNonNegative.java @@ -0,0 +1,81 @@ +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import org.checkerframework.checker.nonempty.qual.PolyNonEmpty; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class IndexOfNonNegative extends AbstractSet { + + @SideEffectFree + public IndexOfNonNegative() {} + + // Query Operations + + @Pure + @Override + public int size() { + return -1; + } + + @Pure + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Pure + private int indexOf(Object value) { + return -1; + } + + @Pure + @Override + public boolean contains(Object value) { + // return indexOf(value) != -1; + if (indexOf(value) != -1) { + return true; + } else { + return false; + } + } + + // Modification Operations + + @Override + public boolean add(E value) { + return false; + } + + @Override + public boolean remove(Object value) { + return true; + } + + // Bulk Operations + + @Override + public boolean addAll(Collection c) { + return false; + } + + @Override + public boolean removeAll(Collection c) { + return true; + } + + // Inherit retainAll() from AbstractCollection. + + @Override + public void clear() {} + + /////////////////////////////////////////////////////////////////////////// + + // iterators + + @Override + // :: error: (override.receiver) + public @PolyNonEmpty Iterator iterator(@PolyNonEmpty IndexOfNonNegative this) { + throw new Error(""); + } +} From ac242c2556537dc59f9b970990f6f0183c4474f7 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 15 Feb 2024 18:54:30 -0800 Subject: [PATCH 045/110] Add test case for `size()` call in `isEmpty()` --- .../tests/nonempty/IndexOfNonNegative.java | 2 + checker/tests/nonempty/SizeInIsEmpty.java | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 checker/tests/nonempty/SizeInIsEmpty.java diff --git a/checker/tests/nonempty/IndexOfNonNegative.java b/checker/tests/nonempty/IndexOfNonNegative.java index d111a82af7e..1a7b7a91537 100644 --- a/checker/tests/nonempty/IndexOfNonNegative.java +++ b/checker/tests/nonempty/IndexOfNonNegative.java @@ -1,3 +1,5 @@ +// @skip-test : contains() has a call to a locally-defined indexOf() method, which is hard to verify + import java.util.AbstractSet; import java.util.Collection; import java.util.Iterator; diff --git a/checker/tests/nonempty/SizeInIsEmpty.java b/checker/tests/nonempty/SizeInIsEmpty.java new file mode 100644 index 00000000000..b1182723f0d --- /dev/null +++ b/checker/tests/nonempty/SizeInIsEmpty.java @@ -0,0 +1,75 @@ +import java.util.AbstractSet; +import java.util.Iterator; +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; +import org.checkerframework.checker.nonempty.qual.PolyNonEmpty; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class SizeInIsEmpty extends AbstractSet { + + @SideEffectFree + public SizeInIsEmpty() {} + + // Query Operations + + @Pure + @Override + public int size() { + return -1; + } + + @Pure + @Override + @EnsuresNonEmptyIf(result = false, expression = "this") + public boolean isEmpty() { + if (size() == 0) { + return true; + } else { + return false; + } + } + + @EnsuresNonEmptyIf(result = false, expression = "this") + public boolean isEmpty2() { + return size() == 0 ? true : false; + } + + @EnsuresNonEmptyIf(result = false, expression = "this") + public boolean isEmpty3() { + return size() == 0; + } + + //// iterators + + @Override + public @PolyNonEmpty Iterator iterator(@PolyNonEmpty SizeInIsEmpty this) { + throw new Error(""); + } + + void testRefineIsEmpty1(SizeInIsEmpty container) { + if (!container.isEmpty()) { + container.iterator().next(); + } else { + // :: error: (method.invocation) + container.iterator().next(); + } + } + + void testRefineIsEmpty2(SizeInIsEmpty container) { + if (!container.isEmpty2()) { + container.iterator().next(); + } else { + // :: error: (method.invocation) + container.iterator().next(); + } + } + + void testRefineIsEmpty3(SizeInIsEmpty container) { + if (!container.isEmpty3()) { + container.iterator().next(); + } else { + // :: error: (method.invocation) + container.iterator().next(); + } + } +} From 0c8747499aafdf74de1e56cccbf65d221dd38e9b Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 16 Feb 2024 09:38:42 -0800 Subject: [PATCH 046/110] Update code example in manual --- docs/manual/non-empty-checker.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index 39840e414dd..8d6436ecf09 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -87,7 +87,7 @@ List sessionIds = getSessionIds(); // sessionIds has type @UnknownNonEmpty ... if (!sessionIds.isEmpty()) { - List firstId = sessionIds.get(0); // OK, sessionIds has type @NonEmpty + sessionIds.iterator().next(); // OK, sessionIds has type @NonEmpty ... } \end{Verbatim} From 3eceeda3ff37414204aa5917a1ea4855bf91ea74 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 16 Feb 2024 15:23:16 -0800 Subject: [PATCH 047/110] Add manual section and test case for delegation --- .../delegation/DelegatedCallTest.java | 42 +++++++++++++++++++ docs/manual/non-empty-checker.tex | 15 +++++++ 2 files changed, 57 insertions(+) create mode 100644 checker/tests/nonempty/delegation/DelegatedCallTest.java diff --git a/checker/tests/nonempty/delegation/DelegatedCallTest.java b/checker/tests/nonempty/delegation/DelegatedCallTest.java new file mode 100644 index 00000000000..85ae5c1d0c8 --- /dev/null +++ b/checker/tests/nonempty/delegation/DelegatedCallTest.java @@ -0,0 +1,42 @@ +import java.util.IdentityHashMap; +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; + +public class DelegatedCallTest extends IdentityHashMap { + + private static final long serialVersionUID = -5147442142854693854L; + + /** The wrapped map. */ + private final IdentityHashMap map; + + private DelegatedCallTest(IdentityHashMap map) { + this.map = map; + } + + @Override + public int size(DelegatedCallTest this) { + return map.size(); + } + + @Override + @EnsuresNonEmptyIf(result = false, expression = "this") + public boolean isEmpty(DelegatedCallTest this) { + return map.isEmpty(); + } + + @Override + public V get(DelegatedCallTest this, Object key) { + return map.get(key); + } + + @Override + @EnsuresNonEmptyIf(result = true, expression = "this") + public boolean containsKey(DelegatedCallTest this, Object key) { + return map.containsKey(key); + } + + @Override + @EnsuresNonEmptyIf(result = true, expression = "this") + public boolean containsValue(DelegatedCallTest this, Object value) { + return map.containsValue(value); + } +} diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex index 8d6436ecf09..90931118f84 100644 --- a/docs/manual/non-empty-checker.tex +++ b/docs/manual/non-empty-checker.tex @@ -73,6 +73,21 @@ \end{description} +\subsectionAndLabel{Delegation qualifiers}{delegation-qualifiers-overview} + +The Non-Empty Checker invokes an Delegation Checker, +TODO: whose annotations indicate whether an object is ... + +\begin{description} +\item[\refqualclass{checker/delegate/qual}{UnknownDelegate}] +\item[\refqualclass{checker/delegate/qual}{Delegate}] +\item[\refqualclass{checker/delegate/qual}{DelegateBottom}] +\end{description} + +\noindent +Use of these annotations can help you to type-check more code. +TODO: add link to section on Delegation Checker + \sectionAndLabel{Annotating your code with \<@NonEmpty>}{annotating-with-non-empty} The default annotation for collections, iterators, iterables, and maps is From 649f3864f16b7a0924a3fdd120fe3a0b8816d711 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 16 Feb 2024 19:16:40 -0800 Subject: [PATCH 048/110] Add type annos for Delegation checking --- .../checker/nonempty/qual/Delegate.java | 25 +++++++++++++++++ .../checker/nonempty/qual/DelegateBottom.java | 24 +++++++++++++++++ .../nonempty/qual/UnknownDelegate.java | 27 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java new file mode 100644 index 00000000000..011c2b8e64a --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; + +/** + * Ths type qualifier belongs to the Delegation type system. It is not used on its own, but in + * conjunction with other type systems that need to reason about delegation in method calls, such as + * {@link org.checkerframework.checker.nonempty.NonEmptyChecker}. + * + *

This type qualifier indicates that the object acts as a delegate through which calls are made; + * postconditions that apply to the annotated object's method should also apply to the method that + * delegates calls to the annotated object. + * + *

TODO: create manual entry for Delegation Checker and add here. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownDelegate.class) +public @interface Delegate {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java new file mode 100644 index 00000000000..3ebafbdf512 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +/** + * The bottom type in the Delegation type system. Programmers should rarely write this type. + * + *

There are no values of this type (not even {@code null}). + * + *

TODO: create manual entry for Delegation Checker and add here. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@SubtypeOf(Delegate.class) +public @interface DelegateBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java new file mode 100644 index 00000000000..8471a0d1a72 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + +/** + * Used internally by the Delegation type system; should never be written by a programmer. + * + *

Indicates that the annotated value is not known to have calls delegated to it within the + * implementation of a method. It is the top type qualifier in the {@link Delegate} type hierarchy. + * + *

TODO: create manual entry for Delegation Checker and add here. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +@DefaultFor(value = TypeUseLocation.LOWER_BOUND, types = Void.class) +public @interface UnknownDelegate {} From d67005deb10d2e8b64051d2acf601d096d8ff9de Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 20 Feb 2024 11:08:38 -0800 Subject: [PATCH 049/110] Simplify `@Delegate` logic --- .../checker/nonempty/qual/Delegate.java | 28 ++++++++++++------- .../checker/nonempty/qual/DelegateBottom.java | 24 ---------------- .../nonempty/qual/UnknownDelegate.java | 27 ------------------ 3 files changed, 18 insertions(+), 61 deletions(-) delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java index 011c2b8e64a..90b6b50193d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java @@ -5,21 +5,29 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; /** - * Ths type qualifier belongs to the Delegation type system. It is not used on its own, but in - * conjunction with other type systems that need to reason about delegation in method calls, such as - * {@link org.checkerframework.checker.nonempty.NonEmptyChecker}. + * This is an annotation that indicates a field is a delegate, fields are not delegates by default. * - *

This type qualifier indicates that the object acts as a delegate through which calls are made; - * postconditions that apply to the annotated object's method should also apply to the method that - * delegates calls to the annotated object. + *

Here is a way that this annotation may be used: * - *

TODO: create manual entry for Delegation Checker and add here. + *


+ * class MyEnumeration<T> implements Enumeration<T> {
+ *    {@literal @}Delegate
+ *    private Enumeration<T> e;
+ *
+ *    public boolean hasMoreElements() {
+ *      return e.hasMoreElements();
+ *    }
+ * }
+ * 
+ * + * In the example above, {@code MyEnumeration.hasMoreElements()} delegates a call to {@code + * e.hasMoreElements()}. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(UnknownDelegate.class) +@Target({ElementType.FIELD}) public @interface Delegate {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java deleted file mode 100644 index 3ebafbdf512..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegateBottom.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; - -/** - * The bottom type in the Delegation type system. Programmers should rarely write this type. - * - *

There are no values of this type (not even {@code null}). - * - *

TODO: create manual entry for Delegation Checker and add here. - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) -@SubtypeOf(Delegate.class) -public @interface DelegateBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java deleted file mode 100644 index 8471a0d1a72..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownDelegate.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.DefaultFor; -import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TypeUseLocation; - -/** - * Used internally by the Delegation type system; should never be written by a programmer. - * - *

Indicates that the annotated value is not known to have calls delegated to it within the - * implementation of a method. It is the top type qualifier in the {@link Delegate} type hierarchy. - * - *

TODO: create manual entry for Delegation Checker and add here. - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({}) -@DefaultQualifierInHierarchy -@DefaultFor(value = TypeUseLocation.LOWER_BOUND, types = Void.class) -public @interface UnknownDelegate {} From 8cb8fd31749c02a49020357ee931d1de10bfa5f9 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 21 Feb 2024 16:40:40 -0800 Subject: [PATCH 050/110] Add basic `DelegationChecker` implementation --- .../InitializationAnnotatedTypeFactory.java | 4 +- .../initialization/InitializationChecker.java | 17 -- .../checker/nonempty/DelegationChecker.java | 174 ++++++++++++++++++ .../checker/nonempty/messages.properties | 2 + .../checkerframework/javacutil/TreeUtils.java | 14 ++ 5 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java create mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java index db848a02e13..0820ee90b73 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java @@ -582,7 +582,7 @@ public IPair, List> getUninitializedFields( boolean isStatic, Collection receiverAnnotations) { ClassTree currentClass = TreePathUtil.enclosingClass(path); - List fields = InitializationChecker.getAllFields(currentClass); + List fields = TreeUtils.fieldsFromTree(currentClass); List uninitWithInvariantAnno = new ArrayList<>(); List uninitWithoutInvariantAnno = new ArrayList<>(); for (VariableTree field : fields) { @@ -635,7 +635,7 @@ public List getInitializedInvariantFields(Store store, TreePath pa // TODO: Instead of passing the TreePath around, can we use // getCurrentClassTree? ClassTree currentClass = TreePathUtil.enclosingClass(path); - List fields = InitializationChecker.getAllFields(currentClass); + List fields = TreeUtils.fieldsFromTree(currentClass); List initializedFields = new ArrayList<>(); for (VariableTree field : fields) { VariableElement fieldElem = TreeUtils.elementFromDeclaration(field); diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java index 28ec4e209fd..620136cedf3 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java @@ -1,10 +1,5 @@ package org.checkerframework.checker.initialization; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.VariableTree; -import java.util.ArrayList; -import java.util.List; import java.util.NavigableSet; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -31,16 +26,4 @@ public NavigableSet getSuppressWarningsPrefixes() { result.add("fbc"); return result; } - - /** Returns a list of all fields of the given class. */ - public static List getAllFields(ClassTree clazz) { - List fields = new ArrayList<>(); - for (Tree t : clazz.getMembers()) { - if (t.getKind() == Tree.Kind.VARIABLE) { - VariableTree vt = (VariableTree) t; - fields.add(vt); - } - } - return fields; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java new file mode 100644 index 00000000000..15a6f4869f4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -0,0 +1,174 @@ +package org.checkerframework.checker.nonempty; + +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ModifiersTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.VariableTree; +import java.util.ArrayList; +import java.util.List; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Name; +import javax.lang.model.element.VariableElement; +import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.javacutil.TreeUtils; + +/** + * This class enforces checks for the {@link Delegate} annotation. + * + *

It is not a checker for a type system. It enforces the following syntactic checks: + * + *

    + *
  • A class may have up to exactly one field marked with the {@link Delegate} annotation. + *
  • An overridden method's implementation must be exactly a call to the delegate field. + *
+ */ +public class DelegationChecker extends BaseTypeChecker { + + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new Visitor(this); + } + + static class Visitor extends BaseTypeVisitor { + + /** The maximum number of fields marked with {@link Delegate} allowed by in a class. */ + private final int MAX_NUM_DELEGATE_FIELDS = 1; + + /** The field marked with {@link Delegate} for the current class. */ + private @Nullable VariableTree delegate; + + public Visitor(DelegationChecker checker) { + super(checker); + } + + @Override + public void setRoot(CompilationUnitTree tree) { + delegate = null; // Unset the previous delegate when visiting a new class + } + + @Override + public void processClassTree(ClassTree tree) { + List delegates = getDelegateFields(tree); + if (delegates.size() > MAX_NUM_DELEGATE_FIELDS) { + VariableTree latestDelegate = delegates.get(delegates.size() - 1); + checker.reportError(latestDelegate, "multiple.delegate.annotations"); + } else if (delegates.size() == 1) { + delegate = delegates.get(0); + } + super.processClassTree(tree); + } + + @Override + public Void visitMethod(MethodTree tree, Void p) { + Void result = super.visitMethod(tree, p); + if (delegate == null || !isMarkedWithOverride(tree)) { + return result; + } + MethodInvocationTree delegatedMethodCall = getDelegatedCall(tree.getBody()); + if (delegatedMethodCall == null) { + checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); + return result; + } + Name enclosingMethodName = tree.getName(); + if (!isValidDelegateCall(enclosingMethodName, delegatedMethodCall)) { + checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); + } + return result; + } + + /** + * Return true if the given method call is a valid delegate call for the enclosing method. + * + *

A delegate method call must fulfill the following properties: its receiver must be the + * current field marked with {@link Delegate} in the class, and the name of the method call must + * match that of the enclosing method. + * + * @param enclosingMethodName the name of the enclosing method + * @param delegatedMethodCall the delegated method call + * @return true if the given method call is a valid delegate call for the enclosing method + */ + private boolean isValidDelegateCall( + Name enclosingMethodName, MethodInvocationTree delegatedMethodCall) { + assert delegate != null; // This method should only be invoked when delegate is non-null + ExpressionTree methodSelectTree = delegatedMethodCall.getMethodSelect(); + MemberSelectTree fieldAccessTree = (MemberSelectTree) methodSelectTree; + VariableElement delegatedField = TreeUtils.asFieldAccess(fieldAccessTree.getExpression()); + Name delegatedMethodName = TreeUtils.methodName(delegatedMethodCall); + // TODO: is there a better way to check? Comparing names seems fragile. + return enclosingMethodName.equals(delegatedMethodName) + && delegatedField != null + && delegatedField.getSimpleName().equals(delegate.getName()); + } + + /** + * Returns the fields of a class marked with a {@link Delegate} annotation. + * + * @param tree a class + * @return the fields of a class marked with a {@link Delegate} annotation + */ + private List getDelegateFields(ClassTree tree) { + List fields = TreeUtils.fieldsFromTree(tree); + List delegateFields = new ArrayList<>(); + for (VariableTree field : fields) { + List annosOnField = + TreeUtils.annotationsFromTypeAnnotationTrees(field.getModifiers().getAnnotations()); + if (annosOnField.stream() + .anyMatch(anno -> atypeFactory.areSameByClass(anno, Delegate.class))) { + delegateFields.add(field); + } + } + return delegateFields; + } + + /** + * Return true if a method is marked with {@link Override}. + * + * @param tree a method declaration + * @return true if the given method declaration is annotated with {@link Override} + */ + private boolean isMarkedWithOverride(MethodTree tree) { + ModifiersTree modifiersAndAnnos = tree.getModifiers(); + List annosOnMethod = + TreeUtils.annotationsFromTypeAnnotationTrees(modifiersAndAnnos.getAnnotations()); + return annosOnMethod.stream() + .anyMatch(anno -> atypeFactory.areSameByClass(anno, Override.class)); + } + + /** + * Returns the delegate method call, if found, in a method body. + * + *

A delegate method call should be the only statement in a method body. If this is not the + * case, or if there are other statements, return null. + * + * @param tree a method body + * @return the delegate method call + */ + private @Nullable MethodInvocationTree getDelegatedCall(BlockTree tree) { + List stmts = tree.getStatements(); + if (stmts.size() != 1) { + return null; + } + StatementTree stmt = stmts.get(0); + if (!(stmt instanceof ReturnTree)) { + return null; + } + ReturnTree returnStmt = (ReturnTree) stmt; + ExpressionTree returnExpr = returnStmt.getExpression(); + if (!(returnExpr instanceof MethodInvocationTree)) { + return null; + } + return (MethodInvocationTree) returnExpr; + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties b/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties new file mode 100644 index 00000000000..d9674faebbb --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties @@ -0,0 +1,2 @@ +multiple.delegate.annotations=A class may only have one @Delegate field +invalid.delegate=%s does not delegate a call to %s diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 4425d0d2d6c..363c7241a4e 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -75,6 +75,7 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; +import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; @@ -332,6 +333,19 @@ public static boolean isSelfAccess(ExpressionTree tree) { return elementFromDeclaration(tree); } + /** + * Returns the fields that are declared within the given class declaration. + * + * @param tree the {@link ClassTree} node to get the fields for + * @return the list of fields that are declared within the given class declaration + */ + public static List fieldsFromTree(ClassTree tree) { + return tree.getMembers().stream() + .filter(t -> t.getKind() == Kind.VARIABLE) + .map(t -> (VariableTree) t) + .collect(Collectors.toList()); + } + /** * Returns the element corresponding to the given tree. * From 2174fb7b5f4d1109815b127bad928232afb3384e Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 21 Feb 2024 16:45:09 -0800 Subject: [PATCH 051/110] Remove override for `setRoot` as it appears to crash with NPE --- .../checker/nonempty/DelegationChecker.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index 15a6f4869f4..6cd71d3f863 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -2,7 +2,6 @@ import com.sun.source.tree.BlockTree; import com.sun.source.tree.ClassTree; -import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; @@ -52,13 +51,10 @@ public Visitor(DelegationChecker checker) { super(checker); } - @Override - public void setRoot(CompilationUnitTree tree) { - delegate = null; // Unset the previous delegate when visiting a new class - } - @Override public void processClassTree(ClassTree tree) { + delegate = null; // Unset the previous delegate whenever a new class is visited + // TODO: what about inner classes? List delegates = getDelegateFields(tree); if (delegates.size() > MAX_NUM_DELEGATE_FIELDS) { VariableTree latestDelegate = delegates.get(delegates.size() - 1); From 349f825ce075629c47430c8802acdc5750c610e4 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 21 Feb 2024 20:09:51 -0800 Subject: [PATCH 052/110] Make `NonEmptyChecker` extend `DelegationChecker` --- .../checker/nonempty/DelegationChecker.java | 2 +- .../checker/nonempty/NonEmptyChecker.java | 4 +--- .../tests/nonempty/delegation/MultiDelegationTest.java | 9 +++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 checker/tests/nonempty/delegation/MultiDelegationTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index 6cd71d3f863..f47830f4213 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -41,7 +41,7 @@ protected BaseTypeVisitor createSourceVisitor() { static class Visitor extends BaseTypeVisitor { - /** The maximum number of fields marked with {@link Delegate} allowed by in a class. */ + /** The maximum number of fields marked with {@link Delegate} permitted in a class. */ private final int MAX_NUM_DELEGATE_FIELDS = 1; /** The field marked with {@link Delegate} for the current class. */ diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java index ca4054c68e3..4ed028442cc 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java @@ -1,11 +1,9 @@ package org.checkerframework.checker.nonempty; -import org.checkerframework.common.basetype.BaseTypeChecker; - /** * A type-checker that prevents {@link java.util.NoSuchElementException} in the use of container * classes. * * @checker_framework.manual #non-empty-checker Non-Empty Checker */ -public class NonEmptyChecker extends BaseTypeChecker {} +public class NonEmptyChecker extends DelegationChecker {} diff --git a/checker/tests/nonempty/delegation/MultiDelegationTest.java b/checker/tests/nonempty/delegation/MultiDelegationTest.java new file mode 100644 index 00000000000..8020b2640fe --- /dev/null +++ b/checker/tests/nonempty/delegation/MultiDelegationTest.java @@ -0,0 +1,9 @@ +import org.checkerframework.checker.nonempty.qual.Delegate; + +class MultiDelegationTest { + + @Delegate public int foo; + + // :: error: (multiple.delegate.annotations) + @Delegate public int bar; +} From 82ec19e622ee952fec86ebc1d7d59e36b92badbe Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 21 Feb 2024 20:46:52 -0800 Subject: [PATCH 053/110] Add test case for invalid delegation --- .../delegation/InvalidDelegateTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 checker/tests/nonempty/delegation/InvalidDelegateTest.java diff --git a/checker/tests/nonempty/delegation/InvalidDelegateTest.java b/checker/tests/nonempty/delegation/InvalidDelegateTest.java new file mode 100644 index 00000000000..bbbe14b5bc2 --- /dev/null +++ b/checker/tests/nonempty/delegation/InvalidDelegateTest.java @@ -0,0 +1,30 @@ +import java.util.IdentityHashMap; +import org.checkerframework.checker.nonempty.qual.Delegate; + +public class InvalidDelegateTest extends IdentityHashMap { + + @Delegate public IdentityHashMap map; + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); // OK + } + + @Override + public int size() { + return map.size(); // OK + } + + @Override + // :: warning: (invalid.delegate) + public boolean containsKey(Object key) { + return true; + } + + @Override + // :: warning: (invalid.delegate) + public boolean containsValue(Object value) { + int x = 3; + return map.containsValue(value); + } +} From ff710d58db2895708e782b9ab8be0e58695b24e0 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 23 Feb 2024 13:29:51 -0800 Subject: [PATCH 054/110] Implement very basic delegation verification --- .../checker/nonempty/NonEmptyTransfer.java | 47 +++++++++++++++++++ .../delegation/DelegatedCallTest.java | 3 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index a5f8d765cb0..30d4a149edc 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -1,10 +1,14 @@ package org.checkerframework.checker.nonempty; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; import java.util.Arrays; import java.util.List; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; @@ -15,6 +19,7 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; /** @@ -48,6 +53,30 @@ public NonEmptyTransfer(CFAnalysis analysis) { this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); } + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput in) { + TransferResult result = super.visitMethodInvocation(n, in); + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(n.getTreePath()); + if (enclosingMethod == null || TreeUtils.isConstructor(enclosingMethod)) { + return result; + } + Tree receiverTree = n.getTarget().getReceiver().getTree(); + if (receiverTree == null) { + return result; + } + Element receiver = TreeUtils.elementFromTree(receiverTree); + AnnotationMirror delegateAnno = aTypeFactory.getDeclAnnotation(receiver, Delegate.class); + if (delegateAnno == null) { + return result; + } + JavaExpression thisExpr = JavaExpression.getImplicitReceiver(receiver); + // TODO: check whether the modifier decl annos actually have ensures annos before refinement + refineStoreForDelegationInvocation( + thisExpr, JavaExpression.fromNode(n.getTarget().getReceiver()), result); + return result; + } + @Override public TransferResult visitEqualTo( EqualToNode n, TransferInput in) { @@ -124,6 +153,24 @@ public TransferResult visitCase( return result; } + // TODO: documentation + private void refineStoreForDelegationInvocation( + JavaExpression targetExpr, JavaExpression delegate, TransferResult result) { + if (result.containsTwoStores()) { + CFStore thenStore = result.getThenStore(); + CFValue delegateThenStoreValue = thenStore.getValue(delegate); + thenStore.replaceValue(targetExpr, delegateThenStoreValue); + + CFStore elseStore = result.getElseStore(); + CFValue delegateElseStoreValue = elseStore.getValue(delegate); + elseStore.replaceValue(targetExpr, delegateElseStoreValue); + } else { + CFStore store = result.getRegularStore(); + CFValue delegateStoreValue = store.getValue(delegate); + store.replaceValue(targetExpr, delegateStoreValue); + } + } + /** * Refine the transfer result's store, given the left- and right-hand side of an equality check * comparing container sizes. diff --git a/checker/tests/nonempty/delegation/DelegatedCallTest.java b/checker/tests/nonempty/delegation/DelegatedCallTest.java index 85ae5c1d0c8..71a8e000f8d 100644 --- a/checker/tests/nonempty/delegation/DelegatedCallTest.java +++ b/checker/tests/nonempty/delegation/DelegatedCallTest.java @@ -1,4 +1,5 @@ import java.util.IdentityHashMap; +import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; public class DelegatedCallTest extends IdentityHashMap { @@ -6,7 +7,7 @@ public class DelegatedCallTest extends IdentityHashMap { private static final long serialVersionUID = -5147442142854693854L; /** The wrapped map. */ - private final IdentityHashMap map; + @Delegate private final IdentityHashMap map; private DelegatedCallTest(IdentityHashMap map) { this.map = map; From 3350681d9773cd55c75dfe3c44605c8715ff254a Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 23 Feb 2024 14:22:49 -0800 Subject: [PATCH 055/110] Simplify logic for finding `Override` annotations --- .../checker/nonempty/DelegationChecker.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index f47830f4213..6f2710a6232 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -6,13 +6,13 @@ import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; -import com.sun.source.tree.ModifiersTree; import com.sun.source.tree.ReturnTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.VariableTree; import java.util.ArrayList; import java.util.List; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.nonempty.qual.Delegate; @@ -134,11 +134,8 @@ private List getDelegateFields(ClassTree tree) { * @return true if the given method declaration is annotated with {@link Override} */ private boolean isMarkedWithOverride(MethodTree tree) { - ModifiersTree modifiersAndAnnos = tree.getModifiers(); - List annosOnMethod = - TreeUtils.annotationsFromTypeAnnotationTrees(modifiersAndAnnos.getAnnotations()); - return annosOnMethod.stream() - .anyMatch(anno -> atypeFactory.areSameByClass(anno, Override.class)); + Element method = TreeUtils.elementFromDeclaration(tree); + return atypeFactory.getDeclAnnotation(method, Override.class) != null; } /** From 6dbab9a7f0e505608aade957958b32c3829586f7 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 23 Feb 2024 15:00:54 -0800 Subject: [PATCH 056/110] Update tests --- .../checker/nonempty/NonEmptyTransfer.java | 13 +++++++++---- .../nonempty/delegation/InvalidDelegateTest.java | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 30d4a149edc..c6d087e04f3 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -9,6 +9,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; @@ -57,8 +58,8 @@ public NonEmptyTransfer(CFAnalysis analysis) { public TransferResult visitMethodInvocation( MethodInvocationNode n, TransferInput in) { TransferResult result = super.visitMethodInvocation(n, in); - MethodTree enclosingMethod = TreePathUtil.enclosingMethod(n.getTreePath()); - if (enclosingMethod == null || TreeUtils.isConstructor(enclosingMethod)) { + MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(n.getTreePath()); + if (enclosingMethodTree == null || TreeUtils.isConstructor(enclosingMethodTree)) { return result; } Tree receiverTree = n.getTarget().getReceiver().getTree(); @@ -66,12 +67,14 @@ public TransferResult visitMethodInvocation( return result; } Element receiver = TreeUtils.elementFromTree(receiverTree); + Element enclosingMethod = TreeUtils.elementFromDeclaration(enclosingMethodTree); AnnotationMirror delegateAnno = aTypeFactory.getDeclAnnotation(receiver, Delegate.class); - if (delegateAnno == null) { + AnnotationMirror nonEmptyConditionalPostconditionAnno = + aTypeFactory.getDeclAnnotation(enclosingMethod, EnsuresNonEmptyIf.class); + if (delegateAnno == null || nonEmptyConditionalPostconditionAnno == null) { return result; } JavaExpression thisExpr = JavaExpression.getImplicitReceiver(receiver); - // TODO: check whether the modifier decl annos actually have ensures annos before refinement refineStoreForDelegationInvocation( thisExpr, JavaExpression.fromNode(n.getTarget().getReceiver()), result); return result; @@ -157,10 +160,12 @@ public TransferResult visitCase( private void refineStoreForDelegationInvocation( JavaExpression targetExpr, JavaExpression delegate, TransferResult result) { if (result.containsTwoStores()) { + // Update the "then" store CFStore thenStore = result.getThenStore(); CFValue delegateThenStoreValue = thenStore.getValue(delegate); thenStore.replaceValue(targetExpr, delegateThenStoreValue); + // Update the "else" store CFStore elseStore = result.getElseStore(); CFValue delegateElseStoreValue = elseStore.getValue(delegate); elseStore.replaceValue(targetExpr, delegateElseStoreValue); diff --git a/checker/tests/nonempty/delegation/InvalidDelegateTest.java b/checker/tests/nonempty/delegation/InvalidDelegateTest.java index bbbe14b5bc2..1b59d78aef7 100644 --- a/checker/tests/nonempty/delegation/InvalidDelegateTest.java +++ b/checker/tests/nonempty/delegation/InvalidDelegateTest.java @@ -18,6 +18,7 @@ public int size() { @Override // :: warning: (invalid.delegate) public boolean containsKey(Object key) { + // :: error: (contracts.conditional.postcondition) return true; } From 3c783c6aa3dd9db50754d0244c83bc3fe5f8cd8a Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sat, 24 Feb 2024 16:16:47 -0800 Subject: [PATCH 057/110] Document code --- .../checker/nonempty/NonEmptyTransfer.java | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index c6d087e04f3..f81791eb32f 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -9,6 +9,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.dataflow.analysis.TransferInput; @@ -67,11 +68,7 @@ public TransferResult visitMethodInvocation( return result; } Element receiver = TreeUtils.elementFromTree(receiverTree); - Element enclosingMethod = TreeUtils.elementFromDeclaration(enclosingMethodTree); - AnnotationMirror delegateAnno = aTypeFactory.getDeclAnnotation(receiver, Delegate.class); - AnnotationMirror nonEmptyConditionalPostconditionAnno = - aTypeFactory.getDeclAnnotation(enclosingMethod, EnsuresNonEmptyIf.class); - if (delegateAnno == null || nonEmptyConditionalPostconditionAnno == null) { + if (!shouldRefineStoreForDelegationInvocation(receiver, enclosingMethodTree)) { return result; } JavaExpression thisExpr = JavaExpression.getImplicitReceiver(receiver); @@ -156,7 +153,42 @@ public TransferResult visitCase( return result; } - // TODO: documentation + /** + * Return true if the transfer store for "this" should be updated, depending on whether a delegate + * method invocation is found within a method body. + * + *

Note: the Non-Empty Checker trusts the {@link Delegate} annotations it finds. The {@link + * DelegationChecker} verifies correct use of the delegation pattern. Since it is run alongside + * the Non-Empty Checker, the annotations it finds should be correct. + * + * @param receiver the receiver of a candidate delegate method call found in a method body + * @param enclosingMethodTree the method enclosing the candidate delegate call + * @return true if the receiver is annotated with {@link Delegate} and the method is annotated + * with a postcondition annotation from the Non-Empty type system. + */ + private boolean shouldRefineStoreForDelegationInvocation( + Element receiver, MethodTree enclosingMethodTree) { + Element enclosingMethod = TreeUtils.elementFromDeclaration(enclosingMethodTree); + AnnotationMirror delegateAnno = aTypeFactory.getDeclAnnotation(receiver, Delegate.class); + AnnotationMirror postConditionAnno = + aTypeFactory.getDeclAnnotation(enclosingMethod, EnsuresNonEmpty.class); + AnnotationMirror conditionalPostconditionAnno = + aTypeFactory.getDeclAnnotation(enclosingMethod, EnsuresNonEmptyIf.class); + return delegateAnno != null + && (postConditionAnno != null && conditionalPostconditionAnno != null); + } + + /** + * Updates the value in the store for the target expression when a delegate call is detected. + * + *

For example, if a field {@code map} is marked with {@link Delegate}, and the enclosing class + * delegates a call to it (e.g., a call to {@code containsValue(Object)}), then an instance of the + * enclosing class should have the same postconditions that hold for {@code map}. + * + * @param targetExpr the value for which the store should be updated + * @param delegate the delegate field + * @param result the transfer result + */ private void refineStoreForDelegationInvocation( JavaExpression targetExpr, JavaExpression delegate, TransferResult result) { if (result.containsTwoStores()) { From f8b5c9fb53bb90df49ededd539a3ca65036b041f Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sat, 24 Feb 2024 16:23:08 -0800 Subject: [PATCH 058/110] Fix boolean operator --- .../org/checkerframework/checker/nonempty/NonEmptyTransfer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index f81791eb32f..a9298589270 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -175,7 +175,7 @@ private boolean shouldRefineStoreForDelegationInvocation( AnnotationMirror conditionalPostconditionAnno = aTypeFactory.getDeclAnnotation(enclosingMethod, EnsuresNonEmptyIf.class); return delegateAnno != null - && (postConditionAnno != null && conditionalPostconditionAnno != null); + && (postConditionAnno != null || conditionalPostconditionAnno != null); } /** From 471b30da14bce13a73e746327543d071f1b7c537 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sat, 24 Feb 2024 16:47:44 -0800 Subject: [PATCH 059/110] Support delgation to void methods --- .../checker/nonempty/DelegationChecker.java | 23 +++++++------------ .../nonempty/delegation/VoidDelegateTest.java | 16 +++++++++++++ 2 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 checker/tests/nonempty/delegation/VoidDelegateTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index 6f2710a6232..aacb0dab033 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -1,14 +1,6 @@ package org.checkerframework.checker.nonempty; -import com.sun.source.tree.BlockTree; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MemberSelectTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.ReturnTree; -import com.sun.source.tree.StatementTree; -import com.sun.source.tree.VariableTree; +import com.sun.source.tree.*; import java.util.ArrayList; import java.util.List; import javax.lang.model.element.AnnotationMirror; @@ -153,15 +145,16 @@ private boolean isMarkedWithOverride(MethodTree tree) { return null; } StatementTree stmt = stmts.get(0); - if (!(stmt instanceof ReturnTree)) { - return null; + ExpressionTree lastExprInMethod = null; + if (stmt instanceof ExpressionStatementTree) { + lastExprInMethod = ((ExpressionStatementTree) stmt).getExpression(); + } else if (stmt instanceof ReturnTree) { + lastExprInMethod = ((ReturnTree) stmt).getExpression(); } - ReturnTree returnStmt = (ReturnTree) stmt; - ExpressionTree returnExpr = returnStmt.getExpression(); - if (!(returnExpr instanceof MethodInvocationTree)) { + if (!(lastExprInMethod instanceof MethodInvocationTree)) { return null; } - return (MethodInvocationTree) returnExpr; + return (MethodInvocationTree) lastExprInMethod; } } } diff --git a/checker/tests/nonempty/delegation/VoidDelegateTest.java b/checker/tests/nonempty/delegation/VoidDelegateTest.java new file mode 100644 index 00000000000..e3d7c72d515 --- /dev/null +++ b/checker/tests/nonempty/delegation/VoidDelegateTest.java @@ -0,0 +1,16 @@ +import java.util.ArrayList; +import org.checkerframework.checker.nonempty.qual.Delegate; + +public class VoidDelegateTest extends ArrayList { + + @Delegate private ArrayList array; + + public VoidDelegateTest(ArrayList array) { + this.array = array; + } + + @Override + public void clear() { + this.array.clear(); // This should be OK + } +} From a3c967ff81d847a52f82035904457f10c6081455 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 26 Feb 2024 20:33:20 -0800 Subject: [PATCH 060/110] Allow exceptional exits for delegate methods --- .../checker/nonempty/DelegationChecker.java | 136 ++++++++++++++---- .../DelegatedCallThrowsException.java | 49 +++++++ 2 files changed, 157 insertions(+), 28 deletions(-) create mode 100644 checker/tests/nonempty/delegation/DelegatedCallThrowsException.java diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index aacb0dab033..f1565ad4df0 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -1,18 +1,17 @@ package org.checkerframework.checker.nonempty; import com.sun.source.tree.*; -import java.util.ArrayList; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.Name; -import javax.lang.model.element.VariableElement; +import java.lang.reflect.Method; +import java.util.*; +import javax.lang.model.element.*; import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; /** * This class enforces checks for the {@link Delegate} annotation. @@ -44,6 +43,7 @@ public Visitor(DelegationChecker checker) { } @Override + @SuppressWarnings("UnusedVariable") public void processClassTree(ClassTree tree) { delegate = null; // Unset the previous delegate whenever a new class is visited // TODO: what about inner classes? @@ -53,7 +53,12 @@ public void processClassTree(ClassTree tree) { checker.reportError(latestDelegate, "multiple.delegate.annotations"); } else if (delegates.size() == 1) { delegate = delegates.get(0); + // TODO: compare the current class's overridden methods with that of the supertype. + // Set overridenMethods = getOverriddenMethods(tree); + // Set declaredMethodInSuperType = + // getDeclaredMethodsInSupertype(tree); } + // Do nothing if no delegate field is found super.processClassTree(tree); } @@ -63,13 +68,18 @@ public Void visitMethod(MethodTree tree, Void p) { if (delegate == null || !isMarkedWithOverride(tree)) { return result; } - MethodInvocationTree delegatedMethodCall = getDelegatedCall(tree.getBody()); - if (delegatedMethodCall == null) { + MethodInvocationTree candidateDelegateCall = getLastExpression(tree.getBody()); + boolean hasExceptionalExit = + hasExceptionalExit(tree.getBody(), UnsupportedOperationException.class); + if (hasExceptionalExit) { + return result; + } + if (candidateDelegateCall == null) { checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); return result; } Name enclosingMethodName = tree.getName(); - if (!isValidDelegateCall(enclosingMethodName, delegatedMethodCall)) { + if (!isValidDelegateCall(enclosingMethodName, candidateDelegateCall)) { checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); } return result; @@ -106,9 +116,8 @@ private boolean isValidDelegateCall( * @return the fields of a class marked with a {@link Delegate} annotation */ private List getDelegateFields(ClassTree tree) { - List fields = TreeUtils.fieldsFromTree(tree); List delegateFields = new ArrayList<>(); - for (VariableTree field : fields) { + for (VariableTree field : TreeUtils.fieldsFromTree(tree)) { List annosOnField = TreeUtils.annotationsFromTypeAnnotationTrees(field.getModifiers().getAnnotations()); if (annosOnField.stream() @@ -120,26 +129,16 @@ private List getDelegateFields(ClassTree tree) { } /** - * Return true if a method is marked with {@link Override}. - * - * @param tree a method declaration - * @return true if the given method declaration is annotated with {@link Override} - */ - private boolean isMarkedWithOverride(MethodTree tree) { - Element method = TreeUtils.elementFromDeclaration(tree); - return atypeFactory.getDeclAnnotation(method, Override.class) != null; - } - - /** - * Returns the delegate method call, if found, in a method body. + * Returns the last expression in a method body. * - *

A delegate method call should be the only statement in a method body. If this is not the - * case, or if there are other statements, return null. + *

This method is used to identify a possible delegate method call. It will check whether a + * method has only one statement (a method invocation or a return statement), and return the + * expression that is associated with it. Otherwise, it will return null. * - * @param tree a method body - * @return the delegate method call + * @param tree the method body + * @return the last expression in the method body */ - private @Nullable MethodInvocationTree getDelegatedCall(BlockTree tree) { + private @Nullable MethodInvocationTree getLastExpression(BlockTree tree) { List stmts = tree.getStatements(); if (stmts.size() != 1) { return null; @@ -156,5 +155,86 @@ private boolean isMarkedWithOverride(MethodTree tree) { } return (MethodInvocationTree) lastExprInMethod; } + + /** + * Return true if the last (and only) statement of the block throws an exception of the given + * class. + * + * @param tree a block tree + * @param clazz a class of exception (usually {@link UnsupportedOperationException}) + * @return true if the last and only statement throws an exception of the given class + */ + private boolean hasExceptionalExit(BlockTree tree, Class clazz) { + List stmts = tree.getStatements(); + if (stmts.size() != 1) { + return false; + } + StatementTree lastStmt = stmts.get(0); + if (!(lastStmt instanceof ThrowTree)) { + return false; + } + ThrowTree throwStmt = (ThrowTree) lastStmt; + AnnotatedTypeMirror throwType = atypeFactory.getAnnotatedType(throwStmt.getExpression()); + Class exceptionClass = TypesUtils.getClassFromType(throwType.getUnderlyingType()); + return exceptionClass.equals(clazz); + } + + /** + * Return a set of all methods in the class that are marked with {@link Override}. + * + * @param tree the class tree + * @return a set of all methods in the class that are marked with {@link Override} + */ + @SuppressWarnings("UnusedMethod") + private Set getOverriddenMethods(ClassTree tree) { + Set overriddenMethods = new HashSet<>(); + for (Tree member : tree.getMembers()) { + if (!(member instanceof MethodTree)) { + continue; + } + MethodTree method = (MethodTree) member; + if (isMarkedWithOverride(method)) { + overriddenMethods.add(TreeUtils.elementFromDeclaration(method)); + } + } + return overriddenMethods; + } + + /** + * Return true if a method is marked with {@link Override}. + * + * @param tree the method declaration + * @return true if the given method declaration is annotated with {@link Override} + */ + private boolean isMarkedWithOverride(MethodTree tree) { + Element method = TreeUtils.elementFromDeclaration(tree); + return atypeFactory.getDeclAnnotation(method, Override.class) != null; + } + + /** + * Return the set of methods declared by the given class's supertype. + * + * @param tree the class tree + * @return the set of method declared by the given class's supertype. + */ + @SuppressWarnings("UnusedMethod") + private Set getDeclaredMethodsInSupertype(ClassTree tree) { + Set declaredMethods = new HashSet<>(); + List superTypes = + atypeFactory.getAnnotatedType(tree).directSupertypes(); + if (superTypes.isEmpty()) { + return declaredMethods; + } + // Multiple inheritance is illegal in Java + Class superType = TypesUtils.getClassFromType(superTypes.get(0).getUnderlyingType()); + for (Method method : superType.getDeclaredMethods()) { + // TODO: check for overrides (e.g., remove(Object) remove(int)) + ExecutableElement methodElement = + TreeUtils.getMethod( + superType.getName(), method.getName(), atypeFactory.getProcessingEnv()); + declaredMethods.add(methodElement); + } + return declaredMethods; + } } } diff --git a/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java b/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java new file mode 100644 index 00000000000..206d5e367d9 --- /dev/null +++ b/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java @@ -0,0 +1,49 @@ +import java.util.IdentityHashMap; +import java.util.Map; +import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; + +public class DelegatedCallThrowsException extends IdentityHashMap { + + private static final long serialVersionUID = -5147442142854693854L; + + /** The wrapped map. */ + @Delegate private final IdentityHashMap map; + + private DelegatedCallThrowsException(IdentityHashMap map) { + this.map = map; + } + + @Override + public int size(DelegatedCallThrowsException this) { + return map.size(); + } + + @Override + @EnsuresNonEmptyIf(result = false, expression = "this") + public boolean isEmpty(DelegatedCallThrowsException this) { + return map.isEmpty(); + } + + @Override + public V get(DelegatedCallThrowsException this, Object key) { + return map.get(key); + } + + @Override + @EnsuresNonEmptyIf(result = true, expression = "this") + public boolean containsKey(DelegatedCallThrowsException this, Object key) { + return map.containsKey(key); + } + + @Override + @EnsuresNonEmptyIf(result = true, expression = "this") + public boolean containsValue(DelegatedCallThrowsException this, Object value) { + return map.containsValue(value); + } + + @Override + public void putAll(DelegatedCallThrowsException this, Map m) { + throw new UnsupportedOperationException(); // OK + } +} From dfb58b9a6e556627e3abaf1916948066cca539e2 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 27 Feb 2024 15:40:46 -0800 Subject: [PATCH 061/110] Start obtaining methods from supertype --- .../checker/nonempty/DelegationChecker.java | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index f1565ad4df0..8a182439339 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -54,9 +54,8 @@ public void processClassTree(ClassTree tree) { } else if (delegates.size() == 1) { delegate = delegates.get(0); // TODO: compare the current class's overridden methods with that of the supertype. - // Set overridenMethods = getOverriddenMethods(tree); - // Set declaredMethodInSuperType = - // getDeclaredMethodsInSupertype(tree); + Set overridenMethods = getOverriddenMethods(tree); + Set declaredMethodInSuperType = getDeclaredMethodsInSupertype(tree); } // Do nothing if no delegate field is found super.processClassTree(tree); @@ -212,29 +211,45 @@ private boolean isMarkedWithOverride(MethodTree tree) { } /** - * Return the set of methods declared by the given class's supertype. + * Return the set of methods declared by the class that the given class extends. + * + *

Note: only the methods declared by the class that the given class extends are returned. + * There is no need to check the methods declared in any interfaces that the given class + * implements, as those must be overridden/declared in the class. * * @param tree the class tree - * @return the set of method declared by the given class's supertype. + * @return the set of methods declared by the class that the given class extends. */ @SuppressWarnings("UnusedMethod") private Set getDeclaredMethodsInSupertype(ClassTree tree) { Set declaredMethods = new HashSet<>(); - List superTypes = - atypeFactory.getAnnotatedType(tree).directSupertypes(); - if (superTypes.isEmpty()) { - return declaredMethods; - } - // Multiple inheritance is illegal in Java - Class superType = TypesUtils.getClassFromType(superTypes.get(0).getUnderlyingType()); + AnnotatedTypeMirror superTypeTm = atypeFactory.getAnnotatedType(tree.getExtendsClause()); + Class superType = TypesUtils.getClassFromType(superTypeTm.getUnderlyingType()); for (Method method : superType.getDeclaredMethods()) { - // TODO: check for overrides (e.g., remove(Object) remove(int)) ExecutableElement methodElement = TreeUtils.getMethod( - superType.getName(), method.getName(), atypeFactory.getProcessingEnv()); + superType.getName(), + method.getName(), + atypeFactory.getProcessingEnv(), + getParameterTypes(method)); declaredMethods.add(methodElement); } return declaredMethods; } + + /** + * Get the list of formal parameter types for a given method. + * + * @param method the method + * @return the formal parameter types for the method + */ + private String[] getParameterTypes(Method method) { + Class[] paramClazzes = method.getParameterTypes(); + String[] paramTypes = new String[paramClazzes.length]; + for (int i = 0; i < paramClazzes.length; i++) { + paramTypes[i] = TypesUtils.typeFromClass(paramClazzes[i], types, elements).toString(); + } + return paramTypes; + } } } From 44d0b51d9a5e095cf6578f50ad25ac276cd4a0ab Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 29 Feb 2024 15:38:04 -0800 Subject: [PATCH 062/110] Add UnmodifiableTest.java --- checker/tests/nonempty/list/UnmodifiableTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 checker/tests/nonempty/list/UnmodifiableTest.java diff --git a/checker/tests/nonempty/list/UnmodifiableTest.java b/checker/tests/nonempty/list/UnmodifiableTest.java new file mode 100644 index 00000000000..78765a8ff82 --- /dev/null +++ b/checker/tests/nonempty/list/UnmodifiableTest.java @@ -0,0 +1,10 @@ +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +class UnmodifiableTest { + + void foo(@NonEmpty List strs) { + Collections.unmodifiableList(strs).iterator().next(); // OK + } +} From 1c7d8d42bff6868b6bb51b92537fb8cb88f67af2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 29 Feb 2024 16:09:07 -0800 Subject: [PATCH 063/110] Expand diagnostics, expand test --- checker/tests/nonempty/list/UnmodifiableTest.java | 11 ++++++++++- .../framework/stub/AnnotationFileElementTypes.java | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/checker/tests/nonempty/list/UnmodifiableTest.java b/checker/tests/nonempty/list/UnmodifiableTest.java index 78765a8ff82..11599a61634 100644 --- a/checker/tests/nonempty/list/UnmodifiableTest.java +++ b/checker/tests/nonempty/list/UnmodifiableTest.java @@ -1,10 +1,19 @@ import java.util.Collections; import java.util.List; import org.checkerframework.checker.nonempty.qual.NonEmpty; +import org.checkerframework.checker.nonempty.qual.PolyNonEmpty; class UnmodifiableTest { void foo(@NonEmpty List strs) { - Collections.unmodifiableList(strs).iterator().next(); // OK + @NonEmpty List copy = Collections.unmodifiableList(strs); + + @NonEmpty List copy2 = unmodifiableList(strs); // WORKS! + + Collections.unmodifiableList(strs).iterator().next(); // should be OK + } + + static @PolyNonEmpty List unmodifiableList(@PolyNonEmpty List list) { + return null; } } diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java index 882464a00f3..83ef1ed7308 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java @@ -454,7 +454,9 @@ private void parseAnnotationFiles(List annotationFiles, AnnotationFileTy if (isParsing()) { System.out.printf("AFET.getDeclAnnotations(%s [%s])%n", elt, elt.getClass()); } else { - System.out.printf("AFET.getDeclAnnotations(%s [%s]) IS NOT PARSING%n", elt, elt.getClass()); + System.out.printf( + "AFET.getDeclAnnotations(%s [%s]) isParsing()==true, so will return empty set%n", + elt, elt.getClass()); } } @@ -894,12 +896,18 @@ private void prepJdkFromFile(URL jdkDirectory) { * @param jdkJarfile the URL pointing to the JDK jarfile */ private void prepJdkFromJar(@SuppressWarnings("UnusedVariable") URL jdkJarfile) { + if (stubDebug) { + System.out.printf("prepJdkFromJar(%s)%n", jdkJarfile); + } JarURLConnection connection = getJarURLConnectionToJdk(); try (JarFile jarFile = connection.getJarFile()) { ArrayList entries = CollectionsPlume.makeArrayList(jarFile.entries()); entries.sort(Comparator.comparing(Object::toString)); for (JarEntry jarEntry : entries) { + if (stubDebug) { + System.out.printf("prepJdkFromJar: considering %s%n", jarEntry.getName()); + } // filter out directories and non-Java files if (jarEntry.isDirectory()) { continue; @@ -923,6 +931,7 @@ private void prepJdkFromJar(@SuppressWarnings("UnusedVariable") URL jdkJarfile) if (stubDebug) { String factoryClass = factory.getClass().getSimpleName().toString(); String jarFileURL = connection.getJarFileURL().toString(); + System.out.printf("Just created remainingJdkStubFilesJar.%n"); System.out.printf( "Contents of remainingJdkStubFilesJar for %s from %s:%n", factoryClass, jarFileURL); printSortedIndented(remainingJdkStubFilesJar.keySet()); From e96fcf502d48105a769ce4b241b34e20a9804053 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 29 Feb 2024 17:00:06 -0800 Subject: [PATCH 064/110] Adding test case for `Collections.java` --- .../collections/UnmodifiableTest.java | 19 +++++++++++++++++++ .../tests/nonempty/list/UnmodifiableTest.java | 10 ---------- 2 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 checker/tests/nonempty/collections/UnmodifiableTest.java delete mode 100644 checker/tests/nonempty/list/UnmodifiableTest.java diff --git a/checker/tests/nonempty/collections/UnmodifiableTest.java b/checker/tests/nonempty/collections/UnmodifiableTest.java new file mode 100644 index 00000000000..4d376481ffc --- /dev/null +++ b/checker/tests/nonempty/collections/UnmodifiableTest.java @@ -0,0 +1,19 @@ +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +class UnmodifiableTest { + + void unmodifiableCopy(@NonEmpty List strs) { + @NonEmpty List strsCopy = Collections.unmodifiableList(strs); // OK + } + + void checkNonEmptyThenCopy(List strs) { + if (strs.isEmpty()) { + // :: error: (method.invocation) + Collections.unmodifiableList(strs).iterator().next(); + } else { + Collections.unmodifiableList(strs).iterator().next(); // OK + } + } +} diff --git a/checker/tests/nonempty/list/UnmodifiableTest.java b/checker/tests/nonempty/list/UnmodifiableTest.java deleted file mode 100644 index 78765a8ff82..00000000000 --- a/checker/tests/nonempty/list/UnmodifiableTest.java +++ /dev/null @@ -1,10 +0,0 @@ -import java.util.Collections; -import java.util.List; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class UnmodifiableTest { - - void foo(@NonEmpty List strs) { - Collections.unmodifiableList(strs).iterator().next(); // OK - } -} From e6fe54a513267d0dd98b39bc017002c936028122 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 29 Feb 2024 21:49:38 -0800 Subject: [PATCH 065/110] Comment-out erroneous override checking --- .../checkerframework/checker/nonempty/DelegationChecker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index 8a182439339..827c8584276 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -54,8 +54,8 @@ public void processClassTree(ClassTree tree) { } else if (delegates.size() == 1) { delegate = delegates.get(0); // TODO: compare the current class's overridden methods with that of the supertype. - Set overridenMethods = getOverriddenMethods(tree); - Set declaredMethodInSuperType = getDeclaredMethodsInSupertype(tree); + // Set overridenMethods = getOverriddenMethods(tree); + // Set declaredMethodInSuperType = getDeclaredMethodsInSupertype(tree); } // Do nothing if no delegate field is found super.processClassTree(tree); From c65f2fc064aa0bbc7c22c75273a6b1891329ffbe Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 3 Mar 2024 10:40:57 -0800 Subject: [PATCH 066/110] Implement naive override checking --- .../checker/nonempty/DelegationChecker.java | 83 ++++++++----------- .../checker/nonempty/messages.properties | 1 + .../delegation/DelegatedCallTest.java | 1 + .../DelegatedCallThrowsException.java | 1 + .../delegation/InvalidDelegateTest.java | 1 + .../nonempty/delegation/VoidDelegateTest.java | 1 + 6 files changed, 41 insertions(+), 47 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index 827c8584276..cc6bbb67a12 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -1,9 +1,11 @@ package org.checkerframework.checker.nonempty; import com.sun.source.tree.*; -import java.lang.reflect.Method; import java.util.*; import javax.lang.model.element.*; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.util.ElementFilter; import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -21,6 +23,7 @@ *

    *
  • A class may have up to exactly one field marked with the {@link Delegate} annotation. *
  • An overridden method's implementation must be exactly a call to the delegate field. + *
  • A class overrides all methods declared in its superclass. *
*/ public class DelegationChecker extends BaseTypeChecker { @@ -53,9 +56,7 @@ public void processClassTree(ClassTree tree) { checker.reportError(latestDelegate, "multiple.delegate.annotations"); } else if (delegates.size() == 1) { delegate = delegates.get(0); - // TODO: compare the current class's overridden methods with that of the supertype. - // Set overridenMethods = getOverriddenMethods(tree); - // Set declaredMethodInSuperType = getDeclaredMethodsInSupertype(tree); + checkSuperClassOverrides(tree); } // Do nothing if no delegate field is found super.processClassTree(tree); @@ -178,13 +179,43 @@ private boolean hasExceptionalExit(BlockTree tree, Class clazz) { return exceptionClass.equals(clazz); } + /** + * Validate whether a class overrides all declared methods in its superclass. + * + *

This is a basic implementation that naively checks whether all the superclass methods have + * been overridden by the subclass. It is unlikely in practice that a delegating subclass needs + * to override all the methods in a superclass for postconditions to hold. + * + * @param tree a class tree + */ + private void checkSuperClassOverrides(ClassTree tree) { + TypeElement classTreeElt = TreeUtils.elementFromDeclaration(tree); + if (classTreeElt == null || classTreeElt.getSuperclass() == null) { + return; + } + DeclaredType superClassMirror = (DeclaredType) classTreeElt.getSuperclass(); + if (superClassMirror == null || superClassMirror.getKind() == TypeKind.NONE) { + return; + } + Set overriddenMethods = getOverriddenMethods(tree); + Set methodsDeclaredInSuperClass = + new HashSet<>( + ElementFilter.methodsIn(superClassMirror.asElement().getEnclosedElements())); + if (!overriddenMethods.containsAll(methodsDeclaredInSuperClass)) { + checker.reportWarning( + tree, + "delegate.override", + tree.getSimpleName(), + TypesUtils.getQualifiedName(superClassMirror)); + } + } + /** * Return a set of all methods in the class that are marked with {@link Override}. * * @param tree the class tree * @return a set of all methods in the class that are marked with {@link Override} */ - @SuppressWarnings("UnusedMethod") private Set getOverriddenMethods(ClassTree tree) { Set overriddenMethods = new HashSet<>(); for (Tree member : tree.getMembers()) { @@ -209,47 +240,5 @@ private boolean isMarkedWithOverride(MethodTree tree) { Element method = TreeUtils.elementFromDeclaration(tree); return atypeFactory.getDeclAnnotation(method, Override.class) != null; } - - /** - * Return the set of methods declared by the class that the given class extends. - * - *

Note: only the methods declared by the class that the given class extends are returned. - * There is no need to check the methods declared in any interfaces that the given class - * implements, as those must be overridden/declared in the class. - * - * @param tree the class tree - * @return the set of methods declared by the class that the given class extends. - */ - @SuppressWarnings("UnusedMethod") - private Set getDeclaredMethodsInSupertype(ClassTree tree) { - Set declaredMethods = new HashSet<>(); - AnnotatedTypeMirror superTypeTm = atypeFactory.getAnnotatedType(tree.getExtendsClause()); - Class superType = TypesUtils.getClassFromType(superTypeTm.getUnderlyingType()); - for (Method method : superType.getDeclaredMethods()) { - ExecutableElement methodElement = - TreeUtils.getMethod( - superType.getName(), - method.getName(), - atypeFactory.getProcessingEnv(), - getParameterTypes(method)); - declaredMethods.add(methodElement); - } - return declaredMethods; - } - - /** - * Get the list of formal parameter types for a given method. - * - * @param method the method - * @return the formal parameter types for the method - */ - private String[] getParameterTypes(Method method) { - Class[] paramClazzes = method.getParameterTypes(); - String[] paramTypes = new String[paramClazzes.length]; - for (int i = 0; i < paramClazzes.length; i++) { - paramTypes[i] = TypesUtils.typeFromClass(paramClazzes[i], types, elements).toString(); - } - return paramTypes; - } } } diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties b/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties index d9674faebbb..8835e594447 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties @@ -1,2 +1,3 @@ multiple.delegate.annotations=A class may only have one @Delegate field invalid.delegate=%s does not delegate a call to %s +delegate.override=%s does not override all methods in %s diff --git a/checker/tests/nonempty/delegation/DelegatedCallTest.java b/checker/tests/nonempty/delegation/DelegatedCallTest.java index 71a8e000f8d..571c616d65e 100644 --- a/checker/tests/nonempty/delegation/DelegatedCallTest.java +++ b/checker/tests/nonempty/delegation/DelegatedCallTest.java @@ -2,6 +2,7 @@ import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; +// :: warning: (delegate.override) public class DelegatedCallTest extends IdentityHashMap { private static final long serialVersionUID = -5147442142854693854L; diff --git a/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java b/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java index 206d5e367d9..dd5e3fa873a 100644 --- a/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java +++ b/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java @@ -3,6 +3,7 @@ import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; +// :: warning: (delegate.override) public class DelegatedCallThrowsException extends IdentityHashMap { private static final long serialVersionUID = -5147442142854693854L; diff --git a/checker/tests/nonempty/delegation/InvalidDelegateTest.java b/checker/tests/nonempty/delegation/InvalidDelegateTest.java index 1b59d78aef7..d12c68dead3 100644 --- a/checker/tests/nonempty/delegation/InvalidDelegateTest.java +++ b/checker/tests/nonempty/delegation/InvalidDelegateTest.java @@ -1,6 +1,7 @@ import java.util.IdentityHashMap; import org.checkerframework.checker.nonempty.qual.Delegate; +// :: warning: (delegate.override) public class InvalidDelegateTest extends IdentityHashMap { @Delegate public IdentityHashMap map; diff --git a/checker/tests/nonempty/delegation/VoidDelegateTest.java b/checker/tests/nonempty/delegation/VoidDelegateTest.java index e3d7c72d515..593dc6d2e03 100644 --- a/checker/tests/nonempty/delegation/VoidDelegateTest.java +++ b/checker/tests/nonempty/delegation/VoidDelegateTest.java @@ -1,6 +1,7 @@ import java.util.ArrayList; import org.checkerframework.checker.nonempty.qual.Delegate; +// :: warning: (delegate.override) public class VoidDelegateTest extends ArrayList { @Delegate private ArrayList array; From 8cdcf6d1f93f50060b2ad4cff2e71aadbd09b076 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 4 Mar 2024 21:23:11 -0800 Subject: [PATCH 067/110] Add `@DelegatorMustOverride` annotation --- .../nonempty/qual/DelegatorMustOverride.java | 49 +++++++++++++++++++ .../checker/nonempty/DelegationChecker.java | 16 +++++- .../delegation/DelegatedCallTest.java | 1 - .../DelegatedCallThrowsException.java | 1 - 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegatorMustOverride.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegatorMustOverride.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegatorMustOverride.java new file mode 100644 index 00000000000..c6c18a2467b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegatorMustOverride.java @@ -0,0 +1,49 @@ +package org.checkerframework.checker.nonempty.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This is an annotation that indicates a method that must be overridden in order for a + * conditional postcondition to hold for a delegating class. + * + *

Here is a way that this annotation may be used: + * + *

Given a class that declares a method with a postcondition annotation: + * + *


+ * class ArrayListVariant<T> {
+ *    {@literal @}EnsuresPresentIf(result = true)
+ *    {@literal @}DelegatorMustOverride
+ *    public boolean hasMoreElements() {
+ *      return e.hasMoreElements();
+ *    }
+ * }
+ * 
+ * + * A delegating client must override the method: + * + *

+ * class MyArrayListVariant<T> extends ArrayListVariant<T> {
+ *
+ *    {@literal @}Delegate
+ *     private ArrayListVariant<T> myList;
+ *
+ *
+ *    {@literal @}Override
+ *    {@literal @}EnsuresPresentIf(result = true)
+ *    public boolean hasMoreElements() {
+ *      return myList.hasMoreElements();
+ *    }
+ * }
+ * 
+ * + * Otherwise, a warning will be raised. + * + * @checker_framework.manual #non-empty-checker Non-Empty Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface DelegatorMustOverride {} diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index cc6bbb67a12..6c41c7e0cac 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -2,11 +2,13 @@ import com.sun.source.tree.*; import java.util.*; +import java.util.stream.Collectors; import javax.lang.model.element.*; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.util.ElementFilter; import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.checker.nonempty.qual.DelegatorMustOverride; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -197,11 +199,21 @@ private void checkSuperClassOverrides(ClassTree tree) { if (superClassMirror == null || superClassMirror.getKind() == TypeKind.NONE) { return; } - Set overriddenMethods = getOverriddenMethods(tree); + Set overriddenMethods = + getOverriddenMethods(tree).stream() + .map(ExecutableElement::getSimpleName) + .collect(Collectors.toSet()); Set methodsDeclaredInSuperClass = new HashSet<>( ElementFilter.methodsIn(superClassMirror.asElement().getEnclosedElements())); - if (!overriddenMethods.containsAll(methodsDeclaredInSuperClass)) { + Set methodsThatMustBeOverriden = + methodsDeclaredInSuperClass.stream() + .filter(e -> atypeFactory.getDeclAnnotation(e, DelegatorMustOverride.class) != null) + .map(ExecutableElement::getSimpleName) + .collect(Collectors.toSet()); + + // TODO: comparing a set of names isn't ideal, what about overloading? + if (!overriddenMethods.containsAll(methodsThatMustBeOverriden)) { checker.reportWarning( tree, "delegate.override", diff --git a/checker/tests/nonempty/delegation/DelegatedCallTest.java b/checker/tests/nonempty/delegation/DelegatedCallTest.java index 571c616d65e..71a8e000f8d 100644 --- a/checker/tests/nonempty/delegation/DelegatedCallTest.java +++ b/checker/tests/nonempty/delegation/DelegatedCallTest.java @@ -2,7 +2,6 @@ import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; -// :: warning: (delegate.override) public class DelegatedCallTest extends IdentityHashMap { private static final long serialVersionUID = -5147442142854693854L; diff --git a/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java b/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java index dd5e3fa873a..206d5e367d9 100644 --- a/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java +++ b/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java @@ -3,7 +3,6 @@ import org.checkerframework.checker.nonempty.qual.Delegate; import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; -// :: warning: (delegate.override) public class DelegatedCallThrowsException extends IdentityHashMap { private static final long serialVersionUID = -5147442142854693854L; From b00303c7cde7a2a4429721ea0212254ad3bb0265 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 5 Mar 2024 16:18:45 -0800 Subject: [PATCH 068/110] Add Non-Empty Checker tree annotator for `NewArrayTree` --- .../NonEmptyAnnotatedTypeFactory.java | 31 +++++++++++++++++++ .../collections/UnmodifiableTest.java | 10 ++++++ 2 files changed, 41 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 07604333bcf..6d88eff76b9 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -1,9 +1,16 @@ package org.checkerframework.checker.nonempty; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.NewArrayTree; +import java.util.List; import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -21,4 +28,28 @@ public NonEmptyAnnotatedTypeFactory(BaseTypeChecker checker) { this.sideEffectsUnrefineAliases = true; this.postInit(); } + + @Override + protected TreeAnnotator createTreeAnnotator() { + return new ListTreeAnnotator(super.createTreeAnnotator(), new NonEmptyTreeAnnotator(this)); + } + + /** The tree annotator for the Non-Empty Checker. */ + private class NonEmptyTreeAnnotator extends TreeAnnotator { + + public NonEmptyTreeAnnotator(AnnotatedTypeFactory aTypeFactory) { + super(aTypeFactory); + } + + @Override + public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { + if (!type.hasEffectiveAnnotation(NON_EMPTY)) { + List initializers = tree.getInitializers(); + if (initializers != null && !initializers.isEmpty()) { + type.replaceAnnotation(NON_EMPTY); + } + } + return super.visitNewArray(tree, type); + } + } } diff --git a/checker/tests/nonempty/collections/UnmodifiableTest.java b/checker/tests/nonempty/collections/UnmodifiableTest.java index 4d376481ffc..559a6296892 100644 --- a/checker/tests/nonempty/collections/UnmodifiableTest.java +++ b/checker/tests/nonempty/collections/UnmodifiableTest.java @@ -16,4 +16,14 @@ void checkNonEmptyThenCopy(List strs) { Collections.unmodifiableList(strs).iterator().next(); // OK } } + + void testVarArgsEmpty() { + // :: error: (assignment) + @NonEmpty List items = List.of(); + } + + void testVarArgsNonEmpty() { + // Requires more than 10 elements to invoke the varargs version + @NonEmpty List items = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); // OK + } } From d0c3ae6531c74ab67e993ad62b4a0bc3f8779218 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 5 Mar 2024 16:28:02 -0800 Subject: [PATCH 069/110] Add test case for `Map.ofEntries` --- .../collections/UnmodifiableTest.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/checker/tests/nonempty/collections/UnmodifiableTest.java b/checker/tests/nonempty/collections/UnmodifiableTest.java index 559a6296892..f0b832c38f4 100644 --- a/checker/tests/nonempty/collections/UnmodifiableTest.java +++ b/checker/tests/nonempty/collections/UnmodifiableTest.java @@ -1,5 +1,8 @@ +import static java.util.Map.entry; + import java.util.Collections; import java.util.List; +import java.util.Map; import org.checkerframework.checker.nonempty.qual.NonEmpty; class UnmodifiableTest { @@ -22,8 +25,27 @@ void testVarArgsEmpty() { @NonEmpty List items = List.of(); } - void testVarArgsNonEmpty() { + void testVarArgsNonEmptyList() { // Requires more than 10 elements to invoke the varargs version @NonEmpty List items = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); // OK } + + void testVarArgsNonEmptyMap() { + // Requires more than 10 elements to invoke the varargs version + @NonEmpty + Map map = + Map.ofEntries( + entry("a", 1), + entry("b", 2), + entry("c", 3), + entry("d", 4), + entry("e", 5), + entry("f", 6), + entry("g", 7), + entry("h", 8), + entry("i", 9), + entry("j", 10), + entry("k", 11), + entry("l", 12)); + } } From 715636447cc761162f5f1740ada843d52a1ae05c Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 7 Mar 2024 08:07:11 -0800 Subject: [PATCH 070/110] Add test cases for `Stream.java` --- checker/tests/nonempty/streams/Streams.java | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 checker/tests/nonempty/streams/Streams.java diff --git a/checker/tests/nonempty/streams/Streams.java b/checker/tests/nonempty/streams/Streams.java new file mode 100644 index 00000000000..57ba66e17dd --- /dev/null +++ b/checker/tests/nonempty/streams/Streams.java @@ -0,0 +1,47 @@ +import java.util.List; +import java.util.stream.Stream; +import org.checkerframework.checker.nonempty.qual.NonEmpty; + +class Streams { + + void testSingletonStreamCreation() { + @NonEmpty Stream strm = Stream.of(1); // OK + } + + void testStreamAnyMatch(Stream strStream) { + if (strStream.anyMatch(str -> str.length() > 10)) { + @NonEmpty Stream neStream = strStream; + } else { + // :: error: (assignment) + @NonEmpty Stream err = strStream; + } + } + + void testStreamAllMatch(Stream strStream) { + if (strStream.allMatch(str -> str.length() > 10)) { + @NonEmpty Stream neStream = strStream; + } else { + // :: error: (assignment) + @NonEmpty Stream err = strStream; + } + } + + void testMapNonEmptyStream(@NonEmpty List strs) { + @NonEmpty Stream lens = strs.stream().map(str -> str.length()); + } + + void testMapNonEmptyStream(Stream strs) { + // :: error: (assignment) + @NonEmpty Stream lens = strs.map(str -> str.length()); + } + + void testNoneMatch(Stream strs) { + if (strs.noneMatch(str -> str.length() < 10)) { + // :: error: (assignment) + @NonEmpty Stream err = strs; + } else { + // something matched; meaning that the stream MUST be non-empty + @NonEmpty Stream nonEmptyStrs = strs; + } + } +} From 074f638371806625743212d7d207c63bc06063a5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 7 Mar 2024 18:09:24 -0800 Subject: [PATCH 071/110] Comments --- .../framework/type/poly/AbstractQualifierPolymorphism.java | 6 +++--- .../org/checkerframework/framework/util/AnnotatedTypes.java | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java index 882266a0cd0..5e577f6cf84 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java +++ b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java @@ -113,9 +113,9 @@ protected AbstractQualifierPolymorphism(ProcessingEnvironment env, AnnotatedType AnnotationMirror top = entry.getValue(); if (type.hasPrimaryAnnotation(poly)) { type.removePrimaryAnnotation(poly); + // Do not add qualifiers to type variables and wildcards. if (type.getKind() != TypeKind.TYPEVAR && type.getKind() != TypeKind.WILDCARD) { - // Do not add qualifiers to type variables and - // wildcards + // It's not a type variable or wildcard. type.addAnnotation(this.qualHierarchy.getBottomAnnotation(top)); } } @@ -475,7 +475,7 @@ private AnnotationMirrorMap visit( * Creates a mapping of polymorphic qualifiers to their instantiations by visiting each * composite type in {@code type}. * - * @param type the AnnotateTypeMirror used to find instantiations + * @param type the AnnotatedTypeMirror used to find instantiations * @param polyType the AnnotatedTypeMirror that may have polymorphic qualifiers * @return a mapping of polymorphic qualifiers to their instantiations */ diff --git a/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java b/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java index 3aa9a966844..ddb027e58a0 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java +++ b/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java @@ -1018,7 +1018,9 @@ public static List expandVarArgsParameters( * *

This expands the parameters if the call uses varargs or contracts the parameters if the call * is to an anonymous class that extends a class with an enclosing type. If the call is neither of - * these, then the parameters are returned unchanged. + * these, then the parameters are returned unchanged. For example, String.format is declared to + * take (String, Object...). Given String.format(a, b, c, d), this returns (String, Object, + * Object, Object). * * @param atypeFactory the type factory to use for fetching annotated types * @param method the method or constructor's type @@ -1099,7 +1101,7 @@ public static List expandVarArgsParametersFromTypes( AnnotatedArrayType varargs = (AnnotatedArrayType) parameters.get(parameters.size() - 1); if (parameters.size() == args.size()) { - // Check if one sent an element or an array + // Check if the client passed an element or an array. AnnotatedTypeMirror lastArg = args.get(args.size() - 1); if (lastArg.getKind() == TypeKind.ARRAY && (getArrayDepth(varargs) == getArrayDepth((AnnotatedArrayType) lastArg) From 2b3cd4b5ca241795f57ef04ba74c892e653d78ae Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 11 Mar 2024 16:44:10 -0700 Subject: [PATCH 072/110] Add WIP for resolving false positives with non-empty streams --- .../checker/optional/OptionalChecker.java | 9 +++ .../checker/optional/OptionalTransfer.java | 68 ++++++++++++++++--- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java index 6604299464d..102a4610b7d 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java @@ -1,6 +1,8 @@ package org.checkerframework.checker.optional; import java.util.Optional; +import java.util.Set; +import org.checkerframework.checker.nonempty.NonEmptyChecker; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.qual.StubFiles; @@ -19,4 +21,11 @@ public class OptionalChecker extends BaseTypeChecker { /** Create an OptionalChecker. */ public OptionalChecker() {} + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + checkers.add(NonEmptyChecker.class); + return checkers; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index de521118adc..a130eb9a88e 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -12,17 +12,24 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.Elements; +import org.checkerframework.checker.nonempty.NonEmptyAnnotatedTypeFactory; +import org.checkerframework.checker.nonempty.NonEmptyChecker; +import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.Present; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; -import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; @@ -38,8 +45,17 @@ public class OptionalTransfer extends CFTransfer { /** The element for java.util.Optional.ifPresentOrElse(), or null. */ private final @Nullable ExecutableElement optionalIfPresentOrElse; - /** The type factory associated with this transfer function. */ - private final AnnotatedTypeFactory atypeFactory; + /** The element for java.util.stream.Stream.max(), or null. */ + private final @Nullable ExecutableElement streamMax; + + /** The element for java.util.stream.Stream.min(), or null. */ + private final @Nullable ExecutableElement streamMin; + + /** The {@link OptionalAnnotatedTypeFactory} instance for this transfer class. */ + private final OptionalAnnotatedTypeFactory optTypeFactory; + + /** The {@link NonEmptyAnnotatedTypeFactory} instance for this transfer class. */ + private final @Nullable NonEmptyAnnotatedTypeFactory neTypeFactory; /** * Create an OptionalTransfer. @@ -48,13 +64,16 @@ public class OptionalTransfer extends CFTransfer { */ public OptionalTransfer(CFAbstractAnalysis analysis) { super(analysis); - atypeFactory = analysis.getTypeFactory(); - Elements elements = atypeFactory.getElementUtils(); + optTypeFactory = (OptionalAnnotatedTypeFactory) analysis.getTypeFactory(); + neTypeFactory = optTypeFactory.getTypeFactoryOfSubcheckerOrNull(NonEmptyChecker.class); + Elements elements = optTypeFactory.getElementUtils(); PRESENT = AnnotationBuilder.fromClass(elements, Present.class); - ProcessingEnvironment env = atypeFactory.getProcessingEnv(); + ProcessingEnvironment env = optTypeFactory.getProcessingEnv(); optionalIfPresent = TreeUtils.getMethod("java.util.Optional", "ifPresent", 1, env); optionalIfPresentOrElse = TreeUtils.getMethodOrNull("java.util.Optional", "ifPresentOrElse", 2, env); + streamMax = TreeUtils.getMethodOrNull("java.util.stream.Stream", "max", 1, env); + streamMin = TreeUtils.getMethodOrNull("java.util.stream.Stream", "min", 1, env); } @Override @@ -70,7 +89,7 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List LambdaExpressionTree lambdaTree = cfgLambda.getLambdaTree(); List lambdaParams = lambdaTree.getParameters(); if (lambdaParams.size() == 1) { - TreePath lambdaPath = atypeFactory.getPath(lambdaTree); + TreePath lambdaPath = optTypeFactory.getPath(lambdaTree); Tree lambdaParent = lambdaPath.getParentPath().getLeaf(); if (lambdaParent.getKind() == Tree.Kind.METHOD_INVOCATION) { MethodInvocationTree invok = (MethodInvocationTree) lambdaParent; @@ -78,9 +97,7 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List if (methodElt.equals(optionalIfPresent) || methodElt.equals(optionalIfPresentOrElse)) { // `underlyingAST` is an invocation of `Optional.ifPresent()` or // `Optional.ifPresentOrElse()`. In the lambda, the receiver is @Present. - ExpressionTree methodSelectTree = TreeUtils.withoutParens(invok.getMethodSelect()); - ExpressionTree receiverTree = ((MemberSelectTree) methodSelectTree).getExpression(); - JavaExpression receiverJe = JavaExpression.fromTree(receiverTree); + JavaExpression receiverJe = JavaExpression.fromTree(getReceiverTree(invok)); result.insertValue(receiverJe, PRESENT); } } @@ -99,4 +116,35 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List return result; } + + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput in) { + TransferResult result = super.visitMethodInvocation(n, in); + if (n.getTree() == null || neTypeFactory == null) { + return result; + } + if (NodeUtils.isMethodInvocation(n, streamMax, optTypeFactory.getProcessingEnv()) + || NodeUtils.isMethodInvocation(n, streamMin, optTypeFactory.getProcessingEnv())) { + ExpressionTree receiverTree = getReceiverTree(n.getTree()); + AnnotatedTypeMirror receiverNonEmptyAtm = neTypeFactory.getAnnotatedType(receiverTree); + if (receiverNonEmptyAtm.hasEffectiveAnnotation(NonEmpty.class)) { + // TODO: debug this + JavaExpression internalRepr = JavaExpression.fromNode(n); + result.getRegularStore().insertValue(internalRepr, PRESENT); + AnnotatedTypeMirror returnType = optTypeFactory.getAnnotatedType(n.getTree()); + System.out.println("ANNOTATED TYPE (BEFORE REPLACEMENT): " + returnType); + System.out.println("N=" + n); + returnType.replaceAnnotation(PRESENT); + System.out.println("ANNOTATED TYPE (AFTER REPLACEMENT): " + returnType); + System.out.println("N=" + n); + } + } + return result; + } + + private ExpressionTree getReceiverTree(MethodInvocationTree invok) { + ExpressionTree methodSelectTree = TreeUtils.withoutParens(invok.getMethodSelect()); + return ((MemberSelectTree) methodSelectTree).getExpression(); + } } From 1ba60430e6bb048a600e68c6b0bdc0092f7dce93 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 11 Mar 2024 17:25:31 -0700 Subject: [PATCH 073/110] Refine the result, not the receiver --- .../checker/optional/OptionalTransfer.java | 83 +++++++++++++------ .../checkerframework/javacutil/TreeUtils.java | 1 + 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index a130eb9a88e..27ada070a9b 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -2,7 +2,6 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LambdaExpressionTree; -import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; @@ -23,6 +22,7 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; @@ -52,10 +52,10 @@ public class OptionalTransfer extends CFTransfer { private final @Nullable ExecutableElement streamMin; /** The {@link OptionalAnnotatedTypeFactory} instance for this transfer class. */ - private final OptionalAnnotatedTypeFactory optTypeFactory; + private final OptionalAnnotatedTypeFactory optionalTypeFactory; /** The {@link NonEmptyAnnotatedTypeFactory} instance for this transfer class. */ - private final @Nullable NonEmptyAnnotatedTypeFactory neTypeFactory; + private final @Nullable NonEmptyAnnotatedTypeFactory nonemptyTypeFactory; /** * Create an OptionalTransfer. @@ -64,11 +64,12 @@ public class OptionalTransfer extends CFTransfer { */ public OptionalTransfer(CFAbstractAnalysis analysis) { super(analysis); - optTypeFactory = (OptionalAnnotatedTypeFactory) analysis.getTypeFactory(); - neTypeFactory = optTypeFactory.getTypeFactoryOfSubcheckerOrNull(NonEmptyChecker.class); - Elements elements = optTypeFactory.getElementUtils(); + optionalTypeFactory = (OptionalAnnotatedTypeFactory) analysis.getTypeFactory(); + nonemptyTypeFactory = + optionalTypeFactory.getTypeFactoryOfSubcheckerOrNull(NonEmptyChecker.class); + Elements elements = optionalTypeFactory.getElementUtils(); PRESENT = AnnotationBuilder.fromClass(elements, Present.class); - ProcessingEnvironment env = optTypeFactory.getProcessingEnv(); + ProcessingEnvironment env = optionalTypeFactory.getProcessingEnv(); optionalIfPresent = TreeUtils.getMethod("java.util.Optional", "ifPresent", 1, env); optionalIfPresentOrElse = TreeUtils.getMethodOrNull("java.util.Optional", "ifPresentOrElse", 2, env); @@ -89,7 +90,7 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List LambdaExpressionTree lambdaTree = cfgLambda.getLambdaTree(); List lambdaParams = lambdaTree.getParameters(); if (lambdaParams.size() == 1) { - TreePath lambdaPath = optTypeFactory.getPath(lambdaTree); + TreePath lambdaPath = optionalTypeFactory.getPath(lambdaTree); Tree lambdaParent = lambdaPath.getParentPath().getLeaf(); if (lambdaParent.getKind() == Tree.Kind.METHOD_INVOCATION) { MethodInvocationTree invok = (MethodInvocationTree) lambdaParent; @@ -97,7 +98,7 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List if (methodElt.equals(optionalIfPresent) || methodElt.equals(optionalIfPresentOrElse)) { // `underlyingAST` is an invocation of `Optional.ifPresent()` or // `Optional.ifPresentOrElse()`. In the lambda, the receiver is @Present. - JavaExpression receiverJe = JavaExpression.fromTree(getReceiverTree(invok)); + JavaExpression receiverJe = JavaExpression.fromTree(TreeUtils.getReceiverTree(invok)); result.insertValue(receiverJe, PRESENT); } } @@ -121,30 +122,60 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List public TransferResult visitMethodInvocation( MethodInvocationNode n, TransferInput in) { TransferResult result = super.visitMethodInvocation(n, in); - if (n.getTree() == null || neTypeFactory == null) { + if (n.getTree() == null || nonemptyTypeFactory == null) { return result; } - if (NodeUtils.isMethodInvocation(n, streamMax, optTypeFactory.getProcessingEnv()) - || NodeUtils.isMethodInvocation(n, streamMin, optTypeFactory.getProcessingEnv())) { - ExpressionTree receiverTree = getReceiverTree(n.getTree()); - AnnotatedTypeMirror receiverNonEmptyAtm = neTypeFactory.getAnnotatedType(receiverTree); + if (NodeUtils.isMethodInvocation(n, streamMax, optionalTypeFactory.getProcessingEnv()) + || NodeUtils.isMethodInvocation(n, streamMin, optionalTypeFactory.getProcessingEnv())) { + ExpressionTree receiverTree = TreeUtils.getReceiverTree(n.getTree()); + AnnotatedTypeMirror receiverNonEmptyAtm = nonemptyTypeFactory.getAnnotatedType(receiverTree); if (receiverNonEmptyAtm.hasEffectiveAnnotation(NonEmpty.class)) { - // TODO: debug this - JavaExpression internalRepr = JavaExpression.fromNode(n); - result.getRegularStore().insertValue(internalRepr, PRESENT); - AnnotatedTypeMirror returnType = optTypeFactory.getAnnotatedType(n.getTree()); - System.out.println("ANNOTATED TYPE (BEFORE REPLACEMENT): " + returnType); - System.out.println("N=" + n); - returnType.replaceAnnotation(PRESENT); - System.out.println("ANNOTATED TYPE (AFTER REPLACEMENT): " + returnType); - System.out.println("N=" + n); + AnnotatedTypeMirror returnType = optionalTypeFactory.getAnnotatedType(n.getTree()); + if (!returnType.hasPrimaryAnnotation(PRESENT)) { + makePresent(result, n); + refineToPresent(result); + } } } return result; } - private ExpressionTree getReceiverTree(MethodInvocationTree invok) { - ExpressionTree methodSelectTree = TreeUtils.withoutParens(invok.getMethodSelect()); - return ((MemberSelectTree) methodSelectTree).getExpression(); + /** + * Sets a given {@link Node} to {@code @Present} in the given {@code store}. + * + * @param store the store to update + * @param node the node that should be absent (non-present) + */ + protected void makePresent(CFStore store, Node node) { + JavaExpression internalRepr = JavaExpression.fromNode(node); + store.insertValue(internalRepr, PRESENT); + } + + /** + * Sets {@code node} to {@code @Present} in the given {@link TransferResult}. + * + * @param result the transfer result to side effect + * @param node the nod to make {@code @Present} + */ + protected void makePresent(TransferResult result, Node node) { + if (result.containsTwoStores()) { + makePresent(result.getThenStore(), node); + makePresent(result.getElseStore(), node); + } else { + makePresent(result.getRegularStore(), node); + } + } + + /** + * Refine the given result to {@code @Present}. + * + * @param result the result to refine to {@code @Present}. + */ + protected void refineToPresent(TransferResult result) { + CFValue oldResultValue = result.getResultValue(); + CFValue refinedResultValue = + analysis.createSingleAnnotationValue(PRESENT, oldResultValue.getUnderlyingType()); + CFValue newResultValue = refinedResultValue.mostSpecific(oldResultValue, null); + result.setResultValue(newResultValue); } } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 1976cac6a46..6bae22d9ecb 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -1105,6 +1105,7 @@ public static boolean isCompileTimeString(ExpressionTree tree) { * @return the expression's receiver tree, or null if it does not have an explicit receiver */ public static @Nullable ExpressionTree getReceiverTree(ExpressionTree expression) { + expression = TreeUtils.withoutParens(expression); ExpressionTree receiver; switch (expression.getKind()) { case METHOD_INVOCATION: From c3478c25d5eaabee05255ba621d54ce76faffb2f Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 11 Mar 2024 20:45:13 -0700 Subject: [PATCH 074/110] Fix typo in documentation --- .../org/checkerframework/checker/optional/OptionalTransfer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 27ada070a9b..0a07ba1fec0 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -155,7 +155,7 @@ protected void makePresent(CFStore store, Node node) { * Sets {@code node} to {@code @Present} in the given {@link TransferResult}. * * @param result the transfer result to side effect - * @param node the nod to make {@code @Present} + * @param node the node to make {@code @Present} */ protected void makePresent(TransferResult result, Node node) { if (result.containsTwoStores()) { From 7b70a6159eee52bad7f6e5258ed9ddd588bdec8e Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 12 Mar 2024 11:15:49 -0700 Subject: [PATCH 075/110] Add support for `Stream.reduce(BinaryOperator)` --- .../checker/optional/OptionalTransfer.java | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 0a07ba1fec0..4c7f28312a6 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -6,7 +6,10 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; +import java.util.function.BinaryOperator; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; @@ -51,6 +54,9 @@ public class OptionalTransfer extends CFTransfer { /** The element for java.util.stream.Stream.min(), or null. */ private final @Nullable ExecutableElement streamMin; + /** The element for java.util.stream.Stream.reduce(BinaryOperator<T>), or null. */ + private final @Nullable ExecutableElement streamReduceNoIdentity; + /** The {@link OptionalAnnotatedTypeFactory} instance for this transfer class. */ private final OptionalAnnotatedTypeFactory optionalTypeFactory; @@ -75,6 +81,7 @@ public OptionalTransfer(CFAbstractAnalysis analysi TreeUtils.getMethodOrNull("java.util.Optional", "ifPresentOrElse", 2, env); streamMax = TreeUtils.getMethodOrNull("java.util.stream.Stream", "max", 1, env); streamMin = TreeUtils.getMethodOrNull("java.util.stream.Stream", "min", 1, env); + streamReduceNoIdentity = TreeUtils.getMethodOrNull("java.util.stream.Stream", "reduce", 1, env); } @Override @@ -125,8 +132,31 @@ public TransferResult visitMethodInvocation( if (n.getTree() == null || nonemptyTypeFactory == null) { return result; } - if (NodeUtils.isMethodInvocation(n, streamMax, optionalTypeFactory.getProcessingEnv()) - || NodeUtils.isMethodInvocation(n, streamMin, optionalTypeFactory.getProcessingEnv())) { + refineStreamOperations(n, result); + return result; + } + + /** + * Refines the result of a call to {@link java.util.stream.Stream#max(Comparator)}, {@link + * java.util.stream.Stream#min(Comparator)}, or {@link + * java.util.stream.Stream#reduce(BinaryOperator)}. + * + *

The presence/emptiness of the Optional value returned in the method invocations above are + * dependent on whether the initial stream (i.e., the receiver) is empty (or not). That is, + * invoking the methods above on a {@code @NonEmpty} stream will return a {@code @Present} + * Optional, while invoking them on an empty stream will return an empty Optional. + * + * @param n the method invocation node + * @param result the transfer result to side effect + */ + private void refineStreamOperations( + MethodInvocationNode n, TransferResult result) { + assert nonemptyTypeFactory != null; + List relevantStreamMethods = + Arrays.asList(streamMax, streamMin, streamReduceNoIdentity); + if (relevantStreamMethods.stream() + .anyMatch( + op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { ExpressionTree receiverTree = TreeUtils.getReceiverTree(n.getTree()); AnnotatedTypeMirror receiverNonEmptyAtm = nonemptyTypeFactory.getAnnotatedType(receiverTree); if (receiverNonEmptyAtm.hasEffectiveAnnotation(NonEmpty.class)) { @@ -137,18 +167,6 @@ public TransferResult visitMethodInvocation( } } } - return result; - } - - /** - * Sets a given {@link Node} to {@code @Present} in the given {@code store}. - * - * @param store the store to update - * @param node the node that should be absent (non-present) - */ - protected void makePresent(CFStore store, Node node) { - JavaExpression internalRepr = JavaExpression.fromNode(node); - store.insertValue(internalRepr, PRESENT); } /** @@ -157,7 +175,7 @@ protected void makePresent(CFStore store, Node node) { * @param result the transfer result to side effect * @param node the node to make {@code @Present} */ - protected void makePresent(TransferResult result, Node node) { + private void makePresent(TransferResult result, Node node) { if (result.containsTwoStores()) { makePresent(result.getThenStore(), node); makePresent(result.getElseStore(), node); @@ -166,12 +184,23 @@ protected void makePresent(TransferResult result, Node node) { } } + /** + * Sets a given {@link Node} to {@code @Present} in the given {@code store}. + * + * @param store the store to update + * @param node the node that should be absent (non-present) + */ + private void makePresent(CFStore store, Node node) { + JavaExpression internalRepr = JavaExpression.fromNode(node); + store.insertValue(internalRepr, PRESENT); + } + /** * Refine the given result to {@code @Present}. * * @param result the result to refine to {@code @Present}. */ - protected void refineToPresent(TransferResult result) { + private void refineToPresent(TransferResult result) { CFValue oldResultValue = result.getResultValue(); CFValue refinedResultValue = analysis.createSingleAnnotationValue(PRESENT, oldResultValue.getUnderlyingType()); From 6d8dcc5b1ac57b8a103814ff73b1f8bb9963c3ac Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 13 Mar 2024 11:39:57 -0700 Subject: [PATCH 076/110] Add support for `Stream.findAny` and `Stream.findFirst` --- .../checker/optional/OptionalTransfer.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 4c7f28312a6..d72d8b5dce1 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -57,6 +57,12 @@ public class OptionalTransfer extends CFTransfer { /** The element for java.util.stream.Stream.reduce(BinaryOperator<T>), or null. */ private final @Nullable ExecutableElement streamReduceNoIdentity; + /** The element for java.util.stream.Stream.findFirst(), or null. */ + private final @Nullable ExecutableElement streamFindFirst; + + /** The element for java.util.stream.Stream.findAny(), or null. */ + private final @Nullable ExecutableElement streamFindAny; + /** The {@link OptionalAnnotatedTypeFactory} instance for this transfer class. */ private final OptionalAnnotatedTypeFactory optionalTypeFactory; @@ -82,6 +88,8 @@ public OptionalTransfer(CFAbstractAnalysis analysi streamMax = TreeUtils.getMethodOrNull("java.util.stream.Stream", "max", 1, env); streamMin = TreeUtils.getMethodOrNull("java.util.stream.Stream", "min", 1, env); streamReduceNoIdentity = TreeUtils.getMethodOrNull("java.util.stream.Stream", "reduce", 1, env); + streamFindFirst = TreeUtils.getMethodOrNull("java.util.stream.Stream", "findFirst", 0, env); + streamFindAny = TreeUtils.getMethodOrNull("java.util.stream.Stream", "findAny", 0, env); } @Override @@ -153,7 +161,7 @@ private void refineStreamOperations( MethodInvocationNode n, TransferResult result) { assert nonemptyTypeFactory != null; List relevantStreamMethods = - Arrays.asList(streamMax, streamMin, streamReduceNoIdentity); + Arrays.asList(streamMax, streamMin, streamReduceNoIdentity, streamFindFirst, streamFindAny); if (relevantStreamMethods.stream() .anyMatch( op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { From d08e2ec733a6341e46860bff0d1c597c6d004947 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 14 Mar 2024 10:03:01 -0700 Subject: [PATCH 077/110] Handle case where receiver may be null --- .../checkerframework/checker/nonempty/NonEmptyTransfer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index a9298589270..64f474fe3bb 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -68,7 +68,8 @@ public TransferResult visitMethodInvocation( return result; } Element receiver = TreeUtils.elementFromTree(receiverTree); - if (!shouldRefineStoreForDelegationInvocation(receiver, enclosingMethodTree)) { + if (receiver == null + || !shouldRefineStoreForDelegationInvocation(receiver, enclosingMethodTree)) { return result; } JavaExpression thisExpr = JavaExpression.getImplicitReceiver(receiver); From a945675d57d8164fae43f2ba58ce99659ab9d128 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 29 Mar 2024 10:20:36 -0700 Subject: [PATCH 078/110] Apply Spotless changes --- .../checker/initialization/InitializationChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java index c1859526397..8942f48ad95 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java @@ -50,6 +50,6 @@ protected boolean messageKeyMatches( // Also support the shorter keys used by typetools return super.messageKeyMatches(messageKey, messageKeyInSuppressWarningsString) || super.messageKeyMatches( - messageKey.replace(".invalid", ""), messageKeyInSuppressWarningsString); + messageKey.replace(".invalid", ""), messageKeyInSuppressWarningsString); } } From ef1eec21a69b57d30f5961619b26c938362b4791 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 9 Apr 2024 10:01:46 -0700 Subject: [PATCH 079/110] Refine types in OptionalTransfer without NonEmptyAnnotatedTypeFactory --- .../checker/nonempty/NonEmptyChecker.java | 14 ++++- .../checker/optional/OptionalChecker.java | 9 --- .../checker/optional/OptionalTransfer.java | 26 ++------- .../checker/optional/OptionalVisitor.java | 58 +++++++++++++++++++ 4 files changed, 76 insertions(+), 31 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java index 4ed028442cc..b703aa2267a 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java @@ -1,9 +1,21 @@ package org.checkerframework.checker.nonempty; +import java.util.Set; +import org.checkerframework.checker.optional.OptionalChecker; +import org.checkerframework.common.basetype.BaseTypeChecker; + /** * A type-checker that prevents {@link java.util.NoSuchElementException} in the use of container * classes. * * @checker_framework.manual #non-empty-checker Non-Empty Checker */ -public class NonEmptyChecker extends DelegationChecker {} +public class NonEmptyChecker extends DelegationChecker { + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + checkers.add(OptionalChecker.class); + return checkers; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java index 102a4610b7d..6604299464d 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java @@ -1,8 +1,6 @@ package org.checkerframework.checker.optional; import java.util.Optional; -import java.util.Set; -import org.checkerframework.checker.nonempty.NonEmptyChecker; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.qual.StubFiles; @@ -21,11 +19,4 @@ public class OptionalChecker extends BaseTypeChecker { /** Create an OptionalChecker. */ public OptionalChecker() {} - - @Override - protected Set> getImmediateSubcheckerClasses() { - Set> checkers = super.getImmediateSubcheckerClasses(); - checkers.add(NonEmptyChecker.class); - return checkers; - } } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index d72d8b5dce1..639ba46f4a1 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -1,6 +1,5 @@ package org.checkerframework.checker.optional; -import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; @@ -14,9 +13,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.Elements; -import org.checkerframework.checker.nonempty.NonEmptyAnnotatedTypeFactory; -import org.checkerframework.checker.nonempty.NonEmptyChecker; -import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.Present; import org.checkerframework.dataflow.analysis.TransferInput; @@ -32,7 +28,6 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; -import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; @@ -66,9 +61,6 @@ public class OptionalTransfer extends CFTransfer { /** The {@link OptionalAnnotatedTypeFactory} instance for this transfer class. */ private final OptionalAnnotatedTypeFactory optionalTypeFactory; - /** The {@link NonEmptyAnnotatedTypeFactory} instance for this transfer class. */ - private final @Nullable NonEmptyAnnotatedTypeFactory nonemptyTypeFactory; - /** * Create an OptionalTransfer. * @@ -77,8 +69,6 @@ public class OptionalTransfer extends CFTransfer { public OptionalTransfer(CFAbstractAnalysis analysis) { super(analysis); optionalTypeFactory = (OptionalAnnotatedTypeFactory) analysis.getTypeFactory(); - nonemptyTypeFactory = - optionalTypeFactory.getTypeFactoryOfSubcheckerOrNull(NonEmptyChecker.class); Elements elements = optionalTypeFactory.getElementUtils(); PRESENT = AnnotationBuilder.fromClass(elements, Present.class); ProcessingEnvironment env = optionalTypeFactory.getProcessingEnv(); @@ -137,7 +127,7 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List public TransferResult visitMethodInvocation( MethodInvocationNode n, TransferInput in) { TransferResult result = super.visitMethodInvocation(n, in); - if (n.getTree() == null || nonemptyTypeFactory == null) { + if (n.getTree() == null) { return result; } refineStreamOperations(n, result); @@ -159,21 +149,13 @@ public TransferResult visitMethodInvocation( */ private void refineStreamOperations( MethodInvocationNode n, TransferResult result) { - assert nonemptyTypeFactory != null; List relevantStreamMethods = Arrays.asList(streamMax, streamMin, streamReduceNoIdentity, streamFindFirst, streamFindAny); if (relevantStreamMethods.stream() .anyMatch( op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { - ExpressionTree receiverTree = TreeUtils.getReceiverTree(n.getTree()); - AnnotatedTypeMirror receiverNonEmptyAtm = nonemptyTypeFactory.getAnnotatedType(receiverTree); - if (receiverNonEmptyAtm.hasEffectiveAnnotation(NonEmpty.class)) { - AnnotatedTypeMirror returnType = optionalTypeFactory.getAnnotatedType(n.getTree()); - if (!returnType.hasPrimaryAnnotation(PRESENT)) { - makePresent(result, n); - refineToPresent(result); - } - } + // TODO: refine result to @Present if the receiver is @NonEmpty + assert result != null; // stub for debugging } } @@ -183,6 +165,7 @@ private void refineStreamOperations( * @param result the transfer result to side effect * @param node the node to make {@code @Present} */ + @SuppressWarnings("UnusedMethod") private void makePresent(TransferResult result, Node node) { if (result.containsTwoStores()) { makePresent(result.getThenStore(), node); @@ -208,6 +191,7 @@ private void makePresent(CFStore store, Node node) { * * @param result the result to refine to {@code @Present}. */ + @SuppressWarnings("UnusedMethod") private void refineToPresent(TransferResult result) { CFValue oldResultValue = result.getResultValue(); CFValue refinedResultValue = diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java index b9c3f38170b..80cb799ad33 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java @@ -8,6 +8,7 @@ import com.sun.source.tree.IfTree; import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; import com.sun.source.tree.ParenthesizedTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; @@ -20,6 +21,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; @@ -28,6 +30,8 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.nonempty.qual.NonEmpty; +import org.checkerframework.checker.nonempty.qual.RequiresNonEmpty; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.OptionalCreator; import org.checkerframework.checker.optional.qual.OptionalEliminator; @@ -40,6 +44,8 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; import org.plumelib.util.IPair; @@ -71,6 +77,9 @@ public class OptionalVisitor /** The element for java.util.stream.Stream.map(). */ private final ExecutableElement streamMap; + /** Set of methods to be checked by the Non-Empty Checker. */ + private final Set methodsForNonEmptyChecker; + /** * Create an OptionalVisitor. * @@ -87,6 +96,7 @@ public OptionalVisitor(BaseTypeChecker checker) { streamFilter = TreeUtils.getMethod("java.util.stream.Stream", "filter", 1, env); streamMap = TreeUtils.getMethod("java.util.stream.Stream", "map", 1, env); + methodsForNonEmptyChecker = new HashSet<>(); } @Override @@ -331,6 +341,41 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { return super.visitMethodInvocation(tree, p); } + @Override + public Void visitMethod(MethodTree tree, Void p) { + if (this.isAnnotatedWithNonEmptyPrecondition(tree) + || this.isAnyFormalAnnotatedWithNonEmpty(tree) + || this.isReturnTypeAnnotatedWithNonEmpty(tree)) { + methodsForNonEmptyChecker.add(tree); + } + return super.visitMethod(tree, p); + } + + private boolean isAnnotatedWithNonEmptyPrecondition(MethodTree tree) { + return TreeUtils.annotationsFromTypeAnnotationTrees(tree.getModifiers().getAnnotations()) + .stream() + .anyMatch(am -> atypeFactory.areSameByClass(am, RequiresNonEmpty.class)); + } + + private boolean isAnyFormalAnnotatedWithNonEmpty(MethodTree tree) { + List params = tree.getParameters(); + AnnotationMirrorSet annotationMirrors = new AnnotationMirrorSet(); + for (VariableTree vt : params) { + annotationMirrors.addAll( + TreeUtils.annotationsFromTypeAnnotationTrees(vt.getModifiers().getAnnotations())); + } + return annotationMirrors.stream() + .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); + } + + private boolean isReturnTypeAnnotatedWithNonEmpty(MethodTree tree) { + if (tree.getReturnType() == null) { + return false; + } + return TreeUtils.typeOf(tree.getReturnType()).getAnnotationMirrors().stream() + .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); + } + @Override public Void visitBinary(BinaryTree tree, Void p) { handleCompareToNull(tree); @@ -472,6 +517,7 @@ public void handleNestedOptionalCreation(MethodInvocationTree tree) { */ @Override public Void visitVariable(VariableTree tree, Void p) { + handleNonEmptyVariableDeclaration(tree); VariableElement ve = TreeUtils.elementFromDeclaration(tree); TypeMirror tm = ve.asType(); if (isOptionalType(tm)) { @@ -491,6 +537,18 @@ public Void visitVariable(VariableTree tree, Void p) { return super.visitVariable(tree, p); } + private void handleNonEmptyVariableDeclaration(VariableTree tree) { + boolean isAnnotatedWithNonEmpty = + TreeUtils.annotationsFromTypeAnnotationTrees(tree.getModifiers().getAnnotations()).stream() + .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); + if (isAnnotatedWithNonEmpty) { + methodsForNonEmptyChecker.add(TreePathUtil.enclosingMethod(this.getCurrentPath())); + System.out.printf( + "Methods to check with the Non-Empty Checker = %s\n", + methodsForNonEmptyChecker.stream().map(MethodTree::getName).collect(Collectors.toSet())); + } + } + /** * Handles Rule #5, part of Rule #6, and also Rule #7. * From 6a89910b22d6bec6f10b2530c4cc2cdf78df5cac Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 9 Apr 2024 19:22:01 -0700 Subject: [PATCH 080/110] Add debugging information --- .../checker/nonempty/NonEmptyAnnotatedTypeFactory.java | 8 ++++++++ .../checker/nonempty/NonEmptyTransfer.java | 2 +- .../checker/optional/OptionalAnnotatedTypeFactory.java | 8 ++++++-- .../checker/optional/OptionalTransfer.java | 1 + 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 6d88eff76b9..a54beb246b9 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -2,6 +2,7 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.Tree; import java.util.List; import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.nonempty.qual.NonEmpty; @@ -34,6 +35,13 @@ protected TreeAnnotator createTreeAnnotator() { return new ListTreeAnnotator(super.createTreeAnnotator(), new NonEmptyTreeAnnotator(this)); } + protected boolean isAnnotatedWithNonEmpty(Tree tree) { + System.out.printf("NonEmptyAnnotatedTypeFactory::Checking if tree = [%s] is annotated with @NonEmpty\n", tree); + AnnotatedTypeMirror annotatedTypeMirror = this.getAnnotatedType(tree); + System.out.printf("Annotated Type Mirror for [%s] = %s\n", tree, annotatedTypeMirror); + return false; // stub + } + /** The tree annotator for the Non-Empty Checker. */ private class NonEmptyTreeAnnotator extends TreeAnnotator { diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java index 64f474fe3bb..6f358cae9dd 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java @@ -43,7 +43,7 @@ public class NonEmptyTransfer extends CFTransfer { private final ExecutableElement indexOf; /** A {@link NonEmptyAnnotatedTypeFactory} instance. */ - private final NonEmptyAnnotatedTypeFactory aTypeFactory; + protected final NonEmptyAnnotatedTypeFactory aTypeFactory; public NonEmptyTransfer(CFAnalysis analysis) { super(analysis); diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java index f8bc39b7cf5..a8716778062 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java @@ -10,8 +10,8 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.nonempty.NonEmptyAnnotatedTypeFactory; import org.checkerframework.checker.optional.qual.Present; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; @@ -22,7 +22,7 @@ import org.checkerframework.javacutil.TreeUtils; /** OptionalAnnotatedTypeFactory for the Optional Checker. */ -public class OptionalAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class OptionalAnnotatedTypeFactory extends NonEmptyAnnotatedTypeFactory { /** The element for java.util.Optional.map(). */ private final ExecutableElement optionalMap; @@ -131,4 +131,8 @@ public CFTransfer createFlowTransferFunction( CFAbstractAnalysis analysis) { return new OptionalTransfer(analysis); } + + public boolean isTreeAnnotatedWithNonEmpty(ExpressionTree tree) { + return super.isAnnotatedWithNonEmpty(tree); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 639ba46f4a1..b68c10c8db5 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -154,6 +154,7 @@ private void refineStreamOperations( if (relevantStreamMethods.stream() .anyMatch( op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { + optionalTypeFactory.isTreeAnnotatedWithNonEmpty(TreeUtils.getReceiverTree(n.getTree())); // TODO: refine result to @Present if the receiver is @NonEmpty assert result != null; // stub for debugging } From 5b2056eb084c5aedcc7fe02fd5d074cce618a40e Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 10 Apr 2024 09:54:06 -0700 Subject: [PATCH 081/110] Apply lint --- .../nonempty/NonEmptyAnnotatedTypeFactory.java | 4 +++- .../checker/optional/OptionalTransfer.java | 2 +- .../checker/optional/RevisedOptionalChecker.java | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index a54beb246b9..018a378c4e7 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -36,7 +36,9 @@ protected TreeAnnotator createTreeAnnotator() { } protected boolean isAnnotatedWithNonEmpty(Tree tree) { - System.out.printf("NonEmptyAnnotatedTypeFactory::Checking if tree = [%s] is annotated with @NonEmpty\n", tree); + System.out.printf( + "NonEmptyAnnotatedTypeFactory::Checking if tree = [%s] is annotated with @NonEmpty\n", + tree); AnnotatedTypeMirror annotatedTypeMirror = this.getAnnotatedType(tree); System.out.printf("Annotated Type Mirror for [%s] = %s\n", tree, annotatedTypeMirror); return false; // stub diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index b68c10c8db5..86f64cded0f 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -154,7 +154,7 @@ private void refineStreamOperations( if (relevantStreamMethods.stream() .anyMatch( op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { - optionalTypeFactory.isTreeAnnotatedWithNonEmpty(TreeUtils.getReceiverTree(n.getTree())); + optionalTypeFactory.isTreeAnnotatedWithNonEmpty(TreeUtils.getReceiverTree(n.getTree())); // TODO: refine result to @Present if the receiver is @NonEmpty assert result != null; // stub for debugging } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java b/checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java new file mode 100644 index 00000000000..b22e5a5497b --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java @@ -0,0 +1,15 @@ +package org.checkerframework.checker.optional; + +import java.util.Set; +import org.checkerframework.checker.nonempty.NonEmptyChecker; +import org.checkerframework.common.basetype.BaseTypeChecker; + +public class RevisedOptionalChecker extends BaseTypeChecker { + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + checkers.add(NonEmptyChecker.class); + return checkers; + } +} From 50bfa3635eb8f46cce91b23cb2d29925b9488c7d Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 1 May 2024 15:21:48 -0700 Subject: [PATCH 082/110] Reading @NonEmpty anno in Optional Transfer --- .../NonEmptyAnnotatedTypeFactory.java | 3 +- .../checker/optional/OptionalTransfer.java | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 018a378c4e7..a2728f96dcb 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -40,7 +40,8 @@ protected boolean isAnnotatedWithNonEmpty(Tree tree) { "NonEmptyAnnotatedTypeFactory::Checking if tree = [%s] is annotated with @NonEmpty\n", tree); AnnotatedTypeMirror annotatedTypeMirror = this.getAnnotatedType(tree); - System.out.printf("Annotated Type Mirror for [%s] = %s\n", tree, annotatedTypeMirror); + System.out.printf( + "Explicit Annotations for [%s] = %s\n", tree, annotatedTypeMirror.getExplicitAnnotations()); return false; // stub } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 86f64cded0f..6f0df7ba56d 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -1,7 +1,10 @@ package org.checkerframework.checker.optional; +import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; @@ -13,6 +16,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.Elements; +import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.Present; import org.checkerframework.dataflow.analysis.TransferInput; @@ -29,6 +33,8 @@ import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; /** The transfer function for the Optional Checker. */ @@ -154,10 +160,33 @@ private void refineStreamOperations( if (relevantStreamMethods.stream() .anyMatch( op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { - optionalTypeFactory.isTreeAnnotatedWithNonEmpty(TreeUtils.getReceiverTree(n.getTree())); - // TODO: refine result to @Present if the receiver is @NonEmpty - assert result != null; // stub for debugging + JavaExpression receiver = JavaExpression.getReceiver(TreeUtils.getReceiverTree(n.getTree())); + VariableTree receiverDeclaration = + getReceiverDeclaration(TreePathUtil.enclosingMethod(n.getTreePath()), receiver); + List receiverAnnotationTrees = + receiverDeclaration.getModifiers().getAnnotations(); + List annotationMirrors = + TreeUtils.annotationsFromTypeAnnotationTrees(receiverAnnotationTrees); + if (annotationMirrors.stream() + .anyMatch(am -> AnnotationUtils.areSameByName(am, NonEmpty.class.getCanonicalName()))) { + // TODO: the receiver of the stream operation is @Non-Empty, therefore the result is + // @Present + makePresent(result, n); + } + } + assert result != null; + } + + private @Nullable VariableTree getReceiverDeclaration(MethodTree tree, JavaExpression receiver) { + for (StatementTree statement : tree.getBody().getStatements()) { + if (statement instanceof VariableTree) { + VariableTree localVariableTree = (VariableTree) statement; + if (localVariableTree.getName().toString().equals(receiver.toString())) { + return localVariableTree; + } + } } + return null; } /** From b5adf0d974d15a26ecf66767fca12557b52065c0 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 1 May 2024 20:20:29 -0700 Subject: [PATCH 083/110] Add documentation and debug logs --- .../checker/optional/OptionalTransfer.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 6f0df7ba56d..79d94a0557c 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -137,6 +137,8 @@ public TransferResult visitMethodInvocation( return result; } refineStreamOperations(n, result); + System.out.printf( + "Visiting method invocation = %s, with store = %s\n", n, in.getRegularStore()); return result; } @@ -177,7 +179,23 @@ private void refineStreamOperations( assert result != null; } + /** + * Find the declaration of the receiver of a method call in a method tree. + * + *

The receiver should appear in one of two places, either as a formal parameter to the method, + * or as a local variable. + * + * @param tree the method tree + * @param receiver the receiver for which to look up a declaration + * @return the declaration of the receiver of the method call, if found. Otherwise, null + */ private @Nullable VariableTree getReceiverDeclaration(MethodTree tree, JavaExpression receiver) { + List params = tree.getParameters(); + for (VariableTree param : params) { + if (param.getName().toString().equals(receiver.toString())) { + return param; + } + } for (StatementTree statement : tree.getBody().getStatements()) { if (statement instanceof VariableTree) { VariableTree localVariableTree = (VariableTree) statement; @@ -195,7 +213,6 @@ private void refineStreamOperations( * @param result the transfer result to side effect * @param node the node to make {@code @Present} */ - @SuppressWarnings("UnusedMethod") private void makePresent(TransferResult result, Node node) { if (result.containsTwoStores()) { makePresent(result.getThenStore(), node); @@ -213,7 +230,9 @@ private void makePresent(TransferResult result, Node node) { */ private void makePresent(CFStore store, Node node) { JavaExpression internalRepr = JavaExpression.fromNode(node); + System.out.printf("Attempting to insert value into store = %s\n", internalRepr); store.insertValue(internalRepr, PRESENT); + System.out.printf("Store after insertion = %s\n", store); } /** From a0abd2fe1a63d0078d618fb2ea6679ca08521ed9 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 1 May 2024 20:39:56 -0700 Subject: [PATCH 084/110] Add additional logging --- .../checker/optional/OptionalTransfer.java | 2 +- .../framework/flow/CFAbstractStore.java | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 79d94a0557c..88600afefbf 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -231,7 +231,7 @@ private void makePresent(TransferResult result, Node node) { private void makePresent(CFStore store, Node node) { JavaExpression internalRepr = JavaExpression.fromNode(node); System.out.printf("Attempting to insert value into store = %s\n", internalRepr); - store.insertValue(internalRepr, PRESENT); + store.insertValuePermitNondeterministic(internalRepr, PRESENT); System.out.printf("Store after insertion = %s\n", store); } diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index 9e84494064e..73c3800ddd0 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -562,15 +562,20 @@ public final void insertValuePermitNondeterministic(JavaExpression expr, @Nullab protected boolean shouldInsert( JavaExpression expr, @Nullable V value, boolean permitNondeterministic) { if (value == null) { + System.out.printf("expr = %s, value = %s, value is null\n", expr, value); // No need to insert a null abstract value because it represents // top and top is also the default value. return false; } if (expr.containsUnknown()) { + System.out.printf("expr = %s, value = %s, expr.containsUnknown() is true\n", expr, value); // Expressions containing unknown expressions are not stored. return false; } if (!(permitNondeterministic || expr.isDeterministic(analysis.getTypeFactory()))) { + System.out.printf( + "expr = %s, value = %s, permitNonDeterministic = %s\n", + expr, value, permitNondeterministic); // Nondeterministic expressions may not be stored. // (They are likely to be quickly evicted, as soon as a side-effecting method is // called.) @@ -599,7 +604,13 @@ protected boolean shouldInsert( protected void insertValue( JavaExpression expr, @Nullable V value, boolean permitNondeterministic) { computeNewValueAndInsert( - expr, value, (old, newValue) -> newValue.mostSpecific(old, null), permitNondeterministic); + expr, + value, + (old, newValue) -> { + System.out.printf("Store replacement :: old value = %s, new value = %s\n", old, newValue); + return newValue.mostSpecific(old, null); + }, + permitNondeterministic); } /** @@ -618,6 +629,7 @@ protected void computeNewValueAndInsert( BinaryOperator merger, boolean permitNondeterministic) { if (!shouldInsert(expr, value, permitNondeterministic)) { + System.out.printf("NOT EVEN BOTHERING TO INSERT\n"); return; } From 05b15d0afc2fb78f4f255f65910886a4de5859dc Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 2 May 2024 14:09:12 -0700 Subject: [PATCH 085/110] Remove unnecessary helper methods --- .../checker/optional/OptionalTransfer.java | 46 +------------------ 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 88600afefbf..2369fb0f848 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -25,7 +25,6 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; @@ -173,7 +172,8 @@ private void refineStreamOperations( .anyMatch(am -> AnnotationUtils.areSameByName(am, NonEmpty.class.getCanonicalName()))) { // TODO: the receiver of the stream operation is @Non-Empty, therefore the result is // @Present - makePresent(result, n); + JavaExpression internalRepr = JavaExpression.fromNode(n); + insertIntoStores(result, internalRepr, PRESENT); } } assert result != null; @@ -206,46 +206,4 @@ private void refineStreamOperations( } return null; } - - /** - * Sets {@code node} to {@code @Present} in the given {@link TransferResult}. - * - * @param result the transfer result to side effect - * @param node the node to make {@code @Present} - */ - private void makePresent(TransferResult result, Node node) { - if (result.containsTwoStores()) { - makePresent(result.getThenStore(), node); - makePresent(result.getElseStore(), node); - } else { - makePresent(result.getRegularStore(), node); - } - } - - /** - * Sets a given {@link Node} to {@code @Present} in the given {@code store}. - * - * @param store the store to update - * @param node the node that should be absent (non-present) - */ - private void makePresent(CFStore store, Node node) { - JavaExpression internalRepr = JavaExpression.fromNode(node); - System.out.printf("Attempting to insert value into store = %s\n", internalRepr); - store.insertValuePermitNondeterministic(internalRepr, PRESENT); - System.out.printf("Store after insertion = %s\n", store); - } - - /** - * Refine the given result to {@code @Present}. - * - * @param result the result to refine to {@code @Present}. - */ - @SuppressWarnings("UnusedMethod") - private void refineToPresent(TransferResult result) { - CFValue oldResultValue = result.getResultValue(); - CFValue refinedResultValue = - analysis.createSingleAnnotationValue(PRESENT, oldResultValue.getUnderlyingType()); - CFValue newResultValue = refinedResultValue.mostSpecific(oldResultValue, null); - result.setResultValue(newResultValue); - } } From 21e21f2ade6075a61d13634d657a3780020cdc03 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 9 May 2024 10:39:22 -0700 Subject: [PATCH 086/110] Refactoring check for non-empty method invocation receiver --- .../checker/optional/OptionalTransfer.java | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 2369fb0f848..d31132e585f 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -42,6 +42,9 @@ public class OptionalTransfer extends CFTransfer { /** The @{@link Present} annotation. */ private final AnnotationMirror PRESENT; + /** The @{@link NonEmpty} annotation. */ + private final AnnotationMirror NON_EMPTY; + /** The element for java.util.Optional.ifPresent(). */ private final ExecutableElement optionalIfPresent; @@ -76,6 +79,7 @@ public OptionalTransfer(CFAbstractAnalysis analysi optionalTypeFactory = (OptionalAnnotatedTypeFactory) analysis.getTypeFactory(); Elements elements = optionalTypeFactory.getElementUtils(); PRESENT = AnnotationBuilder.fromClass(elements, Present.class); + NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); ProcessingEnvironment env = optionalTypeFactory.getProcessingEnv(); optionalIfPresent = TreeUtils.getMethod("java.util.Optional", "ifPresent", 1, env); optionalIfPresentOrElse = @@ -161,22 +165,36 @@ private void refineStreamOperations( if (relevantStreamMethods.stream() .anyMatch( op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { - JavaExpression receiver = JavaExpression.getReceiver(TreeUtils.getReceiverTree(n.getTree())); - VariableTree receiverDeclaration = - getReceiverDeclaration(TreePathUtil.enclosingMethod(n.getTreePath()), receiver); - List receiverAnnotationTrees = - receiverDeclaration.getModifiers().getAnnotations(); - List annotationMirrors = - TreeUtils.annotationsFromTypeAnnotationTrees(receiverAnnotationTrees); - if (annotationMirrors.stream() - .anyMatch(am -> AnnotationUtils.areSameByName(am, NonEmpty.class.getCanonicalName()))) { + if (isReceiverNonEmpty(n)) { // TODO: the receiver of the stream operation is @Non-Empty, therefore the result is // @Present JavaExpression internalRepr = JavaExpression.fromNode(n); + System.out.printf("Non-empty detected for = %s\n", internalRepr); insertIntoStores(result, internalRepr, PRESENT); } } - assert result != null; + } + + /** + * Returns true if the receiver of the given method invocation is annotated with @{@link + * NonEmpty}. + * + * @param methodInvok a method invocation node + * @return true if the receiver of the given method invocation is annotated with @{@link NonEmpty} + */ + private boolean isReceiverNonEmpty(MethodInvocationNode methodInvok) { + JavaExpression receiver = + JavaExpression.getReceiver(TreeUtils.getReceiverTree(methodInvok.getTree())); + VariableTree receiverDeclaration = + getReceiverDeclaration(TreePathUtil.enclosingMethod(methodInvok.getTreePath()), receiver); + if (receiverDeclaration == null) { + return false; + } + List receiverAnnotationTrees = + receiverDeclaration.getModifiers().getAnnotations(); + List annotationMirrors = + TreeUtils.annotationsFromTypeAnnotationTrees(receiverAnnotationTrees); + return AnnotationUtils.containsSame(annotationMirrors, NON_EMPTY); } /** @@ -189,7 +207,11 @@ private void refineStreamOperations( * @param receiver the receiver for which to look up a declaration * @return the declaration of the receiver of the method call, if found. Otherwise, null */ - private @Nullable VariableTree getReceiverDeclaration(MethodTree tree, JavaExpression receiver) { + private @Nullable VariableTree getReceiverDeclaration( + @Nullable MethodTree tree, JavaExpression receiver) { + if (tree == null) { + return null; + } List params = tree.getParameters(); for (VariableTree param : params) { if (param.getName().toString().equals(receiver.toString())) { From 38c5402540aa21284860323a12b8b19766cb7327 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 13 May 2024 09:26:36 -0700 Subject: [PATCH 087/110] Get initial receiver of method invocation --- .../checker/optional/OptionalTransfer.java | 3 ++- .../dataflow/expression/JavaExpression.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index d31132e585f..ca78afdad0c 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -184,7 +184,8 @@ private void refineStreamOperations( */ private boolean isReceiverNonEmpty(MethodInvocationNode methodInvok) { JavaExpression receiver = - JavaExpression.getReceiver(TreeUtils.getReceiverTree(methodInvok.getTree())); + JavaExpression.getInitialReceiverOfMethodInvocation( + TreeUtils.getReceiverTree(methodInvok.getTree())); VariableTree receiverDeclaration = getReceiverDeclaration(TreePathUtil.enclosingMethod(methodInvok.getTreePath()), receiver); if (receiverDeclaration == null) { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java index 73f351f52ca..0da37bf6f95 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -734,6 +734,24 @@ public static JavaExpression getReceiver(ExpressionTree accessTree) { } } + /** + * Returns the initial receiver of a method invocation. + * + *

For example, for a given method invocation sequence {@code a.b().c.d().e()}, return {@code + * a}. + * + * @param tree a tree + * @return the initial receiver of a method invocation + */ + public static JavaExpression getInitialReceiverOfMethodInvocation(ExpressionTree tree) { + assert tree instanceof MethodInvocationTree; + ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree); + while (receiverTree instanceof MethodInvocationNode) { + receiverTree = TreeUtils.getReceiverTree(receiverTree); + } + return getReceiver(receiverTree); + } + /** * Returns the implicit receiver of ele. * From fb2bb560992b39be7a71471b60a4e32b78476471 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 13 May 2024 10:02:30 -0700 Subject: [PATCH 088/110] Enable insertion of non-deterministic expressions into the store --- .../checker/optional/OptionalTransfer.java | 16 +++++++++++++++- .../framework/flow/CFAbstractTransfer.java | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index ca78afdad0c..f84ac79e5d0 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -19,6 +19,7 @@ import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.Present; +import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.UnderlyingAST; @@ -170,11 +171,24 @@ private void refineStreamOperations( // @Present JavaExpression internalRepr = JavaExpression.fromNode(n); System.out.printf("Non-empty detected for = %s\n", internalRepr); - insertIntoStores(result, internalRepr, PRESENT); + if (isAssumePureOrAssumeDeterministicEnabled()) { + insertIntoStoresPermitNonDeterministic(result, internalRepr, PRESENT); + } else { + insertIntoStores(result, internalRepr, PRESENT); + } } } } + /** + * Determine whether this analysis is being executed with the {@literal -AassumePure or {@literal -AassumeDeterministic} flags. + * @return true if the {@literal -AassumePure} or {@literal -AassumeDeterministic} flags are passed to this analysis + */ + private boolean isAssumePureOrAssumeDeterministicEnabled() { + BaseTypeChecker checker = analysis.getTypeFactory().getChecker(); + return checker.hasOption("assumePure") || checker.hasOption("assumeDeterministic"); + } + /** * Returns true if the receiver of the given method invocation is annotated with @{@link * NonEmpty}. 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 20ea7417a65..cb717bff1ca 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java @@ -1356,4 +1356,22 @@ protected static void insertIntoStores( result.getRegularStore().insertValue(target, newAnno); } } + + /** + * Inserts {@code newAnno} into all stores (conditional or not) in the result for node, while + * permitting non-determinism. This is a utility method for subclasses. + * + * @param result the {@link TransferResult} holding the stores to modify + * @param target the receiver whose values should be modified + * @param newAnno the new value + */ + protected static void insertIntoStoresPermitNonDeterministic( + TransferResult result, JavaExpression target, AnnotationMirror newAnno) { + if (result.containsTwoStores()) { + result.getThenStore().insertValuePermitNondeterministic(target, newAnno); + result.getElseStore().insertValuePermitNondeterministic(target, newAnno); + } else { + result.getRegularStore().insertValuePermitNondeterministic(target, newAnno); + } + } } From 7d695dafc9698573cf73822920073b34290a64d8 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 13 May 2024 11:07:27 -0700 Subject: [PATCH 089/110] Clean up logging --- .../checker/optional/OptionalTransfer.java | 2 -- .../checkerframework/framework/flow/CFAbstractStore.java | 7 ------- 2 files changed, 9 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index f84ac79e5d0..d1e1b0604fa 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -141,8 +141,6 @@ public TransferResult visitMethodInvocation( return result; } refineStreamOperations(n, result); - System.out.printf( - "Visiting method invocation = %s, with store = %s\n", n, in.getRegularStore()); return result; } diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index a4d56e03ecf..edb747ef98f 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -563,20 +563,15 @@ public final void insertValuePermitNondeterministic(JavaExpression expr, @Nullab protected boolean shouldInsert( JavaExpression expr, @Nullable V value, boolean permitNondeterministic) { if (value == null) { - System.out.printf("expr = %s, value = %s, value is null\n", expr, value); // No need to insert a null abstract value because it represents // top and top is also the default value. return false; } if (expr.containsUnknown()) { - System.out.printf("expr = %s, value = %s, expr.containsUnknown() is true\n", expr, value); // Expressions containing unknown expressions are not stored. return false; } if (!(permitNondeterministic || expr.isDeterministic(analysis.getTypeFactory()))) { - System.out.printf( - "expr = %s, value = %s, permitNonDeterministic = %s\n", - expr, value, permitNondeterministic); // Nondeterministic expressions may not be stored. // (They are likely to be quickly evicted, as soon as a side-effecting method is // called.) @@ -608,7 +603,6 @@ protected void insertValue( expr, value, (old, newValue) -> { - System.out.printf("Store replacement :: old value = %s, new value = %s\n", old, newValue); return newValue.mostSpecific(old, null); }, permitNondeterministic); @@ -630,7 +624,6 @@ protected void computeNewValueAndInsert( BinaryOperator merger, boolean permitNondeterministic) { if (!shouldInsert(expr, value, permitNondeterministic)) { - System.out.printf("NOT EVEN BOTHERING TO INSERT\n"); return; } From 75fd8fa1872a480234c19c41b8e45883b5cfcfb1 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 13 May 2024 13:18:46 -0700 Subject: [PATCH 090/110] Start implementing method-level support for `skipDefs` flag --- .../common/basetype/BaseTypeVisitor.java | 4 ++++ .../framework/source/SourceChecker.java | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) 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 b077f228525..bee5c0578df 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -959,6 +959,10 @@ protected void checkDefaultConstructor(ClassTree tree) {} */ @Override public Void visitMethod(MethodTree tree, Void p) { + ClassTree enclosingClass = TreePathUtil.enclosingClass(getCurrentPath()); + if (checker.shouldSkipDefs(enclosingClass, tree)) { + return null; + } // We copy the result from getAnnotatedType to ensure that circular types (e.g. K extends // Comparable) are represented by circular AnnotatedTypeMirrors, which avoids problems // with later checks. diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index a6021c601e1..1eb87b1491e 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -123,6 +123,8 @@ "onlyUses", "skipDefs", "onlyDefs", + "skipMethods", + "onlyMethods", "skipFiles", "onlyFiles", "skipDirs", // Obsolete as of 2024-03-15, replaced by "skipFiles". @@ -2631,6 +2633,17 @@ public final boolean shouldSkipDefs(ClassTree tree) { || !onlyDefsPattern.matcher(qualifiedName).find(); } + /** + * Tests whether the method definition should not be checked because it matches the {@code + * checker.skipDefs} property. + * + * @param tree method to potentially skip + * @return true if checker should not test {@code tree} + */ + public final boolean shouldSkipDefs(MethodTree tree) { + return false; // stub + } + /** * Tests whether the method definition should not be checked because it matches the {@code * checker.skipDefs} property. @@ -2642,7 +2655,7 @@ public final boolean shouldSkipDefs(ClassTree tree) { * @return true if checker should not test {@code meth} */ public final boolean shouldSkipDefs(ClassTree cls, MethodTree meth) { - return shouldSkipDefs(cls); + return shouldSkipDefs(cls) || shouldSkipDefs(meth); } /////////////////////////////////////////////////////////////////////////// From a8248cccda78a9b591a3e3cd47278174aaa4fbb7 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 13 May 2024 17:58:18 -0700 Subject: [PATCH 091/110] Correct logic for `JavaExpression.getInitialReceiverOfMethodInvocation` --- .../checkerframework/dataflow/expression/JavaExpression.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java index 0da37bf6f95..91ce61ce720 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -749,7 +749,7 @@ public static JavaExpression getInitialReceiverOfMethodInvocation(ExpressionTree while (receiverTree instanceof MethodInvocationNode) { receiverTree = TreeUtils.getReceiverTree(receiverTree); } - return getReceiver(receiverTree); + return JavaExpression.fromTree(receiverTree); } /** From 988729154b07bad83f650be44790ef72e1078315 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Mon, 13 May 2024 19:00:31 -0700 Subject: [PATCH 092/110] Add debug logs --- .../checkerframework/checker/optional/OptionalTransfer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index d1e1b0604fa..72bc2e4132d 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -136,11 +136,15 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List @Override public TransferResult visitMethodInvocation( MethodInvocationNode n, TransferInput in) { - TransferResult result = super.visitMethodInvocation(n, in); + TransferResult result = + super.visitMethodInvocation( + n, in); // Is the error being emitted because this happens first? Before the store is + // updated? if (n.getTree() == null) { return result; } refineStreamOperations(n, result); + System.out.printf("Store after Stream operation refinement = %s\n", result); return result; } From 8e91f0da2a609883635fb24d2dbc835fb0768528 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Tue, 14 May 2024 15:32:40 -0700 Subject: [PATCH 093/110] Enable `NonEmptyChecker` to access methods to check from the `OptionalChecker` --- .../checker/nonempty/NonEmptyChecker.java | 29 +++++++++++++++++++ .../checker/optional/OptionalVisitor.java | 16 +++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java index b703aa2267a..abab3a51ace 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java @@ -1,7 +1,13 @@ package org.checkerframework.checker.nonempty; +import com.sun.source.tree.MethodTree; +import java.util.HashMap; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.checkerframework.checker.optional.OptionalChecker; +import org.checkerframework.checker.optional.OptionalVisitor; +import org.checkerframework.checker.regex.qual.Regex; import org.checkerframework.common.basetype.BaseTypeChecker; /** @@ -18,4 +24,27 @@ protected Set> getImmediateSubcheckerClasses() checkers.add(OptionalChecker.class); return checkers; } + + @Override + public Map getOptions() { + Map options = new HashMap<>(super.getOptions()); + OptionalChecker optionalChecker = this.getSubchecker(OptionalChecker.class); + if (optionalChecker != null && optionalChecker.getVisitor() instanceof OptionalVisitor) { + OptionalVisitor optionalVisitor = (OptionalVisitor) optionalChecker.getVisitor(); + Set methodsToCheck = optionalVisitor.getMethodsForNonEmptyChecker(); + String namesOfMethodsToCheck = getNamesOfMethodsToCheck(methodsToCheck); + options.put("onlyDefs", namesOfMethodsToCheck); + } + return options; + } + + /** + * Create a regex that matches the names of all methods in the given set of methods. + * + * @param methodsToCheck the set of methods that should be checked by the Non-Empty Checker + * @return a regex that matches the names of all methods in the given set of methods + */ + private @Regex String getNamesOfMethodsToCheck(Set methodsToCheck) { + return methodsToCheck.stream().map(MethodTree::getName).collect(Collectors.joining("|")); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java index 5fe0984e595..0cd7c98e9dc 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java @@ -21,7 +21,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; @@ -41,6 +40,7 @@ import org.checkerframework.common.basetype.BaseTypeValidator; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; @@ -104,6 +104,20 @@ protected BaseTypeValidator createTypeValidator() { return new OptionalTypeValidator(checker, this, atypeFactory); } + /** + * Gets the set of methods that should be verified using the {@link + * org.checkerframework.checker.nonempty.NonEmptyChecker}. + * + *

This should only really be called by the Non-Empty Checker. + * + * @return the set of methods that should be verified using the {@link + * org.checkerframework.checker.nonempty.NonEmptyChecker} + */ + @Pure + public Set getMethodsForNonEmptyChecker() { + return methodsForNonEmptyChecker; + } + /** * Returns true iff {@code expression} is a call to java.util.Optional.get. * From e7906b04a3c5d02063007f73e446bef8a6203680 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 15 May 2024 10:13:55 -0700 Subject: [PATCH 094/110] Stub implementation of `SourceChecker.shouldSkipDefs` and make it non-`final` --- .../org/checkerframework/framework/source/SourceChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 1eb87b1491e..e73b1cb64af 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2640,7 +2640,7 @@ public final boolean shouldSkipDefs(ClassTree tree) { * @param tree method to potentially skip * @return true if checker should not test {@code tree} */ - public final boolean shouldSkipDefs(MethodTree tree) { + public boolean shouldSkipDefs(MethodTree tree) { return false; // stub } From 14c7977d5d596964248390182bf28305a8d84a95 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 15 May 2024 10:15:00 -0700 Subject: [PATCH 095/110] Remove unnecessary flags --- .../org/checkerframework/framework/source/SourceChecker.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index e73b1cb64af..984822baac5 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -123,8 +123,6 @@ "onlyUses", "skipDefs", "onlyDefs", - "skipMethods", - "onlyMethods", "skipFiles", "onlyFiles", "skipDirs", // Obsolete as of 2024-03-15, replaced by "skipFiles". From 6d6515772302aead9943cc468a6e985abab23ed1 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 15 May 2024 11:20:02 -0700 Subject: [PATCH 096/110] Simplify logic for accessing + checking methods to check in the Non-Empty Checker --- .../checker/nonempty/NonEmptyChecker.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java index abab3a51ace..81e8ddd69e4 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java @@ -1,13 +1,10 @@ package org.checkerframework.checker.nonempty; import com.sun.source.tree.MethodTree; -import java.util.HashMap; -import java.util.Map; +import java.util.Collections; import java.util.Set; -import java.util.stream.Collectors; import org.checkerframework.checker.optional.OptionalChecker; import org.checkerframework.checker.optional.OptionalVisitor; -import org.checkerframework.checker.regex.qual.Regex; import org.checkerframework.common.basetype.BaseTypeChecker; /** @@ -26,25 +23,27 @@ protected Set> getImmediateSubcheckerClasses() } @Override - public Map getOptions() { - Map options = new HashMap<>(super.getOptions()); - OptionalChecker optionalChecker = this.getSubchecker(OptionalChecker.class); - if (optionalChecker != null && optionalChecker.getVisitor() instanceof OptionalVisitor) { - OptionalVisitor optionalVisitor = (OptionalVisitor) optionalChecker.getVisitor(); - Set methodsToCheck = optionalVisitor.getMethodsForNonEmptyChecker(); - String namesOfMethodsToCheck = getNamesOfMethodsToCheck(methodsToCheck); - options.put("onlyDefs", namesOfMethodsToCheck); - } - return options; + public boolean shouldSkipDefs(MethodTree tree) { + return !getMethodsToCheck().contains(tree); } /** - * Create a regex that matches the names of all methods in the given set of methods. + * Obtains the methods to check w.r.t. the Non-Empty type system from the Optional Checker. + * + *

The Optional Checker uses explicitly-written (i.e., programmer-written) annotations from the + * Non-Empty type system to refine its analysis with respect to operations on containers (e.g., + * Streams, Collections) that result in values of type Optional. * - * @param methodsToCheck the set of methods that should be checked by the Non-Empty Checker - * @return a regex that matches the names of all methods in the given set of methods + *

This method provides access to the Non-Empty Checker for methods that should be verified + * + * @return a set of methods to be checked by the Non-Empty Checker */ - private @Regex String getNamesOfMethodsToCheck(Set methodsToCheck) { - return methodsToCheck.stream().map(MethodTree::getName).collect(Collectors.joining("|")); + private Set getMethodsToCheck() { + OptionalChecker optionalChecker = getSubchecker(OptionalChecker.class); + if (optionalChecker != null && optionalChecker.getVisitor() instanceof OptionalVisitor) { + OptionalVisitor optionalVisitor = (OptionalVisitor) optionalChecker.getVisitor(); + return optionalVisitor.getMethodsForNonEmptyChecker(); + } + return Collections.emptySet(); } } From 89d38617eb82118e80f5609acfda57d9debab7a6 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Wed, 15 May 2024 13:42:00 -0700 Subject: [PATCH 097/110] Update algorithm to obtain methods to check with Non-Empty Checker --- .../checker/optional/OptionalVisitor.java | 86 +++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java index 0cd7c98e9dc..0cb036ad21d 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java @@ -18,12 +18,16 @@ import com.sun.source.util.TreePath; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; @@ -80,6 +84,9 @@ public class OptionalVisitor /** Set of methods to be checked by the Non-Empty Checker. */ private final Set methodsForNonEmptyChecker; + /** Map of the names of methods to the methods in which they are invoked. */ + private final Map> methodNamesToEnclosingMethods; + /** * Create an OptionalVisitor. * @@ -97,6 +104,7 @@ public OptionalVisitor(BaseTypeChecker checker) { streamFilter = TreeUtils.getMethod("java.util.stream.Stream", "filter", 1, env); streamMap = TreeUtils.getMethod("java.util.stream.Stream", "map", 1, env); methodsForNonEmptyChecker = new HashSet<>(); + methodNamesToEnclosingMethods = new HashMap<>(); } @Override @@ -352,25 +360,90 @@ public void handleConditionalStatementIsPresentGet(IfTree tree) { public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { handleCreationElimination(tree); handleNestedOptionalCreation(tree); + updateMethodNamesToEnclosingMethods(tree); return super.visitMethodInvocation(tree, p); } + /** + * Updates {@link methodNamesToEnclosingMethods} given a method invocation. + * + *

Check whether the method is in the set of methods that must be checked by the Non-Empty + * checker whenever a method invocation is encountered. If the method is in the set, the method + * that immediately encloses the method invocation should also be added to the set of methods to + * be checked by the Non-Empty Checker. + * + *

This ensures that the clients of any methods that must be checked by the Non-Empty + * Checker (i.e., methods that have preconditions related to the Non-Empty type system) are + * included in the set of methods to check. + * + * @param tree a method invocation tree + */ + private void updateMethodNamesToEnclosingMethods(MethodInvocationTree tree) { + String invokedMethodName = tree.getMethodSelect().toString(); + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(this.getCurrentPath()); + Set namesOfMethodsForNonEmptyChecker = + methodsForNonEmptyChecker.stream() + .map(MethodTree::getName) + .map(Name::toString) + .collect(Collectors.toSet()); + if (namesOfMethodsForNonEmptyChecker.contains(invokedMethodName)) { + methodNamesToEnclosingMethods.get(invokedMethodName).add(enclosingMethod); + } else { + Set enclosingMethodsForInvokedMethod = new HashSet<>(); + enclosingMethodsForInvokedMethod.add(enclosingMethod); + methodNamesToEnclosingMethods.put(invokedMethodName, enclosingMethodsForInvokedMethod); + } + } + @Override public Void visitMethod(MethodTree tree, Void p) { if (this.isAnnotatedWithNonEmptyPrecondition(tree) - || this.isAnyFormalAnnotatedWithNonEmpty(tree) - || this.isReturnTypeAnnotatedWithNonEmpty(tree)) { + || this.isAnyFormalAnnotatedWithNonEmpty(tree)) { + updateMethodToCheckWithNonEmptyCheckerGivenPreconditions(tree); + } + if (this.isReturnTypeAnnotatedWithNonEmpty(tree)) { methodsForNonEmptyChecker.add(tree); } return super.visitMethod(tree, p); } + /** + * Updates {@link methodsForNonEmptyChecker} when a method with a precondition from the Non-Empty + * type system (e.g., {@link RequiresNonEmpty}) or a formal annotated with {@link NonEmpty} is + * visited. + * + *

If the method being visited is in {@link methodNamesToEnclosingMethods}, the methods to + * check with the Non-Empty Checker should be updated with all the methods that dispatch calls to + * this method. + * + * @param tree a method tree + */ + private void updateMethodToCheckWithNonEmptyCheckerGivenPreconditions(MethodTree tree) { + String methodName = tree.getName().toString(); + if (methodNamesToEnclosingMethods.containsKey(methodName)) { + methodsForNonEmptyChecker.addAll(methodNamesToEnclosingMethods.get(methodName)); + } + methodsForNonEmptyChecker.add(tree); + } + + /** + * Returns true if a method is explicitly annotated with {@link RequiresNonEmpty}. + * + * @param tree a method tree + * @return true if a method is explicitly annotated with {@link RequiresNonEmpty} + */ private boolean isAnnotatedWithNonEmptyPrecondition(MethodTree tree) { return TreeUtils.annotationsFromTypeAnnotationTrees(tree.getModifiers().getAnnotations()) .stream() .anyMatch(am -> atypeFactory.areSameByClass(am, RequiresNonEmpty.class)); } + /** + * Returns true if any formal parameter of a method is explicitly annotated with {@link NonEmpty}. + * + * @param tree a method tree + * @return true if any formal parameter of a method is explicitly annotated with {@link NonEmpty} + */ private boolean isAnyFormalAnnotatedWithNonEmpty(MethodTree tree) { List params = tree.getParameters(); AnnotationMirrorSet annotationMirrors = new AnnotationMirrorSet(); @@ -382,6 +455,12 @@ private boolean isAnyFormalAnnotatedWithNonEmpty(MethodTree tree) { .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); } + /** + * Returns true if the return type of a method is explicitly annotated with {@link NonEmpty}. + * + * @param tree a method tree + * @return true if the return type of a method is explicitly annotated with {@link NonEmpty} + */ private boolean isReturnTypeAnnotatedWithNonEmpty(MethodTree tree) { if (tree.getReturnType() == null) { return false; @@ -557,9 +636,6 @@ private void handleNonEmptyVariableDeclaration(VariableTree tree) { .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); if (isAnnotatedWithNonEmpty) { methodsForNonEmptyChecker.add(TreePathUtil.enclosingMethod(this.getCurrentPath())); - System.out.printf( - "Methods to check with the Non-Empty Checker = %s\n", - methodsForNonEmptyChecker.stream().map(MethodTree::getName).collect(Collectors.toSet())); } } From 9a67aabeb6b28c179ae239a3d2bf49703905af06 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Thu, 16 May 2024 15:17:38 -0700 Subject: [PATCH 098/110] Override `processMethod` --- .../checker/nonempty/DelegationChecker.java | 11 +++++------ .../checker/optional/OptionalVisitor.java | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java index 6c41c7e0cac..07a2ee17360 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java @@ -65,26 +65,25 @@ public void processClassTree(ClassTree tree) { } @Override - public Void visitMethod(MethodTree tree, Void p) { - Void result = super.visitMethod(tree, p); + public void processMethodTree(MethodTree tree) { + super.processMethodTree(tree); if (delegate == null || !isMarkedWithOverride(tree)) { - return result; + return; } MethodInvocationTree candidateDelegateCall = getLastExpression(tree.getBody()); boolean hasExceptionalExit = hasExceptionalExit(tree.getBody(), UnsupportedOperationException.class); if (hasExceptionalExit) { - return result; + return; } if (candidateDelegateCall == null) { checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); - return result; + return; } Name enclosingMethodName = tree.getName(); if (!isValidDelegateCall(enclosingMethodName, candidateDelegateCall)) { checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); } - return result; } /** diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java index 0cb036ad21d..99ecdee754c 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java @@ -396,7 +396,7 @@ private void updateMethodNamesToEnclosingMethods(MethodInvocationTree tree) { } @Override - public Void visitMethod(MethodTree tree, Void p) { + public void processMethodTree(MethodTree tree) { if (this.isAnnotatedWithNonEmptyPrecondition(tree) || this.isAnyFormalAnnotatedWithNonEmpty(tree)) { updateMethodToCheckWithNonEmptyCheckerGivenPreconditions(tree); @@ -404,7 +404,7 @@ public Void visitMethod(MethodTree tree, Void p) { if (this.isReturnTypeAnnotatedWithNonEmpty(tree)) { methodsForNonEmptyChecker.add(tree); } - return super.visitMethod(tree, p); + super.processMethodTree(tree); } /** From 46a2725beec4683539415e943504c9147b967075 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 17 May 2024 13:35:48 -0700 Subject: [PATCH 099/110] Account for case when `TreeUtils.getReceiverTree` returns the receiver, not a tree --- .../checker/optional/OptionalTransfer.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 72bc2e4132d..cab6fd2d6c6 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.optional; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; @@ -199,9 +200,13 @@ private boolean isAssumePureOrAssumeDeterministicEnabled() { * @return true if the receiver of the given method invocation is annotated with @{@link NonEmpty} */ private boolean isReceiverNonEmpty(MethodInvocationNode methodInvok) { - JavaExpression receiver = - JavaExpression.getInitialReceiverOfMethodInvocation( - TreeUtils.getReceiverTree(methodInvok.getTree())); + ExpressionTree receiverTree = TreeUtils.getReceiverTree(methodInvok.getTree()); + JavaExpression receiver; + if (receiverTree instanceof MethodInvocationTree) { + receiver = JavaExpression.getInitialReceiverOfMethodInvocation(receiverTree); + } else { + receiver = JavaExpression.fromTree(receiverTree); + } VariableTree receiverDeclaration = getReceiverDeclaration(TreePathUtil.enclosingMethod(methodInvok.getTreePath()), receiver); if (receiverDeclaration == null) { From 572584d333b195579920d892bc9ec7c4fc92b2ca Mon Sep 17 00:00:00 2001 From: James Yoo Date: Fri, 17 May 2024 20:51:03 -0700 Subject: [PATCH 100/110] Handle receivers of stream operations that are fields, not locals or formals --- .../checker/optional/OptionalTransfer.java | 47 ++++++-------- .../checker/optional/OptionalVisitor.java | 29 +++++---- .../dataflow/expression/JavaExpression.java | 61 +++++++++++++++++++ 3 files changed, 98 insertions(+), 39 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index cab6fd2d6c6..4921d8c1883 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -1,11 +1,11 @@ package org.checkerframework.checker.optional; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; -import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; @@ -207,8 +207,7 @@ private boolean isReceiverNonEmpty(MethodInvocationNode methodInvok) { } else { receiver = JavaExpression.fromTree(receiverTree); } - VariableTree receiverDeclaration = - getReceiverDeclaration(TreePathUtil.enclosingMethod(methodInvok.getTreePath()), receiver); + VariableTree receiverDeclaration = getReceiverDeclaration(methodInvok, receiver); if (receiverDeclaration == null) { return false; } @@ -220,34 +219,28 @@ private boolean isReceiverNonEmpty(MethodInvocationNode methodInvok) { } /** - * Find the declaration of the receiver of a method call in a method tree. + * Returns the declaration of the initial receiver of the given method invocation node. * - *

The receiver should appear in one of two places, either as a formal parameter to the method, - * or as a local variable. + *

An attempt is first made to find the declaration of the receiver in the method that + * immediately encloses the given method invocation node. If this is unsuccessful, an attempt is + * made to look for the receiver in the fields of the class that immediately encloses the given + * method invocation node. * - * @param tree the method tree - * @param receiver the receiver for which to look up a declaration - * @return the declaration of the receiver of the method call, if found. Otherwise, null + * @param methodInvok a method invocation node + * @param initialReceiver the initial receiver in the method invocation node + * @return the declaration of the receiver if found, else null */ private @Nullable VariableTree getReceiverDeclaration( - @Nullable MethodTree tree, JavaExpression receiver) { - if (tree == null) { - return null; - } - List params = tree.getParameters(); - for (VariableTree param : params) { - if (param.getName().toString().equals(receiver.toString())) { - return param; - } - } - for (StatementTree statement : tree.getBody().getStatements()) { - if (statement instanceof VariableTree) { - VariableTree localVariableTree = (VariableTree) statement; - if (localVariableTree.getName().toString().equals(receiver.toString())) { - return localVariableTree; - } - } + MethodInvocationNode methodInvok, JavaExpression initialReceiver) { + // Look in the method, first + MethodTree methodTree = TreePathUtil.enclosingMethod(methodInvok.getTreePath()); + VariableTree declarationInMethod = + JavaExpression.getReceiverDeclarationInMethod(methodTree, initialReceiver); + if (declarationInMethod != null) { + return declarationInMethod; } - return null; + // If the declaration can't be found in the method, look in the class + ClassTree classTree = TreePathUtil.enclosingClass(methodInvok.getTreePath()); + return JavaExpression.getReceiverDeclarationInClass(classTree, initialReceiver); } } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java index 99ecdee754c..d800dd5c5b0 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java @@ -381,17 +381,19 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { private void updateMethodNamesToEnclosingMethods(MethodInvocationTree tree) { String invokedMethodName = tree.getMethodSelect().toString(); MethodTree enclosingMethod = TreePathUtil.enclosingMethod(this.getCurrentPath()); - Set namesOfMethodsForNonEmptyChecker = - methodsForNonEmptyChecker.stream() - .map(MethodTree::getName) - .map(Name::toString) - .collect(Collectors.toSet()); - if (namesOfMethodsForNonEmptyChecker.contains(invokedMethodName)) { - methodNamesToEnclosingMethods.get(invokedMethodName).add(enclosingMethod); - } else { - Set enclosingMethodsForInvokedMethod = new HashSet<>(); - enclosingMethodsForInvokedMethod.add(enclosingMethod); - methodNamesToEnclosingMethods.put(invokedMethodName, enclosingMethodsForInvokedMethod); + if (enclosingMethod != null) { + Set namesOfMethodsForNonEmptyChecker = + methodsForNonEmptyChecker.stream() + .map(MethodTree::getName) + .map(Name::toString) + .collect(Collectors.toSet()); + if (namesOfMethodsForNonEmptyChecker.contains(invokedMethodName)) { + methodNamesToEnclosingMethods.get(invokedMethodName).add(enclosingMethod); + } else { + Set enclosingMethodsForInvokedMethod = new HashSet<>(); + enclosingMethodsForInvokedMethod.add(enclosingMethod); + methodNamesToEnclosingMethods.put(invokedMethodName, enclosingMethodsForInvokedMethod); + } } } @@ -635,7 +637,10 @@ private void handleNonEmptyVariableDeclaration(VariableTree tree) { TreeUtils.annotationsFromTypeAnnotationTrees(tree.getModifiers().getAnnotations()).stream() .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); if (isAnnotatedWithNonEmpty) { - methodsForNonEmptyChecker.add(TreePathUtil.enclosingMethod(this.getCurrentPath())); + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(this.getCurrentPath()); + if (enclosingMethod != null) { + methodsForNonEmptyChecker.add(enclosingMethod); + } } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java index 91ce61ce720..86fb2802a40 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -2,6 +2,7 @@ import com.sun.source.tree.ArrayAccessTree; import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LiteralTree; @@ -10,6 +11,7 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; import com.sun.source.tree.VariableTree; @@ -929,4 +931,63 @@ private static boolean isVarArgsInvocation( return TypesUtils.getArrayDepth(ElementUtils.getType(lastParamElt)) != TypesUtils.getArrayDepth(lastArgType); } + + /** + * Find the declaration of the receiver of a method call in a method tree. + * + *

The receiver should appear in one of two places, either as a formal parameter to the method, + * or as a local variable. + * + * @param tree the method tree + * @param receiver the receiver for which to look up a declaration + * @return the declaration of the receiver of the method call, if found. Otherwise, null + */ + public static @Nullable VariableTree getReceiverDeclarationInMethod( + @Nullable MethodTree tree, JavaExpression receiver) { + if (tree == null) { + return null; + } + List params = tree.getParameters(); + for (VariableTree param : params) { + if (param.getName().toString().equals(receiver.toString())) { + return param; + } + } + for (StatementTree statement : tree.getBody().getStatements()) { + if (statement instanceof VariableTree) { + VariableTree localVariableTree = (VariableTree) statement; + if (localVariableTree.getName().toString().equals(receiver.toString())) { + return localVariableTree; + } + } + } + return null; + } + + /** + * Find the declaration of the receiver of a method call in a method tree. + * + *

The receiver should appear as a field in the class, if found. + * + *

TODO: what about inherited fields? + * + * @param tree the class tree + * @param receiver the receiver for which to look up a declaration + * @return the declaration of the receiver of the method call, if found. Otherwise, null + */ + public static @Nullable VariableTree getReceiverDeclarationInClass( + @Nullable ClassTree tree, JavaExpression receiver) { + if (tree == null || tree.getMembers().isEmpty()) { + return null; + } + for (Tree member : tree.getMembers()) { + if (member instanceof VariableTree) { + VariableTree field = (VariableTree) member; + if (JavaExpression.fromVariableTree(field).containsSyntacticEqualJavaExpression(receiver)) { + return field; + } + } + } + return null; + } } From 64c469bc69db1174fa04ce58776699469612b330 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 19 May 2024 08:18:27 -0700 Subject: [PATCH 101/110] Use `%n`, not `\n`, for platform independence --- .../checker/nonempty/NonEmptyAnnotatedTypeFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index a2728f96dcb..8e0e6e254bd 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -37,11 +37,11 @@ protected TreeAnnotator createTreeAnnotator() { protected boolean isAnnotatedWithNonEmpty(Tree tree) { System.out.printf( - "NonEmptyAnnotatedTypeFactory::Checking if tree = [%s] is annotated with @NonEmpty\n", + "NonEmptyAnnotatedTypeFactory::Checking if tree = [%s] is annotated with @NonEmpty%n", tree); AnnotatedTypeMirror annotatedTypeMirror = this.getAnnotatedType(tree); System.out.printf( - "Explicit Annotations for [%s] = %s\n", tree, annotatedTypeMirror.getExplicitAnnotations()); + "Explicit Annotations for [%s] = %s%n", tree, annotatedTypeMirror.getExplicitAnnotations()); return false; // stub } From 615205d54d1643985c1296dd8c4b116c45b0980c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 19 May 2024 08:22:40 -0700 Subject: [PATCH 102/110] Minor changes --- .../checker/optional/OptionalTransfer.java | 11 +++++++++-- docs/manual/creating-a-checker.tex | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index 4921d8c1883..e8738799cc1 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -145,7 +145,10 @@ public TransferResult visitMethodInvocation( return result; } refineStreamOperations(n, result); - System.out.printf("Store after Stream operation refinement = %s\n", result); + System.out.printf( + "OptionalTransfer.visitMethodInvocation(%s):%n" + + " TransferResult after Stream operation refinement = %s%n", + n, result); return result; } @@ -173,7 +176,11 @@ private void refineStreamOperations( // TODO: the receiver of the stream operation is @Non-Empty, therefore the result is // @Present JavaExpression internalRepr = JavaExpression.fromNode(n); - System.out.printf("Non-empty detected for = %s\n", internalRepr); + System.out.printf( + "OptionalTransfer.refineStreamOperations: Has non-empty receiver: %s%n", internalRepr); + System.out.printf( + "isAssumePureOrAssumeDeterministicEnabled() = %s%n", + isAssumePureOrAssumeDeterministicEnabled()); if (isAssumePureOrAssumeDeterministicEnabled()) { insertIntoStoresPermitNonDeterministic(result, internalRepr, PRESENT); } else { diff --git a/docs/manual/creating-a-checker.tex b/docs/manual/creating-a-checker.tex index afcdabc51b5..3f0cdc66d72 100644 --- a/docs/manual/creating-a-checker.tex +++ b/docs/manual/creating-a-checker.tex @@ -1872,7 +1872,7 @@ \end{enumerate} -\label{creating-dataflow-disable} % temporary; remove in January 2025 +\label{creating-dataflow-disable} % temporary label; remove in January 2025 \subsectionAndLabel{Further customizing flow-sensitive inference}{creating-dataflow-customization} By default, the Checker Framework assumes that modifications to an object's From 66540334faf17f3ea04ca0f8cfabcd29cfc8c1c7 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 15:11:25 -0700 Subject: [PATCH 103/110] Remove changes related to the Optional Checker --- .../NonEmptyAnnotatedTypeFactory.java | 11 -- .../checker/nonempty/NonEmptyChecker.java | 42 +---- .../OptionalAnnotatedTypeFactory.java | 4 - .../checker/optional/OptionalTransfer.java | 173 ++---------------- 4 files changed, 12 insertions(+), 218 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java index 8e0e6e254bd..6d88eff76b9 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java @@ -2,7 +2,6 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewArrayTree; -import com.sun.source.tree.Tree; import java.util.List; import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.nonempty.qual.NonEmpty; @@ -35,16 +34,6 @@ protected TreeAnnotator createTreeAnnotator() { return new ListTreeAnnotator(super.createTreeAnnotator(), new NonEmptyTreeAnnotator(this)); } - protected boolean isAnnotatedWithNonEmpty(Tree tree) { - System.out.printf( - "NonEmptyAnnotatedTypeFactory::Checking if tree = [%s] is annotated with @NonEmpty%n", - tree); - AnnotatedTypeMirror annotatedTypeMirror = this.getAnnotatedType(tree); - System.out.printf( - "Explicit Annotations for [%s] = %s%n", tree, annotatedTypeMirror.getExplicitAnnotations()); - return false; // stub - } - /** The tree annotator for the Non-Empty Checker. */ private class NonEmptyTreeAnnotator extends TreeAnnotator { diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java index 81e8ddd69e4..4ed028442cc 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java @@ -1,49 +1,9 @@ package org.checkerframework.checker.nonempty; -import com.sun.source.tree.MethodTree; -import java.util.Collections; -import java.util.Set; -import org.checkerframework.checker.optional.OptionalChecker; -import org.checkerframework.checker.optional.OptionalVisitor; -import org.checkerframework.common.basetype.BaseTypeChecker; - /** * A type-checker that prevents {@link java.util.NoSuchElementException} in the use of container * classes. * * @checker_framework.manual #non-empty-checker Non-Empty Checker */ -public class NonEmptyChecker extends DelegationChecker { - - @Override - protected Set> getImmediateSubcheckerClasses() { - Set> checkers = super.getImmediateSubcheckerClasses(); - checkers.add(OptionalChecker.class); - return checkers; - } - - @Override - public boolean shouldSkipDefs(MethodTree tree) { - return !getMethodsToCheck().contains(tree); - } - - /** - * Obtains the methods to check w.r.t. the Non-Empty type system from the Optional Checker. - * - *

The Optional Checker uses explicitly-written (i.e., programmer-written) annotations from the - * Non-Empty type system to refine its analysis with respect to operations on containers (e.g., - * Streams, Collections) that result in values of type Optional. - * - *

This method provides access to the Non-Empty Checker for methods that should be verified - * - * @return a set of methods to be checked by the Non-Empty Checker - */ - private Set getMethodsToCheck() { - OptionalChecker optionalChecker = getSubchecker(OptionalChecker.class); - if (optionalChecker != null && optionalChecker.getVisitor() instanceof OptionalVisitor) { - OptionalVisitor optionalVisitor = (OptionalVisitor) optionalChecker.getVisitor(); - return optionalVisitor.getMethodsForNonEmptyChecker(); - } - return Collections.emptySet(); - } -} +public class NonEmptyChecker extends DelegationChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java index a8716778062..216eaad3d50 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java @@ -131,8 +131,4 @@ public CFTransfer createFlowTransferFunction( CFAbstractAnalysis analysis) { return new OptionalTransfer(analysis); } - - public boolean isTreeAnnotatedWithNonEmpty(ExpressionTree tree) { - return super.isAnnotatedWithNonEmpty(tree); - } } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java index e8738799cc1..de521118adc 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -1,41 +1,29 @@ package org.checkerframework.checker.optional; -import com.sun.source.tree.AnnotationTree; -import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; -import java.util.Arrays; -import java.util.Comparator; import java.util.List; -import java.util.function.BinaryOperator; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.Elements; -import org.checkerframework.checker.nonempty.qual.NonEmpty; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.Present; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.dataflow.analysis.TransferInput; -import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.expression.JavaExpression; -import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; /** The transfer function for the Optional Checker. */ @@ -44,32 +32,14 @@ public class OptionalTransfer extends CFTransfer { /** The @{@link Present} annotation. */ private final AnnotationMirror PRESENT; - /** The @{@link NonEmpty} annotation. */ - private final AnnotationMirror NON_EMPTY; - /** The element for java.util.Optional.ifPresent(). */ private final ExecutableElement optionalIfPresent; /** The element for java.util.Optional.ifPresentOrElse(), or null. */ private final @Nullable ExecutableElement optionalIfPresentOrElse; - /** The element for java.util.stream.Stream.max(), or null. */ - private final @Nullable ExecutableElement streamMax; - - /** The element for java.util.stream.Stream.min(), or null. */ - private final @Nullable ExecutableElement streamMin; - - /** The element for java.util.stream.Stream.reduce(BinaryOperator<T>), or null. */ - private final @Nullable ExecutableElement streamReduceNoIdentity; - - /** The element for java.util.stream.Stream.findFirst(), or null. */ - private final @Nullable ExecutableElement streamFindFirst; - - /** The element for java.util.stream.Stream.findAny(), or null. */ - private final @Nullable ExecutableElement streamFindAny; - - /** The {@link OptionalAnnotatedTypeFactory} instance for this transfer class. */ - private final OptionalAnnotatedTypeFactory optionalTypeFactory; + /** The type factory associated with this transfer function. */ + private final AnnotatedTypeFactory atypeFactory; /** * Create an OptionalTransfer. @@ -78,19 +48,13 @@ public class OptionalTransfer extends CFTransfer { */ public OptionalTransfer(CFAbstractAnalysis analysis) { super(analysis); - optionalTypeFactory = (OptionalAnnotatedTypeFactory) analysis.getTypeFactory(); - Elements elements = optionalTypeFactory.getElementUtils(); + atypeFactory = analysis.getTypeFactory(); + Elements elements = atypeFactory.getElementUtils(); PRESENT = AnnotationBuilder.fromClass(elements, Present.class); - NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); - ProcessingEnvironment env = optionalTypeFactory.getProcessingEnv(); + ProcessingEnvironment env = atypeFactory.getProcessingEnv(); optionalIfPresent = TreeUtils.getMethod("java.util.Optional", "ifPresent", 1, env); optionalIfPresentOrElse = TreeUtils.getMethodOrNull("java.util.Optional", "ifPresentOrElse", 2, env); - streamMax = TreeUtils.getMethodOrNull("java.util.stream.Stream", "max", 1, env); - streamMin = TreeUtils.getMethodOrNull("java.util.stream.Stream", "min", 1, env); - streamReduceNoIdentity = TreeUtils.getMethodOrNull("java.util.stream.Stream", "reduce", 1, env); - streamFindFirst = TreeUtils.getMethodOrNull("java.util.stream.Stream", "findFirst", 0, env); - streamFindAny = TreeUtils.getMethodOrNull("java.util.stream.Stream", "findAny", 0, env); } @Override @@ -106,7 +70,7 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List LambdaExpressionTree lambdaTree = cfgLambda.getLambdaTree(); List lambdaParams = lambdaTree.getParameters(); if (lambdaParams.size() == 1) { - TreePath lambdaPath = optionalTypeFactory.getPath(lambdaTree); + TreePath lambdaPath = atypeFactory.getPath(lambdaTree); Tree lambdaParent = lambdaPath.getParentPath().getLeaf(); if (lambdaParent.getKind() == Tree.Kind.METHOD_INVOCATION) { MethodInvocationTree invok = (MethodInvocationTree) lambdaParent; @@ -114,7 +78,9 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List if (methodElt.equals(optionalIfPresent) || methodElt.equals(optionalIfPresentOrElse)) { // `underlyingAST` is an invocation of `Optional.ifPresent()` or // `Optional.ifPresentOrElse()`. In the lambda, the receiver is @Present. - JavaExpression receiverJe = JavaExpression.fromTree(TreeUtils.getReceiverTree(invok)); + ExpressionTree methodSelectTree = TreeUtils.withoutParens(invok.getMethodSelect()); + ExpressionTree receiverTree = ((MemberSelectTree) methodSelectTree).getExpression(); + JavaExpression receiverJe = JavaExpression.fromTree(receiverTree); result.insertValue(receiverJe, PRESENT); } } @@ -133,121 +99,4 @@ public CFStore initialStore(UnderlyingAST underlyingAST, List return result; } - - @Override - public TransferResult visitMethodInvocation( - MethodInvocationNode n, TransferInput in) { - TransferResult result = - super.visitMethodInvocation( - n, in); // Is the error being emitted because this happens first? Before the store is - // updated? - if (n.getTree() == null) { - return result; - } - refineStreamOperations(n, result); - System.out.printf( - "OptionalTransfer.visitMethodInvocation(%s):%n" - + " TransferResult after Stream operation refinement = %s%n", - n, result); - return result; - } - - /** - * Refines the result of a call to {@link java.util.stream.Stream#max(Comparator)}, {@link - * java.util.stream.Stream#min(Comparator)}, or {@link - * java.util.stream.Stream#reduce(BinaryOperator)}. - * - *

The presence/emptiness of the Optional value returned in the method invocations above are - * dependent on whether the initial stream (i.e., the receiver) is empty (or not). That is, - * invoking the methods above on a {@code @NonEmpty} stream will return a {@code @Present} - * Optional, while invoking them on an empty stream will return an empty Optional. - * - * @param n the method invocation node - * @param result the transfer result to side effect - */ - private void refineStreamOperations( - MethodInvocationNode n, TransferResult result) { - List relevantStreamMethods = - Arrays.asList(streamMax, streamMin, streamReduceNoIdentity, streamFindFirst, streamFindAny); - if (relevantStreamMethods.stream() - .anyMatch( - op -> NodeUtils.isMethodInvocation(n, op, optionalTypeFactory.getProcessingEnv()))) { - if (isReceiverNonEmpty(n)) { - // TODO: the receiver of the stream operation is @Non-Empty, therefore the result is - // @Present - JavaExpression internalRepr = JavaExpression.fromNode(n); - System.out.printf( - "OptionalTransfer.refineStreamOperations: Has non-empty receiver: %s%n", internalRepr); - System.out.printf( - "isAssumePureOrAssumeDeterministicEnabled() = %s%n", - isAssumePureOrAssumeDeterministicEnabled()); - if (isAssumePureOrAssumeDeterministicEnabled()) { - insertIntoStoresPermitNonDeterministic(result, internalRepr, PRESENT); - } else { - insertIntoStores(result, internalRepr, PRESENT); - } - } - } - } - - /** - * Determine whether this analysis is being executed with the {@literal -AassumePure or {@literal -AassumeDeterministic} flags. - * @return true if the {@literal -AassumePure} or {@literal -AassumeDeterministic} flags are passed to this analysis - */ - private boolean isAssumePureOrAssumeDeterministicEnabled() { - BaseTypeChecker checker = analysis.getTypeFactory().getChecker(); - return checker.hasOption("assumePure") || checker.hasOption("assumeDeterministic"); - } - - /** - * Returns true if the receiver of the given method invocation is annotated with @{@link - * NonEmpty}. - * - * @param methodInvok a method invocation node - * @return true if the receiver of the given method invocation is annotated with @{@link NonEmpty} - */ - private boolean isReceiverNonEmpty(MethodInvocationNode methodInvok) { - ExpressionTree receiverTree = TreeUtils.getReceiverTree(methodInvok.getTree()); - JavaExpression receiver; - if (receiverTree instanceof MethodInvocationTree) { - receiver = JavaExpression.getInitialReceiverOfMethodInvocation(receiverTree); - } else { - receiver = JavaExpression.fromTree(receiverTree); - } - VariableTree receiverDeclaration = getReceiverDeclaration(methodInvok, receiver); - if (receiverDeclaration == null) { - return false; - } - List receiverAnnotationTrees = - receiverDeclaration.getModifiers().getAnnotations(); - List annotationMirrors = - TreeUtils.annotationsFromTypeAnnotationTrees(receiverAnnotationTrees); - return AnnotationUtils.containsSame(annotationMirrors, NON_EMPTY); - } - - /** - * Returns the declaration of the initial receiver of the given method invocation node. - * - *

An attempt is first made to find the declaration of the receiver in the method that - * immediately encloses the given method invocation node. If this is unsuccessful, an attempt is - * made to look for the receiver in the fields of the class that immediately encloses the given - * method invocation node. - * - * @param methodInvok a method invocation node - * @param initialReceiver the initial receiver in the method invocation node - * @return the declaration of the receiver if found, else null - */ - private @Nullable VariableTree getReceiverDeclaration( - MethodInvocationNode methodInvok, JavaExpression initialReceiver) { - // Look in the method, first - MethodTree methodTree = TreePathUtil.enclosingMethod(methodInvok.getTreePath()); - VariableTree declarationInMethod = - JavaExpression.getReceiverDeclarationInMethod(methodTree, initialReceiver); - if (declarationInMethod != null) { - return declarationInMethod; - } - // If the declaration can't be found in the method, look in the class - ClassTree classTree = TreePathUtil.enclosingClass(methodInvok.getTreePath()); - return JavaExpression.getReceiverDeclarationInClass(classTree, initialReceiver); - } } From 878ffeb8d685d426c3f3e2fdaa392b2f25aa2df6 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 15:16:13 -0700 Subject: [PATCH 104/110] Remove Non-Empty related code --- .../nonempty/qual/EnsuresNonEmpty.java | 51 --- .../nonempty/qual/EnsuresNonEmptyIf.java | 91 ---- .../checker/nonempty/qual/NonEmpty.java | 20 - .../checker/nonempty/qual/PolyNonEmpty.java | 20 - .../nonempty/qual/RequiresNonEmpty.java | 104 ----- .../nonempty/qual/UnknownNonEmpty.java | 22 - .../delegation}/qual/Delegate.java | 2 +- .../qual/DelegatorMustOverride.java | 2 +- .../NonEmptyAnnotatedTypeFactory.java | 55 --- .../checker/nonempty/NonEmptyChecker.java | 9 - .../checker/nonempty/NonEmptyTransfer.java | 424 ------------------ .../common/delegation}/DelegationChecker.java | 6 +- .../common/delegation}/messages.properties | 0 13 files changed, 5 insertions(+), 801 deletions(-) delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java delete mode 100644 checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java rename checker-qual/src/main/java/org/checkerframework/{checker/nonempty => common/delegation}/qual/Delegate.java (94%) rename checker-qual/src/main/java/org/checkerframework/{checker/nonempty => common/delegation}/qual/DelegatorMustOverride.java (96%) delete mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java rename {checker/src/main/java/org/checkerframework/checker/nonempty => framework/src/main/java/org/checkerframework/common/delegation}/DelegationChecker.java (98%) rename {checker/src/main/java/org/checkerframework/checker/nonempty => framework/src/main/java/org/checkerframework/common/delegation}/messages.properties (100%) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java deleted file mode 100644 index a5754473767..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmpty.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.PostconditionAnnotation; - -/** - * Indicates that the expression evaluates to a non-empty {@link java.util.Collection collection}, - * {@link java.util.Iterator iterator}, {@link java.lang.Iterable iterable}, or {@link java.util.Map - * map}, if the method terminates successfully. - * - *

This postcondition annotation is useful for methods that construct a non-empty collection, - * iterator, iterable, or map: - * - *


- *   {@literal @}EnsuresNonEmpty("ids")
- *   void addId(String id) {
- *     ids.add(id);
- *   }
- * 
- * - * It can also be used for a method that fails if a given collection, iterator, iterable, or map is - * empty, indicating that the argument is non-empty if the method returns normally: - * - *

- *   /** Throws an exception if the argument is empty. */
- *   {@literal @}EnsuresNonEmpty("#1")
- *   void useTheMap(Map<T, U> arg) { ... }
- * 
- * - * @see NonEmpty - * @see org.checkerframework.checker.nonempty.NonEmptyChecker - * @checker_framework.manual #non-empty-checker Non-Empty Checker - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -@PostconditionAnnotation(qualifier = NonEmpty.class) -@InheritedAnnotation -public @interface EnsuresNonEmpty { - /** - * The expression (a collection, iterator, iterable, or map) that is non-empty, if the method - * returns normally. - * - * @return the expression (a collection, iterator, iterable, or map) that is non-empty, if the - * method returns normally - */ - String[] value(); -} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java deleted file mode 100644 index c761c93feb2..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/EnsuresNonEmptyIf.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; -import org.checkerframework.framework.qual.InheritedAnnotation; - -/** - * Indicates that the given expressions which may be {@link java.util.Collection collections}, - * {@link java.util.Iterator iterators}, {@link java.lang.Iterable iterables}, or {@link - * java.util.Map maps} are non-empty, if the method returns the given result (either true or false). - * - *

Here are ways this conditional postcondition annotation can be used: - * - *

Method parameters: Suppose that a method has a parameter that is a list, and returns - * true if the length of the list is non-zero. You could annotate the method as follows: - * - *

 @EnsuresNonEmptyIf(result = true, expression = "#1")
- *  public <T> boolean isLengthGreaterThanZero(List<T> items) { ... }
- * 
- * - * because, if {@code isLengthGreaterThanZero} returns true, then {@code items} was non-empty. Note - * that you can write more than one {@code @EnsuresNonEmptyIf} annotations on a single method. - * - *

Fields: The value expression can refer to fields, even private ones. For example: - * - *

 @EnsuresNonEmptyIf(result = true, expression = "this.orders")
- *  public <T> boolean areOrdersActive() {
- *    return this.orders != null && this.orders.size() > 0;
- * }
- * - * An {@code @EnsuresNonEmptyIf} annotation that refers to a private field is useful for verifying - * that a method establishes a property, even though client code cannot directly affect the field. - * - *

Method postconditions: Suppose that if a method {@code areOrdersActive()} returns - * true,p then {@code getOrders()} will return a non-empty Map. You can express this relationship - * as: - * - *

 @EnsuresNonEmptyIf(result = true, expression = "this.getOrders()")
- *  public <T> boolean areOrdersActive() {
- *    return this.orders != null && this.orders.size() > 0;
- * }
- * - * @see NonEmpty - * @see EnsuresNonEmpty - * @checker_framework.manual #non-empty-checker Non-Empty Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -@ConditionalPostconditionAnnotation(qualifier = NonEmpty.class) -@InheritedAnnotation -public @interface EnsuresNonEmptyIf { - - /** - * Returns the return value of the method for which the postcondition holds. - * - * @return the return value of the method for which the postcondition holds - */ - boolean result(); - - /** - * Returns the Java expressions which may be {@link java.util.Collection collections}, {@link - * java.util.Iterator iterators}, {@link java.lang.Iterable iterables}, or {@link java.util.Map - * maps} that are non-empty after the method returns the given result. - * - * @return the Java expressions that are non-empty after the method returns the given result - */ - String[] expression(); - - /** - * A wrapper annotation that makes the {@link EnsuresNonEmptyIf} annotation repeatable. - * - *

Programmers generally do not need to write ths. It is created by Java when a programmer - * writes more than one {@link EnsuresNonEmptyIf} annotation at the same location. - */ - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) - @ConditionalPostconditionAnnotation(qualifier = NonEmpty.class) - @interface List { - /** - * Returns the repeatable annotations. - * - * @return the repeatable annotations - */ - EnsuresNonEmptyIf[] value(); - } -} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java deleted file mode 100644 index d5a7f8c15d5..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/NonEmpty.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; - -/** - * The {@link java.util.Collection Collection}, {@link java.util.Iterator Iterator}, {@link - * java.lang.Iterable Iterable}, or {@link java.util.Map Map} is definitely non-empty. - * - * @checker_framework.manual #non-empty-checker Non-Empty Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) -@SubtypeOf(UnknownNonEmpty.class) -public @interface NonEmpty {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java deleted file mode 100644 index dd538314595..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/PolyNonEmpty.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.PolymorphicQualifier; - -/** - * A polymorphic qualifier for the Non-Empty type system. - * - * @checker_framework.manual #non-empty-checker Non-Empty Checker - * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(UnknownNonEmpty.class) -public @interface PolyNonEmpty {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java deleted file mode 100644 index 832f21ccaa9..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/RequiresNonEmpty.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.PreconditionAnnotation; - -/** - * Indicates a method precondition: the specified expressions that may be a {@link - * java.util.Collection collection}, {@link java.util.Iterator iterator}, {@link java.lang.Iterable - * iterable}, or {@link java.util.Map map} must be non-empty when the annotated method is invoked. - * - *

For example: - * - *

- * import java.util.LinkedList;
- * import java.util.List;
- * import org.checkerframework.checker.nonempty.qual.NonEmpty;
- * import org.checkerframework.checker.nonempty.qual.RequiresNonEmpty;
- * import org.checkerframework.dataflow.qual.Pure;
- *
- * class MyClass {
- *
- *   List<String> list1 = new LinkedList<>();
- *   List<String> list2;
- *
- *     @RequiresNonEmpty("list1")
- *     @Pure
- *   void m1() {}
- *
- *     @RequiresNonEmpty({"list1", "list2"})
- *     @Pure
- *   void m2() {}
- *
- *     @RequiresNonEmpty({"list1", "list2"})
- *   void m3() {}
- *
- *   void m4() {}
- *
- *   void test(@NonEmpty List<String> l1, @NonEmpty List<String> l2) {
- *     MyClass testClass = new MyClass();
- *
- *     // At this point, we should have an error since m1 requires that list1 is @NonEmpty, which is
- *     // not the case here
- *     // :: error: (contracts.precondition)
- *     testClass.m1();
- *
- *     testClass.list1 = l1;
- *     testClass.m1(); // OK
- *
- *     // A call to m2 is stil illegal here, since list2 is still @UnknownNonEmpty
- *     // :: error: (contracts.precondition)
- *     testClass.m2();
- *
- *     testClass.list2 = l2;
- *     testClass.m2(); // OK
- *
- *     testClass.m4();
- *
- *     // No longer OK to call m2, no guarantee that m4() was pure
- *     // :: error: (contracts.precondition)
- *     testClass.m2();
- *   }
- * }
- * 
- * - * This annotation should not be used for formal parameters (instead, give them a {@code @NonEmpty} - * type). The {@code @RequiresNonEmpty} annotation is intended for non-parameter expressions, such - * as field accesses or method calls. - * - * @checker_framework.manual #non-empty-checker Non-Empty Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.PARAMETER}) -@PreconditionAnnotation(qualifier = NonEmpty.class) -public @interface RequiresNonEmpty { - - /** - * The Java {@link java.util.Collection collection}, {@link java.util.Iterator iterator}, {@link - * java.lang.Iterable iterable}, or {@link java.util.Map map} that must be non-empty. - * - * @return the Java {@link java.util.Collection collection}, {@link java.util.Iterator iterator}, - * {@link java.lang.Iterable iterable}, or {@link java.util.Map map} - */ - String[] value(); - - /** - * A wrapper annotation that makes the {@link RequiresNonEmpty} annotation repeatable. - * - *

Programmers generally do not need to write this. It is created by Java when a programmer - * writes more than one {@link RequiresNonEmpty} annotation at the same location. - */ - @interface List { - /** - * Returns the repeatable annotations. - * - * @return the repeatable annotations - */ - RequiresNonEmpty[] value(); - } -} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java deleted file mode 100644 index 65900f7fa12..00000000000 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/UnknownNonEmpty.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.checkerframework.checker.nonempty.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; - -/** - * The {@link java.util.Collection Collection}, {@link java.util.Iterator Iterator}, {@link - * java.lang.Iterable Iterable}, or {@link java.util.Map Map} may or may not be empty. - * - * @checker_framework.manual #non-empty-checker Non-Empty Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) -@DefaultQualifierInHierarchy -@SubtypeOf({}) -public @interface UnknownNonEmpty {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java similarity index 94% rename from checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java rename to checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java index 90b6b50193d..f59642d8a83 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/Delegate.java +++ b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java @@ -1,4 +1,4 @@ -package org.checkerframework.checker.nonempty.qual; +package org.checkerframework.common.delegation.qual; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegatorMustOverride.java b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/DelegatorMustOverride.java similarity index 96% rename from checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegatorMustOverride.java rename to checker-qual/src/main/java/org/checkerframework/common/delegation/qual/DelegatorMustOverride.java index c6c18a2467b..5e89bd2026f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nonempty/qual/DelegatorMustOverride.java +++ b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/DelegatorMustOverride.java @@ -1,4 +1,4 @@ -package org.checkerframework.checker.nonempty.qual; +package org.checkerframework.common.delegation.qual; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java deleted file mode 100644 index 6d88eff76b9..00000000000 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyAnnotatedTypeFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.checkerframework.checker.nonempty; - -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.NewArrayTree; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.javacutil.AnnotationBuilder; - -public class NonEmptyAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - - /** The @{@link NonEmpty} annotation. */ - public final AnnotationMirror NON_EMPTY = AnnotationBuilder.fromClass(elements, NonEmpty.class); - - /** - * Creates a new {@link NonEmptyAnnotatedTypeFactory} that operates on a particular AST. - * - * @param checker the checker to use - */ - public NonEmptyAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker); - this.sideEffectsUnrefineAliases = true; - this.postInit(); - } - - @Override - protected TreeAnnotator createTreeAnnotator() { - return new ListTreeAnnotator(super.createTreeAnnotator(), new NonEmptyTreeAnnotator(this)); - } - - /** The tree annotator for the Non-Empty Checker. */ - private class NonEmptyTreeAnnotator extends TreeAnnotator { - - public NonEmptyTreeAnnotator(AnnotatedTypeFactory aTypeFactory) { - super(aTypeFactory); - } - - @Override - public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { - if (!type.hasEffectiveAnnotation(NON_EMPTY)) { - List initializers = tree.getInitializers(); - if (initializers != null && !initializers.isEmpty()) { - type.replaceAnnotation(NON_EMPTY); - } - } - return super.visitNewArray(tree, type); - } - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java deleted file mode 100644 index 4ed028442cc..00000000000 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyChecker.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.checkerframework.checker.nonempty; - -/** - * A type-checker that prevents {@link java.util.NoSuchElementException} in the use of container - * classes. - * - * @checker_framework.manual #non-empty-checker Non-Empty Checker - */ -public class NonEmptyChecker extends DelegationChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java b/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java deleted file mode 100644 index 6f358cae9dd..00000000000 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/NonEmptyTransfer.java +++ /dev/null @@ -1,424 +0,0 @@ -package org.checkerframework.checker.nonempty; - -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.Tree; -import java.util.Arrays; -import java.util.List; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.nonempty.qual.Delegate; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; -import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.dataflow.analysis.TransferInput; -import org.checkerframework.dataflow.analysis.TransferResult; -import org.checkerframework.dataflow.cfg.node.*; -import org.checkerframework.dataflow.expression.JavaExpression; -import org.checkerframework.dataflow.util.NodeUtils; -import org.checkerframework.framework.flow.CFAnalysis; -import org.checkerframework.framework.flow.CFStore; -import org.checkerframework.framework.flow.CFTransfer; -import org.checkerframework.framework.flow.CFValue; -import org.checkerframework.javacutil.TreePathUtil; -import org.checkerframework.javacutil.TreeUtils; - -/** - * This class provides methods used by the Non-Empty Checker as transfer functions for type rules - * that cannot be expressed via simple pre- or post-conditional annotations. - */ -public class NonEmptyTransfer extends CFTransfer { - - /** A {@link ProcessingEnvironment} instance. */ - private final ProcessingEnvironment env; - - /** The {@code size()} method of the {@link java.util.Collection} interface. */ - private final ExecutableElement collectionSize; - - /** The {@code size()} method of the {@link java.util.Map} class. */ - private final ExecutableElement mapSize; - - /** The {@code indexOf(Object)} method of the {@link java.util.List} class. */ - private final ExecutableElement indexOf; - - /** A {@link NonEmptyAnnotatedTypeFactory} instance. */ - protected final NonEmptyAnnotatedTypeFactory aTypeFactory; - - public NonEmptyTransfer(CFAnalysis analysis) { - super(analysis); - - this.env = analysis.getTypeFactory().getProcessingEnv(); - this.collectionSize = TreeUtils.getMethod("java.util.Collection", "size", 0, this.env); - this.mapSize = TreeUtils.getMethod("java.util.Map", "size", 0, this.env); - this.indexOf = TreeUtils.getMethod("java.util.List", "indexOf", 1, this.env); - this.aTypeFactory = (NonEmptyAnnotatedTypeFactory) analysis.getTypeFactory(); - } - - @Override - public TransferResult visitMethodInvocation( - MethodInvocationNode n, TransferInput in) { - TransferResult result = super.visitMethodInvocation(n, in); - MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(n.getTreePath()); - if (enclosingMethodTree == null || TreeUtils.isConstructor(enclosingMethodTree)) { - return result; - } - Tree receiverTree = n.getTarget().getReceiver().getTree(); - if (receiverTree == null) { - return result; - } - Element receiver = TreeUtils.elementFromTree(receiverTree); - if (receiver == null - || !shouldRefineStoreForDelegationInvocation(receiver, enclosingMethodTree)) { - return result; - } - JavaExpression thisExpr = JavaExpression.getImplicitReceiver(receiver); - refineStoreForDelegationInvocation( - thisExpr, JavaExpression.fromNode(n.getTarget().getReceiver()), result); - return result; - } - - @Override - public TransferResult visitEqualTo( - EqualToNode n, TransferInput in) { - TransferResult result = super.visitEqualTo(n, in); - // Account for the case where the sizes of two containers are compared - strengthenAnnotationSizeEquals(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); - // Account for the case where size is checked against a non-zero integer - refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); - refineGTE(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); - // A == 0 is the inversion of A != 0 - refineNotEqual(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); - refineNotEqual(n.getRightOperand(), n.getLeftOperand(), result.getElseStore()); - return result; - } - - @Override - public TransferResult visitNotEqual( - NotEqualNode n, TransferInput in) { - TransferResult result = super.visitNotEqual(n, in); - strengthenAnnotationSizeEquals(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); - refineNotEqual(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); - refineNotEqual(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); - return result; - } - - @Override - public TransferResult visitLessThan( - LessThanNode n, TransferInput in) { - TransferResult result = super.visitLessThan(n, in); - - // A < B is equivalent to B > A - refineGT(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); - // This handles the case where n < container.size() - refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); - return result; - } - - @Override - public TransferResult visitLessThanOrEqual( - LessThanOrEqualNode n, TransferInput in) { - TransferResult result = super.visitLessThanOrEqual(n, in); - - // A <= B is equivalent to B > A - refineGT(n.getLeftOperand(), n.getRightOperand(), result.getElseStore()); - // This handles the case where n <= container.size() - refineGTE(n.getRightOperand(), n.getLeftOperand(), result.getThenStore()); - return result; - } - - @Override - public TransferResult visitGreaterThan( - GreaterThanNode n, TransferInput in) { - TransferResult result = super.visitGreaterThan(n, in); - refineGT(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); - return result; - } - - @Override - public TransferResult visitGreaterThanOrEqual( - GreaterThanOrEqualNode n, TransferInput in) { - TransferResult result = super.visitGreaterThanOrEqual(n, in); - refineGTE(n.getLeftOperand(), n.getRightOperand(), result.getThenStore()); - return result; - } - - @Override - public TransferResult visitCase( - CaseNode n, TransferInput in) { - TransferResult result = super.visitCase(n, in); - List caseOperands = n.getCaseOperands(); - AssignmentNode assign = n.getSwitchOperand(); - Node switchNode = assign.getExpression(); - refineSwitchStatement(switchNode, caseOperands, result.getThenStore(), result.getElseStore()); - return result; - } - - /** - * Return true if the transfer store for "this" should be updated, depending on whether a delegate - * method invocation is found within a method body. - * - *

Note: the Non-Empty Checker trusts the {@link Delegate} annotations it finds. The {@link - * DelegationChecker} verifies correct use of the delegation pattern. Since it is run alongside - * the Non-Empty Checker, the annotations it finds should be correct. - * - * @param receiver the receiver of a candidate delegate method call found in a method body - * @param enclosingMethodTree the method enclosing the candidate delegate call - * @return true if the receiver is annotated with {@link Delegate} and the method is annotated - * with a postcondition annotation from the Non-Empty type system. - */ - private boolean shouldRefineStoreForDelegationInvocation( - Element receiver, MethodTree enclosingMethodTree) { - Element enclosingMethod = TreeUtils.elementFromDeclaration(enclosingMethodTree); - AnnotationMirror delegateAnno = aTypeFactory.getDeclAnnotation(receiver, Delegate.class); - AnnotationMirror postConditionAnno = - aTypeFactory.getDeclAnnotation(enclosingMethod, EnsuresNonEmpty.class); - AnnotationMirror conditionalPostconditionAnno = - aTypeFactory.getDeclAnnotation(enclosingMethod, EnsuresNonEmptyIf.class); - return delegateAnno != null - && (postConditionAnno != null || conditionalPostconditionAnno != null); - } - - /** - * Updates the value in the store for the target expression when a delegate call is detected. - * - *

For example, if a field {@code map} is marked with {@link Delegate}, and the enclosing class - * delegates a call to it (e.g., a call to {@code containsValue(Object)}), then an instance of the - * enclosing class should have the same postconditions that hold for {@code map}. - * - * @param targetExpr the value for which the store should be updated - * @param delegate the delegate field - * @param result the transfer result - */ - private void refineStoreForDelegationInvocation( - JavaExpression targetExpr, JavaExpression delegate, TransferResult result) { - if (result.containsTwoStores()) { - // Update the "then" store - CFStore thenStore = result.getThenStore(); - CFValue delegateThenStoreValue = thenStore.getValue(delegate); - thenStore.replaceValue(targetExpr, delegateThenStoreValue); - - // Update the "else" store - CFStore elseStore = result.getElseStore(); - CFValue delegateElseStoreValue = elseStore.getValue(delegate); - elseStore.replaceValue(targetExpr, delegateElseStoreValue); - } else { - CFStore store = result.getRegularStore(); - CFValue delegateStoreValue = store.getValue(delegate); - store.replaceValue(targetExpr, delegateStoreValue); - } - } - - /** - * Refine the transfer result's store, given the left- and right-hand side of an equality check - * comparing container sizes. - * - * @param lhs a node that may be a method invocation for {@link java.util.Collection size()} or - * {@link java.util.Map size()} - * @param rhs a node that may be a method invocation for {@link java.util.Collection size()} or - * {@link java.util.Map size()} - * @param store the "then" store of the comparison operation - */ - private void strengthenAnnotationSizeEquals(Node lhs, Node rhs, CFStore store) { - if (!isSizeAccess(lhs) || !isSizeAccess(rhs)) { - return; - } - AnnotationMirror lhsNonEmptyAnno = - aTypeFactory.getAnnotationFromJavaExpression( - getReceiver(lhs), lhs.getTree(), NonEmpty.class); - AnnotationMirror rhsNonEmptyAnno = - aTypeFactory.getAnnotationFromJavaExpression( - getReceiver(rhs), rhs.getTree(), NonEmpty.class); - // TODO: use aTypeFactory.getQualifierHierarchy().greatestLowerBoundQualifiersOnly() ? - if (lhsNonEmptyAnno != null) { - store.insertValue(getReceiver(rhs), aTypeFactory.NON_EMPTY); - } else if (rhsNonEmptyAnno != null) { - store.insertValue(getReceiver(lhs), aTypeFactory.NON_EMPTY); - } - } - - /** - * Updates the transfer result's store with information from the Non-Empty type system for - * expressions of the form {@code container.size() != n}, {@code n != container.size()}, or {@code - * container.indexOf(Object) != n}. - * - *

For example, the type of {@code container} in the "then" branch of a conditional statement - * with the test {@code container.size() != n} where {@code n} is 0 should refine to - * {@code @NonEmpty}. - * - *

This method is also used to refine the "else" store of an equality comparison where {@code - * container.size()} is compared against 0. - * - * @param left the left operand of a binary operation - * @param right the right operand of a binary operation - * @param store the abstract store to update - */ - private void refineNotEqual(Node left, Node right, CFStore store) { - boolean isSizeComparison = isSizeComparison(left, right); - boolean isIndexOfComparison = isIndexOfComparison(left, right); - if (!isSizeComparison && !isIndexOfComparison) { - return; - } - // In case of a size() comparison, refine the store if the value is 0 - // In case of a indexOf(Object) check, refine the store if the value is -1 - int threshold = isSizeComparison ? 0 : -1; - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) right; - if (integerLiteralNode.getValue() == threshold) { - JavaExpression receiver = getReceiver(left); - store.insertValue(receiver, aTypeFactory.NON_EMPTY); - } - } - - /** - * Updates the transfer result's store with information from the Non-Empty type system for - * expressions of the form {@code container.size() > n} or {@code container.indexOf(Object) > n}. - * - *

For example, the type of {@code container} in the "then" branch of a conditional statement - * with the test {@code container.size() > n} where {@code n >= 0} should be refined to - * {@code @NonEmpty}. - * - * @param left the left operand of a binary operation - * @param right the right operand of a binary operation - * @param store the abstract store to update - */ - private void refineGT(Node left, Node right, CFStore store) { - boolean isSizeComparison = isSizeComparison(left, right); - boolean isIndexOfComparison = isIndexOfComparison(left, right); - if (!isSizeComparison && !isIndexOfComparison) { - return; - } - // In case of a size() comparison, refine the store if the value is 0 - // In case of a indexOf(Object) check, refine the store if the value is -1 - int threshold = isSizeComparison ? 0 : -1; - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) right; - if (integerLiteralNode.getValue() >= threshold) { - JavaExpression receiver = getReceiver(left); - store.insertValue(receiver, aTypeFactory.NON_EMPTY); - } - } - - /** - * Updates the transfer result's store with information from the Non-Empty type system for - * expressions of the form {@code container.size() >= n} or {@code container.indexOf(Object) >= - * n}. - * - *

For example, the type of {@code container} in the "then" branch of a conditional statement - * with the test {@code container.size() >= n} where {@code n > 0} should be refined to - * {@code @NonEmpty}. - * - *

This method is also used to refine the "then" branch of an equality comparison where {@code - * container.size()} is compared against a non-zero value. - * - * @param left the left operand of a binary operation - * @param right the right operand of a binary operation - * @param store the abstract store to update - */ - private void refineGTE(Node left, Node right, CFStore store) { - boolean isSizeComparison = isSizeComparison(left, right); - boolean isIndexOfComparison = isIndexOfComparison(left, right); - if (!isSizeComparison && !isIndexOfComparison) { - return; - } - IntegerLiteralNode integerLiteralNode = (IntegerLiteralNode) right; - JavaExpression receiver = getReceiver(left); - // In an indexOf(Object) comparison, if the index is GTE 0, then the object is within the - // container - if (isIndexOfComparison && integerLiteralNode.getValue() >= 0) { - store.insertValue(receiver, aTypeFactory.NON_EMPTY); - return; - } - if (integerLiteralNode.getValue() > 0) { - store.insertValue(receiver, aTypeFactory.NON_EMPTY); - } - } - - /** - * Updates the transfer result's store with information from the Non-Empty type system for switch - * statements, where the test expression is of the form {@code container.size()} or {@code - * container.indexOf(Object)}. - * - *

For example, the "then" store of any case node with an integer value greater than 0 (or -1, - * in the case of the test expression being a call to {@code container.indexOf(Object)}) should - * refine the type of {@code container} to {@code @NonEmpty}. - * - * @param testNode a node that is the test expression for a {@code switch} statement - * @param caseOperands the operands within each case label - * @param thenStore the "then" store - * @param elseStore the "else" store, corresponding to the "default" case label - */ - private void refineSwitchStatement( - Node testNode, List caseOperands, CFStore thenStore, CFStore elseStore) { - boolean isIndexOfAccess = NodeUtils.isMethodInvocation(testNode, indexOf, env); - if (!isSizeAccess(testNode) && !isIndexOfAccess) { - return; - } - for (Node caseOperand : caseOperands) { - if (!(caseOperand instanceof IntegerLiteralNode)) { - continue; - } - IntegerLiteralNode caseIntegerLiteral = (IntegerLiteralNode) caseOperand; - JavaExpression receiver = getReceiver(testNode); - CFStore storeToUpdate; - if (isIndexOfAccess) { - storeToUpdate = caseIntegerLiteral.getValue() >= 0 ? thenStore : elseStore; - } else { - storeToUpdate = caseIntegerLiteral.getValue() > 0 ? thenStore : elseStore; - } - storeToUpdate.insertValue(receiver, aTypeFactory.NON_EMPTY); - } - } - - /** - * Check whether a given binary operation corresponds to a {@link java.util.List size()} or {@link - * java.util.Map size()}comparison. - * - * @param left the left operand of a binary operation - * @param right the right operand of a binary operation - * @return true if the operands correspond to a {@link java.util.List size()} or {@link - * java.util.Map size()} comparison - */ - private boolean isSizeComparison(Node left, Node right) { - // Use `List.of()` in Java 9+ - return isSizeAccess(left) && right instanceof IntegerLiteralNode; - } - - /** - * Return true if the given node is an instance of a method invocation node for {@link - * java.util.Collection size()} or {@link java.util.Map size()}. - * - * @param possibleSizeAccess a node that may be a method call to the {@code size()} method in the - * {@link java.util.Collection} or {@link java.util.Map} types - * @return true if the node is a method call to size() - */ - private boolean isSizeAccess(Node possibleSizeAccess) { - // In Java 9+, use `List.of()` - List sizeAccessMethods = Arrays.asList(collectionSize, mapSize); - return sizeAccessMethods.stream() - .anyMatch( - sizeAccessMethod -> - NodeUtils.isMethodInvocation(possibleSizeAccess, sizeAccessMethod, env)); - } - - /** - * Check whether a given binary operation corresponds to a {@link java.util.List indexOf(Object)} - * comparison. - * - * @param left the left operand of a binary operation - * @param right the right operand of a binary operation - * @return true if the operands correspond to a {@link java.util.List indexOf(Object)} comparison. - */ - private boolean isIndexOfComparison(Node left, Node right) { - return NodeUtils.isMethodInvocation(left, indexOf, env) && right instanceof IntegerLiteralNode; - } - - /** - * Return the receiver as a {@link JavaExpression} given a method invocation node. - * - * @param node an instance of a method access - * @return the receiver as a {@link JavaExpression} - */ - private JavaExpression getReceiver(Node node) { - MethodAccessNode methodAccessNode = ((MethodInvocationNode) node).getTarget(); - return JavaExpression.fromNode(methodAccessNode.getReceiver()); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java b/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java similarity index 98% rename from checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java rename to framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java index 07a2ee17360..f7c1141bd4c 100644 --- a/checker/src/main/java/org/checkerframework/checker/nonempty/DelegationChecker.java +++ b/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java @@ -1,4 +1,4 @@ -package org.checkerframework.checker.nonempty; +package org.checkerframework.common.delegation; import com.sun.source.tree.*; import java.util.*; @@ -7,12 +7,12 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.util.ElementFilter; -import org.checkerframework.checker.nonempty.qual.Delegate; -import org.checkerframework.checker.nonempty.qual.DelegatorMustOverride; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.common.delegation.qual.Delegate; +import org.checkerframework.common.delegation.qual.DelegatorMustOverride; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; diff --git a/checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties b/framework/src/main/java/org/checkerframework/common/delegation/messages.properties similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/nonempty/messages.properties rename to framework/src/main/java/org/checkerframework/common/delegation/messages.properties From 6ec05160c9f8dd9b892e8b48d528cc6416c667db Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 15:54:34 -0700 Subject: [PATCH 105/110] Start of Standalone Delegation Checker --- .../common/delegation/qual/Delegate.java | 3 - .../OptionalAnnotatedTypeFactory.java | 4 +- .../checker/optional/OptionalVisitor.java | 153 ------------ .../optional/RevisedOptionalChecker.java | 15 -- .../checker/test/junit/NonEmptyTest.java | 24 -- .../tests/nonempty/IndexOfNonNegative.java | 83 ------- checker/tests/nonempty/Issue6407.java | 61 ----- .../tests/nonempty/NonEmptyHierarchyTest.java | 15 -- .../tests/nonempty/PredicateTestMethod.java | 17 -- checker/tests/nonempty/SizeInIsEmpty.java | 75 ------ .../collections/UnmodifiableTest.java | 51 ---- .../nonempty/iterator/IteratorOperations.java | 75 ------ checker/tests/nonempty/list/Comparisons.java | 226 ------------------ .../list/ImmutableListOperations.java | 18 -- .../tests/nonempty/list/ListOperations.java | 51 ---- .../nonempty/map/ImmutableMapOperations.java | 29 --- checker/tests/nonempty/map/MapOperations.java | 44 ---- .../postconditions/EnsuresNonEmptyIfTest.java | 33 --- .../postconditions/EnsuresNonEmptyTest.java | 22 -- .../preconditions/RequiresNonEmptyTest.java | 49 ---- .../nonempty/set/ImmutableSetOperations.java | 18 -- checker/tests/nonempty/set/SetOperations.java | 55 ----- checker/tests/nonempty/streams/Streams.java | 47 ---- .../common/delegation/DelegationChecker.java | 1 - .../framework/test/junit/DelegationTest.java | 21 ++ .../tests}/delegation/DelegatedCallTest.java | 6 +- .../DelegatedCallThrowsException.java | 6 +- .../delegation/InvalidDelegateTest.java | 2 +- .../delegation/MultiDelegationTest.java | 2 +- .../tests}/delegation/VoidDelegateTest.java | 2 +- 30 files changed, 28 insertions(+), 1180 deletions(-) delete mode 100644 checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java delete mode 100644 checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java delete mode 100644 checker/tests/nonempty/IndexOfNonNegative.java delete mode 100644 checker/tests/nonempty/Issue6407.java delete mode 100644 checker/tests/nonempty/NonEmptyHierarchyTest.java delete mode 100644 checker/tests/nonempty/PredicateTestMethod.java delete mode 100644 checker/tests/nonempty/SizeInIsEmpty.java delete mode 100644 checker/tests/nonempty/collections/UnmodifiableTest.java delete mode 100644 checker/tests/nonempty/iterator/IteratorOperations.java delete mode 100644 checker/tests/nonempty/list/Comparisons.java delete mode 100644 checker/tests/nonempty/list/ImmutableListOperations.java delete mode 100644 checker/tests/nonempty/list/ListOperations.java delete mode 100644 checker/tests/nonempty/map/ImmutableMapOperations.java delete mode 100644 checker/tests/nonempty/map/MapOperations.java delete mode 100644 checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java delete mode 100644 checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java delete mode 100644 checker/tests/nonempty/preconditions/RequiresNonEmptyTest.java delete mode 100644 checker/tests/nonempty/set/ImmutableSetOperations.java delete mode 100644 checker/tests/nonempty/set/SetOperations.java delete mode 100644 checker/tests/nonempty/streams/Streams.java create mode 100644 framework/src/test/java/org/checkerframework/framework/test/junit/DelegationTest.java rename {checker/tests/nonempty => framework/tests}/delegation/DelegatedCallTest.java (74%) rename {checker/tests/nonempty => framework/tests}/delegation/DelegatedCallThrowsException.java (79%) rename {checker/tests/nonempty => framework/tests}/delegation/InvalidDelegateTest.java (91%) rename {checker/tests/nonempty => framework/tests}/delegation/MultiDelegationTest.java (69%) rename {checker/tests/nonempty => framework/tests}/delegation/VoidDelegateTest.java (84%) diff --git a/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java index f59642d8a83..98381e8ea4c 100644 --- a/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java +++ b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java @@ -1,10 +1,8 @@ package org.checkerframework.common.delegation.qual; import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** * This is an annotation that indicates a field is a delegate, fields are not delegates by default. @@ -29,5 +27,4 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.FIELD}) public @interface Delegate {} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java index 216eaad3d50..f8bc39b7cf5 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java @@ -10,8 +10,8 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.nonempty.NonEmptyAnnotatedTypeFactory; import org.checkerframework.checker.optional.qual.Present; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; @@ -22,7 +22,7 @@ import org.checkerframework.javacutil.TreeUtils; /** OptionalAnnotatedTypeFactory for the Optional Checker. */ -public class OptionalAnnotatedTypeFactory extends NonEmptyAnnotatedTypeFactory { +public class OptionalAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The element for java.util.Optional.map(). */ private final ExecutableElement optionalMap; diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java index d800dd5c5b0..c2d8aca90fc 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java @@ -8,7 +8,6 @@ import com.sun.source.tree.IfTree; import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.ParenthesizedTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; @@ -18,23 +17,17 @@ import com.sun.source.util.TreePath; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; -import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.checker.nonempty.qual.RequiresNonEmpty; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.OptionalCreator; import org.checkerframework.checker.optional.qual.OptionalEliminator; @@ -44,12 +37,9 @@ import org.checkerframework.common.basetype.BaseTypeValidator; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.dataflow.expression.JavaExpression; -import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; -import org.checkerframework.javacutil.AnnotationMirrorSet; -import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; import org.plumelib.util.IPair; @@ -81,12 +71,6 @@ public class OptionalVisitor /** The element for java.util.stream.Stream.map(). */ private final ExecutableElement streamMap; - /** Set of methods to be checked by the Non-Empty Checker. */ - private final Set methodsForNonEmptyChecker; - - /** Map of the names of methods to the methods in which they are invoked. */ - private final Map> methodNamesToEnclosingMethods; - /** * Create an OptionalVisitor. * @@ -103,8 +87,6 @@ public OptionalVisitor(BaseTypeChecker checker) { streamFilter = TreeUtils.getMethod("java.util.stream.Stream", "filter", 1, env); streamMap = TreeUtils.getMethod("java.util.stream.Stream", "map", 1, env); - methodsForNonEmptyChecker = new HashSet<>(); - methodNamesToEnclosingMethods = new HashMap<>(); } @Override @@ -112,20 +94,6 @@ protected BaseTypeValidator createTypeValidator() { return new OptionalTypeValidator(checker, this, atypeFactory); } - /** - * Gets the set of methods that should be verified using the {@link - * org.checkerframework.checker.nonempty.NonEmptyChecker}. - * - *

This should only really be called by the Non-Empty Checker. - * - * @return the set of methods that should be verified using the {@link - * org.checkerframework.checker.nonempty.NonEmptyChecker} - */ - @Pure - public Set getMethodsForNonEmptyChecker() { - return methodsForNonEmptyChecker; - } - /** * Returns true iff {@code expression} is a call to java.util.Optional.get. * @@ -360,117 +328,9 @@ public void handleConditionalStatementIsPresentGet(IfTree tree) { public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { handleCreationElimination(tree); handleNestedOptionalCreation(tree); - updateMethodNamesToEnclosingMethods(tree); return super.visitMethodInvocation(tree, p); } - /** - * Updates {@link methodNamesToEnclosingMethods} given a method invocation. - * - *

Check whether the method is in the set of methods that must be checked by the Non-Empty - * checker whenever a method invocation is encountered. If the method is in the set, the method - * that immediately encloses the method invocation should also be added to the set of methods to - * be checked by the Non-Empty Checker. - * - *

This ensures that the clients of any methods that must be checked by the Non-Empty - * Checker (i.e., methods that have preconditions related to the Non-Empty type system) are - * included in the set of methods to check. - * - * @param tree a method invocation tree - */ - private void updateMethodNamesToEnclosingMethods(MethodInvocationTree tree) { - String invokedMethodName = tree.getMethodSelect().toString(); - MethodTree enclosingMethod = TreePathUtil.enclosingMethod(this.getCurrentPath()); - if (enclosingMethod != null) { - Set namesOfMethodsForNonEmptyChecker = - methodsForNonEmptyChecker.stream() - .map(MethodTree::getName) - .map(Name::toString) - .collect(Collectors.toSet()); - if (namesOfMethodsForNonEmptyChecker.contains(invokedMethodName)) { - methodNamesToEnclosingMethods.get(invokedMethodName).add(enclosingMethod); - } else { - Set enclosingMethodsForInvokedMethod = new HashSet<>(); - enclosingMethodsForInvokedMethod.add(enclosingMethod); - methodNamesToEnclosingMethods.put(invokedMethodName, enclosingMethodsForInvokedMethod); - } - } - } - - @Override - public void processMethodTree(MethodTree tree) { - if (this.isAnnotatedWithNonEmptyPrecondition(tree) - || this.isAnyFormalAnnotatedWithNonEmpty(tree)) { - updateMethodToCheckWithNonEmptyCheckerGivenPreconditions(tree); - } - if (this.isReturnTypeAnnotatedWithNonEmpty(tree)) { - methodsForNonEmptyChecker.add(tree); - } - super.processMethodTree(tree); - } - - /** - * Updates {@link methodsForNonEmptyChecker} when a method with a precondition from the Non-Empty - * type system (e.g., {@link RequiresNonEmpty}) or a formal annotated with {@link NonEmpty} is - * visited. - * - *

If the method being visited is in {@link methodNamesToEnclosingMethods}, the methods to - * check with the Non-Empty Checker should be updated with all the methods that dispatch calls to - * this method. - * - * @param tree a method tree - */ - private void updateMethodToCheckWithNonEmptyCheckerGivenPreconditions(MethodTree tree) { - String methodName = tree.getName().toString(); - if (methodNamesToEnclosingMethods.containsKey(methodName)) { - methodsForNonEmptyChecker.addAll(methodNamesToEnclosingMethods.get(methodName)); - } - methodsForNonEmptyChecker.add(tree); - } - - /** - * Returns true if a method is explicitly annotated with {@link RequiresNonEmpty}. - * - * @param tree a method tree - * @return true if a method is explicitly annotated with {@link RequiresNonEmpty} - */ - private boolean isAnnotatedWithNonEmptyPrecondition(MethodTree tree) { - return TreeUtils.annotationsFromTypeAnnotationTrees(tree.getModifiers().getAnnotations()) - .stream() - .anyMatch(am -> atypeFactory.areSameByClass(am, RequiresNonEmpty.class)); - } - - /** - * Returns true if any formal parameter of a method is explicitly annotated with {@link NonEmpty}. - * - * @param tree a method tree - * @return true if any formal parameter of a method is explicitly annotated with {@link NonEmpty} - */ - private boolean isAnyFormalAnnotatedWithNonEmpty(MethodTree tree) { - List params = tree.getParameters(); - AnnotationMirrorSet annotationMirrors = new AnnotationMirrorSet(); - for (VariableTree vt : params) { - annotationMirrors.addAll( - TreeUtils.annotationsFromTypeAnnotationTrees(vt.getModifiers().getAnnotations())); - } - return annotationMirrors.stream() - .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); - } - - /** - * Returns true if the return type of a method is explicitly annotated with {@link NonEmpty}. - * - * @param tree a method tree - * @return true if the return type of a method is explicitly annotated with {@link NonEmpty} - */ - private boolean isReturnTypeAnnotatedWithNonEmpty(MethodTree tree) { - if (tree.getReturnType() == null) { - return false; - } - return TreeUtils.typeOf(tree.getReturnType()).getAnnotationMirrors().stream() - .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); - } - @Override public Void visitBinary(BinaryTree tree, Void p) { handleCompareToNull(tree); @@ -612,7 +472,6 @@ public void handleNestedOptionalCreation(MethodInvocationTree tree) { */ @Override public Void visitVariable(VariableTree tree, Void p) { - handleNonEmptyVariableDeclaration(tree); VariableElement ve = TreeUtils.elementFromDeclaration(tree); TypeMirror tm = ve.asType(); if (isOptionalType(tm)) { @@ -632,18 +491,6 @@ public Void visitVariable(VariableTree tree, Void p) { return super.visitVariable(tree, p); } - private void handleNonEmptyVariableDeclaration(VariableTree tree) { - boolean isAnnotatedWithNonEmpty = - TreeUtils.annotationsFromTypeAnnotationTrees(tree.getModifiers().getAnnotations()).stream() - .anyMatch(am -> atypeFactory.areSameByClass(am, NonEmpty.class)); - if (isAnnotatedWithNonEmpty) { - MethodTree enclosingMethod = TreePathUtil.enclosingMethod(this.getCurrentPath()); - if (enclosingMethod != null) { - methodsForNonEmptyChecker.add(enclosingMethod); - } - } - } - /** * Handles Rule #5, part of Rule #6, and also Rule #7. * diff --git a/checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java b/checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java deleted file mode 100644 index b22e5a5497b..00000000000 --- a/checker/src/main/java/org/checkerframework/checker/optional/RevisedOptionalChecker.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.checkerframework.checker.optional; - -import java.util.Set; -import org.checkerframework.checker.nonempty.NonEmptyChecker; -import org.checkerframework.common.basetype.BaseTypeChecker; - -public class RevisedOptionalChecker extends BaseTypeChecker { - - @Override - protected Set> getImmediateSubcheckerClasses() { - Set> checkers = super.getImmediateSubcheckerClasses(); - checkers.add(NonEmptyChecker.class); - return checkers; - } -} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java deleted file mode 100644 index fa061b632e7..00000000000 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.checkerframework.checker.test.junit; - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -/** JUnit tests for the Non-Empty Checker */ -public class NonEmptyTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a NonEmptyTest. - * - * @param testFiles the files containing test code to be type-checked - */ - public NonEmptyTest(List testFiles) { - super(testFiles, org.checkerframework.checker.nonempty.NonEmptyChecker.class, "nonempty"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"nonempty"}; - } -} diff --git a/checker/tests/nonempty/IndexOfNonNegative.java b/checker/tests/nonempty/IndexOfNonNegative.java deleted file mode 100644 index 1a7b7a91537..00000000000 --- a/checker/tests/nonempty/IndexOfNonNegative.java +++ /dev/null @@ -1,83 +0,0 @@ -// @skip-test : contains() has a call to a locally-defined indexOf() method, which is hard to verify - -import java.util.AbstractSet; -import java.util.Collection; -import java.util.Iterator; -import org.checkerframework.checker.nonempty.qual.PolyNonEmpty; -import org.checkerframework.dataflow.qual.Pure; -import org.checkerframework.dataflow.qual.SideEffectFree; - -public class IndexOfNonNegative extends AbstractSet { - - @SideEffectFree - public IndexOfNonNegative() {} - - // Query Operations - - @Pure - @Override - public int size() { - return -1; - } - - @Pure - @Override - public boolean isEmpty() { - return size() == 0; - } - - @Pure - private int indexOf(Object value) { - return -1; - } - - @Pure - @Override - public boolean contains(Object value) { - // return indexOf(value) != -1; - if (indexOf(value) != -1) { - return true; - } else { - return false; - } - } - - // Modification Operations - - @Override - public boolean add(E value) { - return false; - } - - @Override - public boolean remove(Object value) { - return true; - } - - // Bulk Operations - - @Override - public boolean addAll(Collection c) { - return false; - } - - @Override - public boolean removeAll(Collection c) { - return true; - } - - // Inherit retainAll() from AbstractCollection. - - @Override - public void clear() {} - - /////////////////////////////////////////////////////////////////////////// - - // iterators - - @Override - // :: error: (override.receiver) - public @PolyNonEmpty Iterator iterator(@PolyNonEmpty IndexOfNonNegative this) { - throw new Error(""); - } -} diff --git a/checker/tests/nonempty/Issue6407.java b/checker/tests/nonempty/Issue6407.java deleted file mode 100644 index c52697ae253..00000000000 --- a/checker/tests/nonempty/Issue6407.java +++ /dev/null @@ -1,61 +0,0 @@ -import java.util.LinkedList; -import java.util.List; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; -import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.checker.nonempty.qual.UnknownNonEmpty; - -class Issue6407 { - - void usesJdk() { - // items initially has the type @UnknownNonEmpty - List items = new LinkedList<>(); - items.add("hello"); - @NonEmpty List bar = items; // OK - items.remove("hello"); - // :: error: (assignment) - @NonEmpty List baz = items; // I expect an error here - } - - static class MyList { - @SuppressWarnings("contracts.postcondition") // nonfunctional class - @EnsuresNonEmpty("this") - boolean add(E e) { - return true; - } - - boolean remove(@NonEmpty MyList this, E e) { - return true; - } - } - - boolean removeIt(@NonEmpty MyList myl, T e) { - return true; - } - - void noJdk() { - // items initially has the type @UnknownNonEmpty - @UnknownNonEmpty MyList items = new MyList<>(); - items.add("hello"); - @NonEmpty MyList bar = items; // OK - items.remove("hello"); - // :: error: (assignment) - @NonEmpty MyList baz = items; - } - - void noJdk2() { - // items initially has the type @UnknownNonEmpty - @UnknownNonEmpty MyList items = new MyList<>(); - items.add("hello"); - @NonEmpty MyList bar = items; // OK - removeIt(items, "hello"); - // :: error: (assignment) - @NonEmpty MyList baz = items; - } - - void initialRemoval() { - // items initially has the type @UnknownNonEmpty - MyList items = new MyList<>(); - // :: error: (method.invocation) - items.remove("hello"); - } -} diff --git a/checker/tests/nonempty/NonEmptyHierarchyTest.java b/checker/tests/nonempty/NonEmptyHierarchyTest.java deleted file mode 100644 index a68659d84d7..00000000000 --- a/checker/tests/nonempty/NonEmptyHierarchyTest.java +++ /dev/null @@ -1,15 +0,0 @@ -import java.util.List; -import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.checker.nonempty.qual.UnknownNonEmpty; - -class NonEmptyHierarchyTest { - - void testAssignments(@NonEmpty List l1, @UnknownNonEmpty List l2) { - @NonEmpty List l3 = l1; // OK, both are @NonEmpty - - // :: error: (assignment) - @NonEmpty List l4 = l2; - - List l5 = l1; // l5 implicitly has type @UnknownNonEmpty, assigning l1 to it is legal - } -} diff --git a/checker/tests/nonempty/PredicateTestMethod.java b/checker/tests/nonempty/PredicateTestMethod.java deleted file mode 100644 index 620edc7f260..00000000000 --- a/checker/tests/nonempty/PredicateTestMethod.java +++ /dev/null @@ -1,17 +0,0 @@ -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Predicate; - -class PredicateTestMethod { - - public static List filter1(Collection coll, Predicate filter) { - List result = new ArrayList<>(); - for (T elt : coll) { - if (filter.test(elt)) { - result.add(elt); - } - } - return result; - } -} diff --git a/checker/tests/nonempty/SizeInIsEmpty.java b/checker/tests/nonempty/SizeInIsEmpty.java deleted file mode 100644 index b1182723f0d..00000000000 --- a/checker/tests/nonempty/SizeInIsEmpty.java +++ /dev/null @@ -1,75 +0,0 @@ -import java.util.AbstractSet; -import java.util.Iterator; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; -import org.checkerframework.checker.nonempty.qual.PolyNonEmpty; -import org.checkerframework.dataflow.qual.Pure; -import org.checkerframework.dataflow.qual.SideEffectFree; - -public class SizeInIsEmpty extends AbstractSet { - - @SideEffectFree - public SizeInIsEmpty() {} - - // Query Operations - - @Pure - @Override - public int size() { - return -1; - } - - @Pure - @Override - @EnsuresNonEmptyIf(result = false, expression = "this") - public boolean isEmpty() { - if (size() == 0) { - return true; - } else { - return false; - } - } - - @EnsuresNonEmptyIf(result = false, expression = "this") - public boolean isEmpty2() { - return size() == 0 ? true : false; - } - - @EnsuresNonEmptyIf(result = false, expression = "this") - public boolean isEmpty3() { - return size() == 0; - } - - //// iterators - - @Override - public @PolyNonEmpty Iterator iterator(@PolyNonEmpty SizeInIsEmpty this) { - throw new Error(""); - } - - void testRefineIsEmpty1(SizeInIsEmpty container) { - if (!container.isEmpty()) { - container.iterator().next(); - } else { - // :: error: (method.invocation) - container.iterator().next(); - } - } - - void testRefineIsEmpty2(SizeInIsEmpty container) { - if (!container.isEmpty2()) { - container.iterator().next(); - } else { - // :: error: (method.invocation) - container.iterator().next(); - } - } - - void testRefineIsEmpty3(SizeInIsEmpty container) { - if (!container.isEmpty3()) { - container.iterator().next(); - } else { - // :: error: (method.invocation) - container.iterator().next(); - } - } -} diff --git a/checker/tests/nonempty/collections/UnmodifiableTest.java b/checker/tests/nonempty/collections/UnmodifiableTest.java deleted file mode 100644 index f0b832c38f4..00000000000 --- a/checker/tests/nonempty/collections/UnmodifiableTest.java +++ /dev/null @@ -1,51 +0,0 @@ -import static java.util.Map.entry; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class UnmodifiableTest { - - void unmodifiableCopy(@NonEmpty List strs) { - @NonEmpty List strsCopy = Collections.unmodifiableList(strs); // OK - } - - void checkNonEmptyThenCopy(List strs) { - if (strs.isEmpty()) { - // :: error: (method.invocation) - Collections.unmodifiableList(strs).iterator().next(); - } else { - Collections.unmodifiableList(strs).iterator().next(); // OK - } - } - - void testVarArgsEmpty() { - // :: error: (assignment) - @NonEmpty List items = List.of(); - } - - void testVarArgsNonEmptyList() { - // Requires more than 10 elements to invoke the varargs version - @NonEmpty List items = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); // OK - } - - void testVarArgsNonEmptyMap() { - // Requires more than 10 elements to invoke the varargs version - @NonEmpty - Map map = - Map.ofEntries( - entry("a", 1), - entry("b", 2), - entry("c", 3), - entry("d", 4), - entry("e", 5), - entry("f", 6), - entry("g", 7), - entry("h", 8), - entry("i", 9), - entry("j", 10), - entry("k", 11), - entry("l", 12)); - } -} diff --git a/checker/tests/nonempty/iterator/IteratorOperations.java b/checker/tests/nonempty/iterator/IteratorOperations.java deleted file mode 100644 index a862cbf959c..00000000000 --- a/checker/tests/nonempty/iterator/IteratorOperations.java +++ /dev/null @@ -1,75 +0,0 @@ -import java.util.Iterator; -import java.util.List; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class IteratorOperations { - - void testPolyNonEmptyIterator(List nums) { - // :: error: (method.invocation) - nums.iterator().next(); - - if (!nums.isEmpty()) { - @NonEmpty Iterator nonEmptyIterator = nums.iterator(); - nonEmptyIterator.next(); - } else { - // :: error: (assignment) - @NonEmpty Iterator unknownEmptyIterator = nums.iterator(); - } - } - - void testSwitchRefinementNoFallthrough(List nums) { - switch (nums.size()) { - case 0: - // :: error: (method.invocation) - nums.iterator().next(); - break; - case 1: - @NonEmpty List nums2 = nums; // OK - break; - default: - @NonEmpty List nums3 = nums; // OK - } - } - - void testSwitchRefinementWithFallthrough(List nums) { - switch (nums.size()) { - case 0: - // :: error: (method.invocation) - nums.iterator().next(); - case 1: - // :: error: (assignment) - @NonEmpty List nums2 = nums; - default: - // :: error: (assignment) - @NonEmpty List nums3 = nums; - } - } - - void testSwitchRefinementNoZero(List nums) { - switch (nums.size()) { - case 1: - nums.iterator().next(); - break; - default: - // :: error: (assignment) - @NonEmpty List nums3 = nums; - } - } - - void testSwitchRefinementIndexOf(List strs, String s) { - switch (strs.indexOf(s)) { - case -1: - // :: error: (method.invocation) - strs.iterator().next(); - break; - case 0: - @NonEmpty List strs2 = strs; - case 2: - case 3: - strs.iterator().next(); - break; - default: - @NonEmpty List strs3 = strs; - } - } -} diff --git a/checker/tests/nonempty/list/Comparisons.java b/checker/tests/nonempty/list/Comparisons.java deleted file mode 100644 index 9ad02ae857c..00000000000 --- a/checker/tests/nonempty/list/Comparisons.java +++ /dev/null @@ -1,226 +0,0 @@ -import java.util.List; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class Comparisons { - - /**** Tests for EQ ****/ - void testEqZeroWithReturn(List strs) { - if (strs.size() == 0) { - // :: error: (method.invocation) - strs.iterator().next(); - return; - } - strs.iterator().next(); // OK - } - - void testEqZeroFallthrough(List strs) { - if (strs.size() == 0) { - // :: error: (method.invocation) - strs.iterator().next(); - } - // :: error: (method.invocation) - strs.iterator().next(); - } - - void testEqNonZero(List strs) { - if (1 == strs.size()) { - strs.iterator().next(); - } else { - // :: error: (method.invocation) - strs.iterator().next(); - } - } - - void testImplicitNonZero(List strs1, List strs2) { - if (strs1.isEmpty()) { - return; - } - if (strs1.size() == strs2.size()) { - @NonEmpty List strs3 = strs2; // OK - } - // :: error: (assignment) - @NonEmpty List strs4 = strs2; - } - - void testEqualIndexOfRefinement(List objs, Object obj) { - if (objs.indexOf(obj) == -1) { - // :: error: (assignment) - @NonEmpty List objs2 = objs; - } else { - objs.iterator().next(); - } - } - - /**** Tests for NE ****/ - void t0(List strs) { - if (strs.size() != 0) { - strs.iterator().next(); - } - if (0 != strs.size()) { - strs.iterator().next(); - } - if (1 != strs.size()) { - // :: error: (method.invocation) - strs.iterator().next(); - } - } - - void testNotEqualsRefineElse(List strs1, List strs2) { - if (strs1.size() <= 0) { - return; - } - if (strs1.size() != strs2.size()) { - // :: error: (assignment) - @NonEmpty List strs3 = strs2; - } else { - @NonEmpty List strs4 = strs1; - @NonEmpty List strs5 = strs2; - } - } - - void testNotEqualsRefineIndexOf(List objs, Object obj) { - if (objs.indexOf(obj) != -1) { - @NonEmpty List objs2 = objs; - } else { - // :: error: (method.invocation) - objs.iterator().next(); - } - if (-1 != objs.indexOf(obj)) { - @NonEmpty List objs2 = objs; - } else { - // :: error: (method.invocation) - objs.iterator().next(); - } - } - - /**** Tests for GT ****/ - void t1(List strs) { - if (strs.size() > 10) { - strs.iterator().next(); - } else if (0 > strs.size()) { - // :: error: (method.invocation) - strs.iterator().next(); - } else if (100 > strs.size()) { - // :: error: (method.invocation) - strs.iterator().next(); - } - if (strs.size() > 0) { - strs.iterator().next(); - } else { - // :: error: (method.invocation) - strs.iterator().next(); - } - - if (0 > strs.size()) { - // :: error: (method.invocation) - strs.iterator().next(); - } else { - // :: error: (method.invocation) - strs.iterator().next(); - } - } - - void t2(List strs) { - if (strs.size() > -1) { - // :: error: (method.invocation) - strs.iterator().next(); - } - } - - void testRefineIndexOfGT(List objs, Object obj) { - if (objs.indexOf(obj) > -1) { - @NonEmpty List objs2 = objs; - } else { - // :: error: (method.invocation) - objs.iterator().next(); - } - } - - /**** Tests for GTE ****/ - void t3(List strs) { - if (strs.size() >= 0) { - // :: error: (method.invocation) - strs.iterator().next(); - } else if (strs.size() >= 1) { - strs.iterator().next(); - } - } - - void t4(List strs) { - if (0 >= strs.size()) { - // :: error: (method.invocation) - strs.iterator().next(); - } - } - - void testRefineGTEIndexOf(List strs, String s) { - if (strs.indexOf(s) >= 0) { - strs.iterator().next(); - } else { - // :: error: (assignment) - @NonEmpty List strs2 = strs; - } - } - - /**** Tests for LT ****/ - void t5(List strs) { - if (strs.size() < 10) { - // :: error: (method.invocation) - strs.iterator().next(); - } - if (strs.size() < 1) { - // :: error: (method.invocation) - strs.iterator().next(); - } else { - strs.iterator().next(); // OK - } - } - - void t6(List strs) { - if (0 < strs.size()) { - strs.iterator().next(); // Equiv. to strs.size() > 0 - } else { - // :: error: (method.invocation) - strs.iterator().next(); // Equiv. to strs.size() <= 0 - } - - if (strs.size() < 10) { - // Doesn't tell us a useful fact - // :: error: (method.invocation) - strs.iterator().next(); - } else { - strs.iterator().next(); - } - } - - /**** Tests for LTE ****/ - void t7(List strs) { - if (strs.size() <= 2) { - // :: error: (method.invocation) - strs.iterator().next(); - } - if (strs.size() <= 0) { - // :: error: (method.invocation) - strs.iterator().next(); - } else { - strs.iterator().next(); // OK, since strs must be non-empty - } - } - - void t8(List strs) { - if (1 <= strs.size()) { - strs.iterator().next(); - } else { - // :: error: (method.invocation) - strs.iterator().next(); - } - - if (0 <= strs.size()) { - // :: error: (method.invocation) - strs.iterator().next(); - } else { - // :: error: (method.invocation) - strs.iterator().next(); - } - } -} diff --git a/checker/tests/nonempty/list/ImmutableListOperations.java b/checker/tests/nonempty/list/ImmutableListOperations.java deleted file mode 100644 index 1fdb87ad008..00000000000 --- a/checker/tests/nonempty/list/ImmutableListOperations.java +++ /dev/null @@ -1,18 +0,0 @@ -import java.util.List; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class ImmutableListOperations { - - void testCreateEmptyImmutableList() { - List emptyInts = List.of(); - // Creating a copy of an empty list should also yield an empty list - // :: error: (assignment) - @NonEmpty List copyOfEmptyInts = List.copyOf(emptyInts); - } - - void testCreateNonEmptyImmutableList() { - List nonEmptyInts = List.of(1, 2, 3); - // Creating a copy of a non-empty list should also yield a non-empty list - @NonEmpty List copyOfNonEmptyInts = List.copyOf(nonEmptyInts); // OK - } -} diff --git a/checker/tests/nonempty/list/ListOperations.java b/checker/tests/nonempty/list/ListOperations.java deleted file mode 100644 index fe0956fec12..00000000000 --- a/checker/tests/nonempty/list/ListOperations.java +++ /dev/null @@ -1,51 +0,0 @@ -import java.util.ArrayList; -import java.util.List; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class ListOperations { - - // Skip test until we decide whether to handle accesses on empty containers - // void testGetOnEmptyList(List strs) { - // // :: error: (method.invocation) - // strs.get(0); - // } - - // Skip test until we decide whether to handle accesses on empty containers - // void testGetOnNonEmptyList(List strs) { - // if (strs.isEmpty()) { - // // :: error: (method.invocation) - // strs.get(0); - // } else { - // strs.get(0); // OK - // } - // } - - void testAddToEmptyListAndGet() { - List nums = new ArrayList<>(); - nums.add(1); // nums has type @NonEmpty after this line - nums.get(0); // OK - } - - void testAddAllWithEmptyList() { - List nums = new ArrayList<>(); - nums.addAll(List.of()); - // :: error: (assignment) - @NonEmpty List nums2 = nums; - } - - void testAddAllWithNonEmptyList() { - List nums = new ArrayList<>(); - if (nums.addAll(List.of(1, 2, 3))) { - @NonEmpty List nums2 = nums; // OK - } - } - - void testContains(List nums) { - if (nums.contains(11)) { - @NonEmpty List nums2 = nums; // OK - } - // :: error: (assignment) - @NonEmpty List nums2 = nums; - } - // TODO: consider other sequences (e.g., calling get(int) after clear()) -} diff --git a/checker/tests/nonempty/map/ImmutableMapOperations.java b/checker/tests/nonempty/map/ImmutableMapOperations.java deleted file mode 100644 index f097a4589f1..00000000000 --- a/checker/tests/nonempty/map/ImmutableMapOperations.java +++ /dev/null @@ -1,29 +0,0 @@ -import java.util.Map; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -// @skip-test until JDK is annotated with Non-Empty type qualifiers - -class ImmutableMapOperations { - - void emptyImmutableMap() { - Map emptyMap = Map.of(); - // :: error: (assignment) - @NonEmpty Map nonEmptyMap = emptyMap; - } - - void nonEmptyImmutableMap() { - Map nonEmptyMap = Map.of("Hello", 1); - @NonEmpty Map m1 = nonEmptyMap; - } - - void immutableCopyEmptyMap() { - Map emptyMap = Map.of(); - // :: error: (assignment) - @NonEmpty Map nonEmptyMap = Map.copyOf(emptyMap); - } - - void immutableCopyNonEmptyMap() { - Map nonEmptyMap = Map.of("Hello", 1, "World", 2); - @NonEmpty Map m2 = Map.copyOf(nonEmptyMap); - } -} diff --git a/checker/tests/nonempty/map/MapOperations.java b/checker/tests/nonempty/map/MapOperations.java deleted file mode 100644 index 63073baaf6a..00000000000 --- a/checker/tests/nonempty/map/MapOperations.java +++ /dev/null @@ -1,44 +0,0 @@ -import java.util.Map; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class MapOperations { - - // Skip test until we decide whether to handle accesses on empty containers - // void addToMapParam(Map m) { - // // :: error: (method.invocation) - // m.get("hello"); - - // m.put("hello", 1); - - // @NonEmpty Map m2 = m; // OK - // m.get("hello"); // OK - // } - - // Skip test until we decide whether to handle accesses on empty containers - // void clearMap(Map m) { - // m.put("hello", 1); - // m.get("hello"); // OK - - // m.clear(); - // // :: error: (method.invocation) - // m.get("hello"); - // } - - void containsKeyRefinement(Map m, String key) { - if (m.containsKey(key)) { - @NonEmpty Map m2 = m; // OK - } else { - // :: error: (assignment) - @NonEmpty Map m2 = m; // OK - } - } - - void containsValueRefinement(Map m, Integer value) { - if (m.containsValue(value)) { - @NonEmpty Map m2 = m; - } else { - // :: error: (assignment) - @NonEmpty Map m2 = m; - } - } -} diff --git a/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java b/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java deleted file mode 100644 index f1669b8b90e..00000000000 --- a/checker/tests/nonempty/postconditions/EnsuresNonEmptyIfTest.java +++ /dev/null @@ -1,33 +0,0 @@ -import java.util.ArrayList; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class EnsuresNonEmptyIfTest { - - @EnsuresNonEmptyIf(result = true, expression = "#1") - boolean m1(ArrayList l1) { - try { - l1.add("foo"); - return true; - } catch (Exception e) { - // As per the JDK documentation for Collections, an exception is thrown when adding to a - // collection fails - return false; - } - } - - void m2(@NonEmpty ArrayList l1) {} - - void test(ArrayList l1) { - // m2 requires a @NonEmpty collection, l1 has type @UnknownNonEmpty - // :: error: (argument) - m2(l1); - - if (!m1(l1)) { - // :: error: (argument) - m2(l1); - } else { - m2(l1); // OK - } - } -} diff --git a/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java b/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java deleted file mode 100644 index 1c60cae2576..00000000000 --- a/checker/tests/nonempty/postconditions/EnsuresNonEmptyTest.java +++ /dev/null @@ -1,22 +0,0 @@ -import java.util.ArrayList; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmpty; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class EnsuresNonEmptyTest { - - @EnsuresNonEmpty("#1") - void m1(ArrayList l1) { - l1.add("foo"); - } - - void m2(@NonEmpty ArrayList l1) {} - - void test(ArrayList l1) { - // m2 requires a @NonEmpty collection, l1 has type @UnknownNonEmpty - // :: error: (argument) - m2(l1); - - m1(l1); - m2(l1); // OK - } -} diff --git a/checker/tests/nonempty/preconditions/RequiresNonEmptyTest.java b/checker/tests/nonempty/preconditions/RequiresNonEmptyTest.java deleted file mode 100644 index b1fad95613b..00000000000 --- a/checker/tests/nonempty/preconditions/RequiresNonEmptyTest.java +++ /dev/null @@ -1,49 +0,0 @@ -import java.util.LinkedList; -import java.util.List; -import org.checkerframework.checker.nonempty.qual.NonEmpty; -import org.checkerframework.checker.nonempty.qual.RequiresNonEmpty; -import org.checkerframework.dataflow.qual.Pure; - -class MyClass { - - List list1 = new LinkedList<>(); - List list2; - - @RequiresNonEmpty("list1") - @Pure - void m1() {} - - @RequiresNonEmpty({"list1", "list2"}) - @Pure - void m2() {} - - @RequiresNonEmpty({"list1", "list2"}) - void m3() {} - - void m4() {} - - void test(@NonEmpty List l1, @NonEmpty List l2) { - MyClass testClass = new MyClass(); - - // At this point, we should have an error since m1 requires that list1 is @NonEmpty, which is - // not the case here - // :: error: (contracts.precondition) - testClass.m1(); - - testClass.list1 = l1; - testClass.m1(); // OK - - // A call to m2 is stil illegal here, since list2 is still @UnknownNonEmpty - // :: error: (contracts.precondition) - testClass.m2(); - - testClass.list2 = l2; - testClass.m2(); // OK - - testClass.m4(); - - // No longer OK to call m2, no guarantee that m4() was pure - // :: error: (contracts.precondition) - testClass.m2(); - } -} diff --git a/checker/tests/nonempty/set/ImmutableSetOperations.java b/checker/tests/nonempty/set/ImmutableSetOperations.java deleted file mode 100644 index 02c002cae50..00000000000 --- a/checker/tests/nonempty/set/ImmutableSetOperations.java +++ /dev/null @@ -1,18 +0,0 @@ -import java.util.Set; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class ImmutableSetOperations { - - void testCreateEmptyImmutableSet() { - Set emptyInts = Set.of(); - // Creating a copy of an empty set should also yield an empty set - // :: error: (assignment) - @NonEmpty Set copyOfEmptyInts = Set.copyOf(emptyInts); - } - - void testCreateNonEmptyImmutableSet() { - Set nonEmptyInts = Set.of(1, 2, 3); - // Creating a copy of a non-empty set should also yield a non-empty set - @NonEmpty Set copyOfNonEmptyInts = Set.copyOf(nonEmptyInts); - } -} diff --git a/checker/tests/nonempty/set/SetOperations.java b/checker/tests/nonempty/set/SetOperations.java deleted file mode 100644 index ca8af22822c..00000000000 --- a/checker/tests/nonempty/set/SetOperations.java +++ /dev/null @@ -1,55 +0,0 @@ -import java.util.HashSet; -import java.util.Set; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class SetOperations { - - void testIsEmpty(Set nums) { - if (nums.isEmpty()) { - // :: error: (assignment) - @NonEmpty Set nums2 = nums; - } else { - @NonEmpty Set nums3 = nums; // OK - } - } - - void testContains(Set nums) { - if (nums.contains(1)) { - @NonEmpty Set nums2 = nums; - } else { - // :: error: (assignment) - @NonEmpty Set nums3 = nums; - } - } - - void testAdd(Set nums) { - // :: error: (assignment) - @NonEmpty Set nums2 = nums; // No guarantee that the set is non-empty here - if (nums.add(1)) { - @NonEmpty Set nums3 = nums; - } - } - - void testAddAllEmptySet() { - Set nums = new HashSet<>(); - // :: error: (assignment) - @NonEmpty Set nums2 = nums; - if (nums.addAll(Set.of())) { - // Adding an empty set will always return false, this is effectively dead code - @NonEmpty Set nums3 = nums; - } else { - // :: error: (assignment) - @NonEmpty Set nums3 = nums; - } - } - - void testRemove() { - Set nums = new HashSet<>(); - nums.add(1); - @NonEmpty Set nums2 = nums; - nums.remove(1); - - // :: error: (assignment) - @NonEmpty Set nums3 = nums; - } -} diff --git a/checker/tests/nonempty/streams/Streams.java b/checker/tests/nonempty/streams/Streams.java deleted file mode 100644 index 57ba66e17dd..00000000000 --- a/checker/tests/nonempty/streams/Streams.java +++ /dev/null @@ -1,47 +0,0 @@ -import java.util.List; -import java.util.stream.Stream; -import org.checkerframework.checker.nonempty.qual.NonEmpty; - -class Streams { - - void testSingletonStreamCreation() { - @NonEmpty Stream strm = Stream.of(1); // OK - } - - void testStreamAnyMatch(Stream strStream) { - if (strStream.anyMatch(str -> str.length() > 10)) { - @NonEmpty Stream neStream = strStream; - } else { - // :: error: (assignment) - @NonEmpty Stream err = strStream; - } - } - - void testStreamAllMatch(Stream strStream) { - if (strStream.allMatch(str -> str.length() > 10)) { - @NonEmpty Stream neStream = strStream; - } else { - // :: error: (assignment) - @NonEmpty Stream err = strStream; - } - } - - void testMapNonEmptyStream(@NonEmpty List strs) { - @NonEmpty Stream lens = strs.stream().map(str -> str.length()); - } - - void testMapNonEmptyStream(Stream strs) { - // :: error: (assignment) - @NonEmpty Stream lens = strs.map(str -> str.length()); - } - - void testNoneMatch(Stream strs) { - if (strs.noneMatch(str -> str.length() < 10)) { - // :: error: (assignment) - @NonEmpty Stream err = strs; - } else { - // something matched; meaning that the stream MUST be non-empty - @NonEmpty Stream nonEmptyStrs = strs; - } - } -} diff --git a/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java b/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java index f7c1141bd4c..1a89b904dcf 100644 --- a/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java +++ b/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java @@ -48,7 +48,6 @@ public Visitor(DelegationChecker checker) { } @Override - @SuppressWarnings("UnusedVariable") public void processClassTree(ClassTree tree) { delegate = null; // Unset the previous delegate whenever a new class is visited // TODO: what about inner classes? diff --git a/framework/src/test/java/org/checkerframework/framework/test/junit/DelegationTest.java b/framework/src/test/java/org/checkerframework/framework/test/junit/DelegationTest.java new file mode 100644 index 00000000000..9ae76be9e63 --- /dev/null +++ b/framework/src/test/java/org/checkerframework/framework/test/junit/DelegationTest.java @@ -0,0 +1,21 @@ +package org.checkerframework.framework.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +public class DelegationTest extends CheckerFrameworkPerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public DelegationTest(List testFiles) { + super(testFiles, org.checkerframework.common.delegation.DelegationChecker.class, "delegation"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"delegation"}; + } +} diff --git a/checker/tests/nonempty/delegation/DelegatedCallTest.java b/framework/tests/delegation/DelegatedCallTest.java similarity index 74% rename from checker/tests/nonempty/delegation/DelegatedCallTest.java rename to framework/tests/delegation/DelegatedCallTest.java index 71a8e000f8d..5d9b4f3e0ca 100644 --- a/checker/tests/nonempty/delegation/DelegatedCallTest.java +++ b/framework/tests/delegation/DelegatedCallTest.java @@ -1,6 +1,5 @@ import java.util.IdentityHashMap; -import org.checkerframework.checker.nonempty.qual.Delegate; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; +import org.checkerframework.common.delegation.qual.*; public class DelegatedCallTest extends IdentityHashMap { @@ -19,7 +18,6 @@ public int size(DelegatedCallTest this) { } @Override - @EnsuresNonEmptyIf(result = false, expression = "this") public boolean isEmpty(DelegatedCallTest this) { return map.isEmpty(); } @@ -30,13 +28,11 @@ public V get(DelegatedCallTest this, Object key) { } @Override - @EnsuresNonEmptyIf(result = true, expression = "this") public boolean containsKey(DelegatedCallTest this, Object key) { return map.containsKey(key); } @Override - @EnsuresNonEmptyIf(result = true, expression = "this") public boolean containsValue(DelegatedCallTest this, Object value) { return map.containsValue(value); } diff --git a/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java b/framework/tests/delegation/DelegatedCallThrowsException.java similarity index 79% rename from checker/tests/nonempty/delegation/DelegatedCallThrowsException.java rename to framework/tests/delegation/DelegatedCallThrowsException.java index 206d5e367d9..1cd095ddc73 100644 --- a/checker/tests/nonempty/delegation/DelegatedCallThrowsException.java +++ b/framework/tests/delegation/DelegatedCallThrowsException.java @@ -1,7 +1,6 @@ import java.util.IdentityHashMap; import java.util.Map; -import org.checkerframework.checker.nonempty.qual.Delegate; -import org.checkerframework.checker.nonempty.qual.EnsuresNonEmptyIf; +import org.checkerframework.common.delegation.qual.*; public class DelegatedCallThrowsException extends IdentityHashMap { @@ -20,7 +19,6 @@ public int size(DelegatedCallThrowsException this) { } @Override - @EnsuresNonEmptyIf(result = false, expression = "this") public boolean isEmpty(DelegatedCallThrowsException this) { return map.isEmpty(); } @@ -31,13 +29,11 @@ public V get(DelegatedCallThrowsException this, Object key) { } @Override - @EnsuresNonEmptyIf(result = true, expression = "this") public boolean containsKey(DelegatedCallThrowsException this, Object key) { return map.containsKey(key); } @Override - @EnsuresNonEmptyIf(result = true, expression = "this") public boolean containsValue(DelegatedCallThrowsException this, Object value) { return map.containsValue(value); } diff --git a/checker/tests/nonempty/delegation/InvalidDelegateTest.java b/framework/tests/delegation/InvalidDelegateTest.java similarity index 91% rename from checker/tests/nonempty/delegation/InvalidDelegateTest.java rename to framework/tests/delegation/InvalidDelegateTest.java index d12c68dead3..28916743f20 100644 --- a/checker/tests/nonempty/delegation/InvalidDelegateTest.java +++ b/framework/tests/delegation/InvalidDelegateTest.java @@ -1,5 +1,5 @@ import java.util.IdentityHashMap; -import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.common.delegation.qual.*; // :: warning: (delegate.override) public class InvalidDelegateTest extends IdentityHashMap { diff --git a/checker/tests/nonempty/delegation/MultiDelegationTest.java b/framework/tests/delegation/MultiDelegationTest.java similarity index 69% rename from checker/tests/nonempty/delegation/MultiDelegationTest.java rename to framework/tests/delegation/MultiDelegationTest.java index 8020b2640fe..584f1bcef2d 100644 --- a/checker/tests/nonempty/delegation/MultiDelegationTest.java +++ b/framework/tests/delegation/MultiDelegationTest.java @@ -1,4 +1,4 @@ -import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.common.delegation.qual.*; class MultiDelegationTest { diff --git a/checker/tests/nonempty/delegation/VoidDelegateTest.java b/framework/tests/delegation/VoidDelegateTest.java similarity index 84% rename from checker/tests/nonempty/delegation/VoidDelegateTest.java rename to framework/tests/delegation/VoidDelegateTest.java index 593dc6d2e03..b12a053eaf7 100644 --- a/checker/tests/nonempty/delegation/VoidDelegateTest.java +++ b/framework/tests/delegation/VoidDelegateTest.java @@ -1,5 +1,5 @@ import java.util.ArrayList; -import org.checkerframework.checker.nonempty.qual.Delegate; +import org.checkerframework.common.delegation.qual.*; // :: warning: (delegate.override) public class VoidDelegateTest extends ArrayList { From 5c658417fb1b61b4b6beb0ea237620fb6f58b8b2 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 16:19:02 -0700 Subject: [PATCH 106/110] Remove unrelated changes --- .../InitializationAnnotatedTypeFactory.java | 4 +- .../dataflow/expression/JavaExpression.java | 61 ------ docs/manual/figures/nonempty-subtyping.svg | 157 --------------- docs/manual/introduction.tex | 3 - docs/manual/manual.tex | 1 - docs/manual/non-empty-checker.tex | 186 ------------------ .../common/basetype/BaseTypeVisitor.java | 1 + .../checkerframework/javacutil/TreeUtils.java | 14 -- 8 files changed, 3 insertions(+), 424 deletions(-) delete mode 100644 docs/manual/figures/nonempty-subtyping.svg delete mode 100644 docs/manual/non-empty-checker.tex diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java index 0820ee90b73..db848a02e13 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java @@ -582,7 +582,7 @@ public IPair, List> getUninitializedFields( boolean isStatic, Collection receiverAnnotations) { ClassTree currentClass = TreePathUtil.enclosingClass(path); - List fields = TreeUtils.fieldsFromTree(currentClass); + List fields = InitializationChecker.getAllFields(currentClass); List uninitWithInvariantAnno = new ArrayList<>(); List uninitWithoutInvariantAnno = new ArrayList<>(); for (VariableTree field : fields) { @@ -635,7 +635,7 @@ public List getInitializedInvariantFields(Store store, TreePath pa // TODO: Instead of passing the TreePath around, can we use // getCurrentClassTree? ClassTree currentClass = TreePathUtil.enclosingClass(path); - List fields = TreeUtils.fieldsFromTree(currentClass); + List fields = InitializationChecker.getAllFields(currentClass); List initializedFields = new ArrayList<>(); for (VariableTree field : fields) { VariableElement fieldElem = TreeUtils.elementFromDeclaration(field); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java index 86fb2802a40..91ce61ce720 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -2,7 +2,6 @@ import com.sun.source.tree.ArrayAccessTree; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LiteralTree; @@ -11,7 +10,6 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; import com.sun.source.tree.VariableTree; @@ -931,63 +929,4 @@ private static boolean isVarArgsInvocation( return TypesUtils.getArrayDepth(ElementUtils.getType(lastParamElt)) != TypesUtils.getArrayDepth(lastArgType); } - - /** - * Find the declaration of the receiver of a method call in a method tree. - * - *

The receiver should appear in one of two places, either as a formal parameter to the method, - * or as a local variable. - * - * @param tree the method tree - * @param receiver the receiver for which to look up a declaration - * @return the declaration of the receiver of the method call, if found. Otherwise, null - */ - public static @Nullable VariableTree getReceiverDeclarationInMethod( - @Nullable MethodTree tree, JavaExpression receiver) { - if (tree == null) { - return null; - } - List params = tree.getParameters(); - for (VariableTree param : params) { - if (param.getName().toString().equals(receiver.toString())) { - return param; - } - } - for (StatementTree statement : tree.getBody().getStatements()) { - if (statement instanceof VariableTree) { - VariableTree localVariableTree = (VariableTree) statement; - if (localVariableTree.getName().toString().equals(receiver.toString())) { - return localVariableTree; - } - } - } - return null; - } - - /** - * Find the declaration of the receiver of a method call in a method tree. - * - *

The receiver should appear as a field in the class, if found. - * - *

TODO: what about inherited fields? - * - * @param tree the class tree - * @param receiver the receiver for which to look up a declaration - * @return the declaration of the receiver of the method call, if found. Otherwise, null - */ - public static @Nullable VariableTree getReceiverDeclarationInClass( - @Nullable ClassTree tree, JavaExpression receiver) { - if (tree == null || tree.getMembers().isEmpty()) { - return null; - } - for (Tree member : tree.getMembers()) { - if (member instanceof VariableTree) { - VariableTree field = (VariableTree) member; - if (JavaExpression.fromVariableTree(field).containsSyntacticEqualJavaExpression(receiver)) { - return field; - } - } - } - return null; - } } diff --git a/docs/manual/figures/nonempty-subtyping.svg b/docs/manual/figures/nonempty-subtyping.svg deleted file mode 100644 index 84314d3700c..00000000000 --- a/docs/manual/figures/nonempty-subtyping.svg +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - @UnknownNonEmpty - - - - - @NonEmpty - - - - - - - - - diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 8e423db4170..9f1f66d9787 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -49,9 +49,6 @@ \item \ahrefloc{index-checker}{Index Checker} for array accesses (see \chapterpageref{index-checker}) -\item - \ahrefloc{non-empty-checker}{Non-Empty Checker} to determine whether a - collection, iterator, iterable, or map is non-empty (see \chapterpageref{non-empty-checker}) \item \ahrefloc{regex-checker}{Regex Checker} to prevent use of syntactically invalid regular expressions (see \chapterpageref{regex-checker}) diff --git a/docs/manual/manual.tex b/docs/manual/manual.tex index fc0c4ad34bb..4d3de0321d6 100644 --- a/docs/manual/manual.tex +++ b/docs/manual/manual.tex @@ -58,7 +58,6 @@ \input{tainting-checker.tex} \input{lock-checker.tex} \input{index-checker.tex} -\input{non-empty-checker.tex} % These are focused on strings: \input{regex-checker.tex} diff --git a/docs/manual/non-empty-checker.tex b/docs/manual/non-empty-checker.tex deleted file mode 100644 index 90931118f84..00000000000 --- a/docs/manual/non-empty-checker.tex +++ /dev/null @@ -1,186 +0,0 @@ -\htmlhr -\chapterAndLabel{Non-Empty Checker for container classes}{non-empty-checker} - -The Non-Empty Checker tracks whether a container is possibly-empty or is -definitely non-empty. It works on containers such as -\s, \s, \s, and \s. - -If the Non-Empty Checker issues no warnings, then your program does not -throw \ as a result of calling methods such as -\, \, \, or -\. - -To run the Non-Empty Checker, run either of these commands: - -\begin{alltt} - javac -processor nonempty \emph{MyJavaFile}.java - javac -processor org.checkerframework.checker.nonempty.NonEmptyChecker \emph{MyJavaFile}.java -\end{alltt} - -\sectionAndLabel{Non-Empty annotations}{non-empty-annotations} - -These qualifiers make up the Non-Empty type system: - -\begin{description} - -\item[\refqualclass{checker/nonempty/qual}{UnknownNonEmpty}] - The annotated collection, iterator, iterable, or map may or may not be empty. - This is the top type; programmers need not explicitly write it. - -\item[\refqualclass{checker/nonempty/qual}{NonEmpty}] - The annotated collection, iterator, iterable, or map is \emph{definitely} - non-empty. - -\item[\refqualclass{checker/nonempty/qual}{PolyNonEmpty}] - indicates qualifier polymorphism. - For a description of qualifier polymorphism, see - Section~\ref{method-qualifier-polymorphism}. - -\end{description} - -\begin{figure} -\includeimage{nonempty-subtyping}{3.75cm} -\caption{The subtyping relationship of the Non-Empty Checker's qualifiers.} -\label{fig-nonempty-hierarchy} -\end{figure} - -\subsectionAndLabel{Non-Empty method annotations}{non-empty-method-annotations} - -The Non-Empty Checker supports several annotations that specify method -behavior. These are declaration annotations, not type annotations; they -apply to the annotated method itself rather than to some particular type. - -\begin{description} - -\item[\refqualclass{checker/nonempty/qual}{RequiresNonEmpty}] - indicates a method precondition. The annotated method expects the - specified expresssion to be non-empty when the - method is invoked. \<@RequiresNonEmpty> may be appropriate for - a field that may not always be non-empty, but the annotated method requires - the field to be non-empty. - -\item[\refqualclass{checker/nonempty/qual}{EnsuresNonEmpty}] - indicates a method postcondition. The successful return (i.e., a - non-exceptional return) of the annotated method results in the given - expression being non-empty. See the Javadoc for examples of its use. - -\item[\refqualclass{checker/nonempty/qual}{EnsuresNonEmptyIf}] - indicates a method postcondition. With \<@EnsuresNonEmpty>, the given - expression is non-empty after the method returns normally. With - \<@EnsuresNonEmptyIf>, if the annotated - method returns the given boolean value (true or false), then the given - expression is non-empty. See the Javadoc for examples of their use. - -\end{description} - -\subsectionAndLabel{Delegation qualifiers}{delegation-qualifiers-overview} - -The Non-Empty Checker invokes an Delegation Checker, -TODO: whose annotations indicate whether an object is ... - -\begin{description} -\item[\refqualclass{checker/delegate/qual}{UnknownDelegate}] -\item[\refqualclass{checker/delegate/qual}{Delegate}] -\item[\refqualclass{checker/delegate/qual}{DelegateBottom}] -\end{description} - -\noindent -Use of these annotations can help you to type-check more code. -TODO: add link to section on Delegation Checker - -\sectionAndLabel{Annotating your code with \<@NonEmpty>}{annotating-with-non-empty} - -The default annotation for collections, iterators, iterables, and maps is -\<@UnknownNonEmpty>. -Refinement to the \<@NonEmpty> type occurs in certain cases, such as after -conditional checks for empty/non-emptiness (see~\ref{type-refinement} for -more details): - -\begin{Verbatim} - public List getSessionIds() { ... } - ... - List sessionIds = getSessionIds(); // sessionIds has type @UnknownNonEmpty - ... - if (!sessionIds.isEmpty()) { - sessionIds.iterator().next(); // OK, sessionIds has type @NonEmpty - ... - } -\end{Verbatim} - -Or on the result of a method that returns a non-empty collection: - -\begin{Verbatim} - List countryCodes; // Has default type @UnknownNonEmpty - List countryCodes = List.of("CA", "US"); // Has type @NonEmpty -\end{Verbatim} - -A programmer can manually annotate code in cases where a collection, -iterator, iterable, or map is always known to be non-empty, but that fact is -unable to be inferred by the type system: - -\begin{Verbatim} - // This call always returns a non-empty map; there is always at least one user in the store - public @NonEmpty Map getUserMapping() { ... } - ... - Map users = getUserMapping(); // users has type @NonEmpty -\end{Verbatim} - -\sectionAndLabel{What the Non-Empty Checker checks}{non-empty-checker-checks} - -The Non-Empty Checker ensures that collections, iterators, iterables, or maps -are non-empty at certain points in a program. -If a program type-checks cleanly under the Non-Empty Checker (i.e., no errors -are issued by the checker), then the program is certified with a compile-time -guarantee of the absence of errors rooted in the use of operations that -rely on whether a collection is non-empty. -For example, calling \ on an iterator that is known to be \<@NonEmpty> -should never fail, or, getting the first element of a \<@NonEmpty> list should -not throw an exception. - -The Non-Empty Checker does \emph{not} provide guarantees about the fixed -length or size of collections, iterators, iterables, or maps, beyond whether -it has a length or size of at least 1 (i.e., it is non-empty). -The Index Checker~(See Chapter~\ref{index-checker}) is a checker that analyzes -array bounds and indices and warns about potential -\s. - -\sectionAndLabel{Suppressing non-empty warnings}{suppressing-non-empty-warnings} - -Like any sound static analysis tool, the Non-Empty Checker may issue a warning -for code that is correct. -It is often best to change your code or annotations in this case. -Alternatively, you may choose to suppress the warning. -This does not change the code, but prevents the warning from being presented to -you. -The Checker Framework supplies several mechanisms to suppress warnings. -See Chapter~\ref{suppressing-warnings} for additional usages. -The \<@SuppressWarnings("nonempty")> annotation is specific to warnings raised -by the Non-Empty Checker: - -\begin{Verbatim} - // This method might return an empty list, depending on the argument - List getRegionIds(String region) { ... } - - void parseRegions() { - @SuppressWarnings("nonempty") // A non-empty list is returned when getRegionIds is invoked with argument x - @NonEmpty List regionIds = getRegionIds(x); - } -\end{Verbatim} - -\subsectionAndLabel{Suppressing warnings with assertions}{suppressing-warnings-with-assertions} - -Occasionally, it is inconvenient or verbose to use the \<@SuppressWarnings> -annotation. -For example, Java does not permit annotations such as \<@SuppressWarnings> to -appear on expressions, static initializers, etc. -Here are two ways to suppress a warning in such cases: - -\begin{itemize} -\item - Create a local variable to hold a subexpression, and - suppress a warning on the local variable declaration. -\item - Use the \<@AssumeAssertion> string in - an \ message (see Section~\ref{assumeassertion}). -\end{itemize} - 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 cf8de531a1e..70741b94fc3 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -974,6 +974,7 @@ public final Void visitMethod(MethodTree tree, Void p) { * @param tree the method to type-check */ public void processMethodTree(MethodTree tree) { + // We copy the result from getAnnotatedType to ensure that circular types (e.g. K extends // Comparable) are represented by circular AnnotatedTypeMirrors, which avoids problems // with later checks. diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 149d92b62df..57855fdc2f0 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -75,7 +75,6 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; -import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; @@ -334,19 +333,6 @@ public static boolean isSelfAccess(ExpressionTree tree) { return elementFromDeclaration(tree); } - /** - * Returns the fields that are declared within the given class declaration. - * - * @param tree the {@link ClassTree} node to get the fields for - * @return the list of fields that are declared within the given class declaration - */ - public static List fieldsFromTree(ClassTree tree) { - return tree.getMembers().stream() - .filter(t -> t.getKind() == Kind.VARIABLE) - .map(t -> (VariableTree) t) - .collect(Collectors.toList()); - } - /** * Returns the element corresponding to the given tree. * From afe12ccef51789e878747d74d43cd0464ad01cb8 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 16:21:58 -0700 Subject: [PATCH 107/110] Remove additional unrelated code --- .../framework/flow/CFAbstractStore.java | 7 +------ .../framework/flow/CFAbstractTransfer.java | 18 ------------------ .../poly/AbstractQualifierPolymorphism.java | 4 ++-- .../checkerframework/javacutil/TreeUtils.java | 1 - 4 files changed, 3 insertions(+), 27 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index edb747ef98f..607faae3787 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -600,12 +600,7 @@ protected boolean shouldInsert( protected void insertValue( JavaExpression expr, @Nullable V value, boolean permitNondeterministic) { computeNewValueAndInsert( - expr, - value, - (old, newValue) -> { - return newValue.mostSpecific(old, null); - }, - permitNondeterministic); + expr, value, (old, newValue) -> newValue.mostSpecific(old, null), permitNondeterministic); } /** 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 43f969b624c..2c67a76885a 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java @@ -1448,22 +1448,4 @@ protected static void insertIntoStores( result.getRegularStore().insertValue(target, newAnno); } } - - /** - * Inserts {@code newAnno} into all stores (conditional or not) in the result for node, while - * permitting non-determinism. This is a utility method for subclasses. - * - * @param result the {@link TransferResult} holding the stores to modify - * @param target the receiver whose values should be modified - * @param newAnno the new value - */ - protected static void insertIntoStoresPermitNonDeterministic( - TransferResult result, JavaExpression target, AnnotationMirror newAnno) { - if (result.containsTwoStores()) { - result.getThenStore().insertValuePermitNondeterministic(target, newAnno); - result.getElseStore().insertValuePermitNondeterministic(target, newAnno); - } else { - result.getRegularStore().insertValuePermitNondeterministic(target, newAnno); - } - } } diff --git a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java index d3b5722e57e..2bda809ef9e 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java +++ b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java @@ -113,9 +113,9 @@ protected AbstractQualifierPolymorphism(ProcessingEnvironment env, AnnotatedType AnnotationMirror top = entry.getValue(); if (type.hasPrimaryAnnotation(poly)) { type.removePrimaryAnnotation(poly); - // Do not add qualifiers to type variables and wildcards. if (type.getKind() != TypeKind.TYPEVAR && type.getKind() != TypeKind.WILDCARD) { - // It's not a type variable or wildcard. + // Do not add qualifiers to type variables and + // wildcards type.addAnnotation(this.qualHierarchy.getBottomAnnotation(top)); } } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 57855fdc2f0..137b6496394 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -1020,7 +1020,6 @@ public static boolean isCompileTimeString(ExpressionTree tree) { * @return the expression's receiver tree, or null if it does not have an explicit receiver */ public static @Nullable ExpressionTree getReceiverTree(ExpressionTree expression) { - expression = TreeUtils.withoutParens(expression); ExpressionTree receiver; switch (expression.getKind()) { case METHOD_INVOCATION: From 87f8e0e0815abdf102f05534433afcd348ca985a Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 16:23:29 -0700 Subject: [PATCH 108/110] Remove manual entry --- .../dataflow/expression/JavaExpression.java | 18 ------------------ docs/manual/advanced-features.tex | 3 --- 2 files changed, 21 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java index 91ce61ce720..73f351f52ca 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -734,24 +734,6 @@ public static JavaExpression getReceiver(ExpressionTree accessTree) { } } - /** - * Returns the initial receiver of a method invocation. - * - *

For example, for a given method invocation sequence {@code a.b().c.d().e()}, return {@code - * a}. - * - * @param tree a tree - * @return the initial receiver of a method invocation - */ - public static JavaExpression getInitialReceiverOfMethodInvocation(ExpressionTree tree) { - assert tree instanceof MethodInvocationTree; - ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree); - while (receiverTree instanceof MethodInvocationNode) { - receiverTree = TreeUtils.getReceiverTree(receiverTree); - } - return JavaExpression.fromTree(receiverTree); - } - /** * Returns the implicit receiver of ele. * diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 27ea110b960..11722fc7638 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -919,9 +919,6 @@ \item \ahrefloc{index-checker}{Index Checker} for array accesses (see \chapterpageref{index-checker}) -\item - \ahrefloc{non-empty-checker}{Non-Empty Checker} to determine whether a - collection, iterator, iterable, or map is non-empty (see \chapterpageref{non-empty-checker}) \item \ahrefloc{resource-leak-checker}{Resource Leak Checker} for ensuring that resources are disposed of properly (see \chapterpageref{resource-leak-checker}) From 36ebf2ee5c749c2f757fe8c7f5734ba3bd59971e Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 16:27:31 -0700 Subject: [PATCH 109/110] Add back `TreeUtils.fieldsFromTree` --- .../org/checkerframework/javacutil/TreeUtils.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 137b6496394..77dc0c7b160 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -75,6 +75,7 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; +import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; @@ -333,6 +334,19 @@ public static boolean isSelfAccess(ExpressionTree tree) { return elementFromDeclaration(tree); } + /** + * Returns the fields that are declared within the given class declaration. + * + * @param tree the {@link ClassTree} node to get the fields for + * @return the list of fields that are declared within the given class declaration + */ + public static List fieldsFromTree(ClassTree tree) { + return tree.getMembers().stream() + .filter(t -> t.getKind() == Kind.VARIABLE) + .map(t -> (VariableTree) t) + .collect(Collectors.toList()); + } + /** * Returns the element corresponding to the given tree. * From 06d74b12717b3dcdeb0e17b3532fca1115234f67 Mon Sep 17 00:00:00 2001 From: James Yoo Date: Sun, 19 May 2024 16:47:38 -0700 Subject: [PATCH 110/110] Extracting delegation checking logic into `DelegationVisitor` --- .../common/delegation/qual/Delegate.java | 3 + .../DelegationAnnotatedTypeFactory.java | 22 ++ .../common/delegation/DelegationChecker.java | 236 +--------------- .../common/delegation/DelegationVisitor.java | 253 ++++++++++++++++++ 4 files changed, 279 insertions(+), 235 deletions(-) create mode 100644 framework/src/main/java/org/checkerframework/common/delegation/DelegationAnnotatedTypeFactory.java create mode 100644 framework/src/main/java/org/checkerframework/common/delegation/DelegationVisitor.java diff --git a/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java index 98381e8ea4c..f59642d8a83 100644 --- a/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java +++ b/checker-qual/src/main/java/org/checkerframework/common/delegation/qual/Delegate.java @@ -1,8 +1,10 @@ package org.checkerframework.common.delegation.qual; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * This is an annotation that indicates a field is a delegate, fields are not delegates by default. @@ -27,4 +29,5 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) public @interface Delegate {} diff --git a/framework/src/main/java/org/checkerframework/common/delegation/DelegationAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/common/delegation/DelegationAnnotatedTypeFactory.java new file mode 100644 index 00000000000..9d03213f98d --- /dev/null +++ b/framework/src/main/java/org/checkerframework/common/delegation/DelegationAnnotatedTypeFactory.java @@ -0,0 +1,22 @@ +package org.checkerframework.common.delegation; + +import java.lang.annotation.Annotation; +import java.util.Set; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.delegation.qual.Delegate; + +/** Annotated type factory for the Delegation Checker. */ +public class DelegationAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** Create the type factory. */ + public DelegationAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return getBundledTypeQualifiers(Delegate.class); + } +} diff --git a/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java b/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java index 1a89b904dcf..ece943c7b6d 100644 --- a/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java +++ b/framework/src/main/java/org/checkerframework/common/delegation/DelegationChecker.java @@ -2,20 +2,9 @@ import com.sun.source.tree.*; import java.util.*; -import java.util.stream.Collectors; import javax.lang.model.element.*; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.util.ElementFilter; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.common.delegation.qual.Delegate; -import org.checkerframework.common.delegation.qual.DelegatorMustOverride; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** * This class enforces checks for the {@link Delegate} annotation. @@ -28,227 +17,4 @@ *

  • A class overrides all methods declared in its superclass. * */ -public class DelegationChecker extends BaseTypeChecker { - - @Override - protected BaseTypeVisitor createSourceVisitor() { - return new Visitor(this); - } - - static class Visitor extends BaseTypeVisitor { - - /** The maximum number of fields marked with {@link Delegate} permitted in a class. */ - private final int MAX_NUM_DELEGATE_FIELDS = 1; - - /** The field marked with {@link Delegate} for the current class. */ - private @Nullable VariableTree delegate; - - public Visitor(DelegationChecker checker) { - super(checker); - } - - @Override - public void processClassTree(ClassTree tree) { - delegate = null; // Unset the previous delegate whenever a new class is visited - // TODO: what about inner classes? - List delegates = getDelegateFields(tree); - if (delegates.size() > MAX_NUM_DELEGATE_FIELDS) { - VariableTree latestDelegate = delegates.get(delegates.size() - 1); - checker.reportError(latestDelegate, "multiple.delegate.annotations"); - } else if (delegates.size() == 1) { - delegate = delegates.get(0); - checkSuperClassOverrides(tree); - } - // Do nothing if no delegate field is found - super.processClassTree(tree); - } - - @Override - public void processMethodTree(MethodTree tree) { - super.processMethodTree(tree); - if (delegate == null || !isMarkedWithOverride(tree)) { - return; - } - MethodInvocationTree candidateDelegateCall = getLastExpression(tree.getBody()); - boolean hasExceptionalExit = - hasExceptionalExit(tree.getBody(), UnsupportedOperationException.class); - if (hasExceptionalExit) { - return; - } - if (candidateDelegateCall == null) { - checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); - return; - } - Name enclosingMethodName = tree.getName(); - if (!isValidDelegateCall(enclosingMethodName, candidateDelegateCall)) { - checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); - } - } - - /** - * Return true if the given method call is a valid delegate call for the enclosing method. - * - *

    A delegate method call must fulfill the following properties: its receiver must be the - * current field marked with {@link Delegate} in the class, and the name of the method call must - * match that of the enclosing method. - * - * @param enclosingMethodName the name of the enclosing method - * @param delegatedMethodCall the delegated method call - * @return true if the given method call is a valid delegate call for the enclosing method - */ - private boolean isValidDelegateCall( - Name enclosingMethodName, MethodInvocationTree delegatedMethodCall) { - assert delegate != null; // This method should only be invoked when delegate is non-null - ExpressionTree methodSelectTree = delegatedMethodCall.getMethodSelect(); - MemberSelectTree fieldAccessTree = (MemberSelectTree) methodSelectTree; - VariableElement delegatedField = TreeUtils.asFieldAccess(fieldAccessTree.getExpression()); - Name delegatedMethodName = TreeUtils.methodName(delegatedMethodCall); - // TODO: is there a better way to check? Comparing names seems fragile. - return enclosingMethodName.equals(delegatedMethodName) - && delegatedField != null - && delegatedField.getSimpleName().equals(delegate.getName()); - } - - /** - * Returns the fields of a class marked with a {@link Delegate} annotation. - * - * @param tree a class - * @return the fields of a class marked with a {@link Delegate} annotation - */ - private List getDelegateFields(ClassTree tree) { - List delegateFields = new ArrayList<>(); - for (VariableTree field : TreeUtils.fieldsFromTree(tree)) { - List annosOnField = - TreeUtils.annotationsFromTypeAnnotationTrees(field.getModifiers().getAnnotations()); - if (annosOnField.stream() - .anyMatch(anno -> atypeFactory.areSameByClass(anno, Delegate.class))) { - delegateFields.add(field); - } - } - return delegateFields; - } - - /** - * Returns the last expression in a method body. - * - *

    This method is used to identify a possible delegate method call. It will check whether a - * method has only one statement (a method invocation or a return statement), and return the - * expression that is associated with it. Otherwise, it will return null. - * - * @param tree the method body - * @return the last expression in the method body - */ - private @Nullable MethodInvocationTree getLastExpression(BlockTree tree) { - List stmts = tree.getStatements(); - if (stmts.size() != 1) { - return null; - } - StatementTree stmt = stmts.get(0); - ExpressionTree lastExprInMethod = null; - if (stmt instanceof ExpressionStatementTree) { - lastExprInMethod = ((ExpressionStatementTree) stmt).getExpression(); - } else if (stmt instanceof ReturnTree) { - lastExprInMethod = ((ReturnTree) stmt).getExpression(); - } - if (!(lastExprInMethod instanceof MethodInvocationTree)) { - return null; - } - return (MethodInvocationTree) lastExprInMethod; - } - - /** - * Return true if the last (and only) statement of the block throws an exception of the given - * class. - * - * @param tree a block tree - * @param clazz a class of exception (usually {@link UnsupportedOperationException}) - * @return true if the last and only statement throws an exception of the given class - */ - private boolean hasExceptionalExit(BlockTree tree, Class clazz) { - List stmts = tree.getStatements(); - if (stmts.size() != 1) { - return false; - } - StatementTree lastStmt = stmts.get(0); - if (!(lastStmt instanceof ThrowTree)) { - return false; - } - ThrowTree throwStmt = (ThrowTree) lastStmt; - AnnotatedTypeMirror throwType = atypeFactory.getAnnotatedType(throwStmt.getExpression()); - Class exceptionClass = TypesUtils.getClassFromType(throwType.getUnderlyingType()); - return exceptionClass.equals(clazz); - } - - /** - * Validate whether a class overrides all declared methods in its superclass. - * - *

    This is a basic implementation that naively checks whether all the superclass methods have - * been overridden by the subclass. It is unlikely in practice that a delegating subclass needs - * to override all the methods in a superclass for postconditions to hold. - * - * @param tree a class tree - */ - private void checkSuperClassOverrides(ClassTree tree) { - TypeElement classTreeElt = TreeUtils.elementFromDeclaration(tree); - if (classTreeElt == null || classTreeElt.getSuperclass() == null) { - return; - } - DeclaredType superClassMirror = (DeclaredType) classTreeElt.getSuperclass(); - if (superClassMirror == null || superClassMirror.getKind() == TypeKind.NONE) { - return; - } - Set overriddenMethods = - getOverriddenMethods(tree).stream() - .map(ExecutableElement::getSimpleName) - .collect(Collectors.toSet()); - Set methodsDeclaredInSuperClass = - new HashSet<>( - ElementFilter.methodsIn(superClassMirror.asElement().getEnclosedElements())); - Set methodsThatMustBeOverriden = - methodsDeclaredInSuperClass.stream() - .filter(e -> atypeFactory.getDeclAnnotation(e, DelegatorMustOverride.class) != null) - .map(ExecutableElement::getSimpleName) - .collect(Collectors.toSet()); - - // TODO: comparing a set of names isn't ideal, what about overloading? - if (!overriddenMethods.containsAll(methodsThatMustBeOverriden)) { - checker.reportWarning( - tree, - "delegate.override", - tree.getSimpleName(), - TypesUtils.getQualifiedName(superClassMirror)); - } - } - - /** - * Return a set of all methods in the class that are marked with {@link Override}. - * - * @param tree the class tree - * @return a set of all methods in the class that are marked with {@link Override} - */ - private Set getOverriddenMethods(ClassTree tree) { - Set overriddenMethods = new HashSet<>(); - for (Tree member : tree.getMembers()) { - if (!(member instanceof MethodTree)) { - continue; - } - MethodTree method = (MethodTree) member; - if (isMarkedWithOverride(method)) { - overriddenMethods.add(TreeUtils.elementFromDeclaration(method)); - } - } - return overriddenMethods; - } - - /** - * Return true if a method is marked with {@link Override}. - * - * @param tree the method declaration - * @return true if the given method declaration is annotated with {@link Override} - */ - private boolean isMarkedWithOverride(MethodTree tree) { - Element method = TreeUtils.elementFromDeclaration(tree); - return atypeFactory.getDeclAnnotation(method, Override.class) != null; - } - } -} +public class DelegationChecker extends BaseTypeChecker {} diff --git a/framework/src/main/java/org/checkerframework/common/delegation/DelegationVisitor.java b/framework/src/main/java/org/checkerframework/common/delegation/DelegationVisitor.java new file mode 100644 index 00000000000..46aa7046a4c --- /dev/null +++ b/framework/src/main/java/org/checkerframework/common/delegation/DelegationVisitor.java @@ -0,0 +1,253 @@ +package org.checkerframework.common.delegation; + +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.ThrowTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.util.ElementFilter; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.common.delegation.qual.Delegate; +import org.checkerframework.common.delegation.qual.DelegatorMustOverride; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +public class DelegationVisitor extends BaseTypeVisitor { + + /** The maximum number of fields marked with {@link Delegate} permitted in a class. */ + private final int MAX_NUM_DELEGATE_FIELDS = 1; + + /** The field marked with {@link Delegate} for the current class. */ + private @Nullable VariableTree delegate; + + public DelegationVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + public void processClassTree(ClassTree tree) { + delegate = null; // Unset the previous delegate whenever a new class is visited + // TODO: what about inner classes? + List delegates = getDelegateFields(tree); + if (delegates.size() > MAX_NUM_DELEGATE_FIELDS) { + VariableTree latestDelegate = delegates.get(delegates.size() - 1); + checker.reportError(latestDelegate, "multiple.delegate.annotations"); + } else if (delegates.size() == 1) { + delegate = delegates.get(0); + checkSuperClassOverrides(tree); + } + // Do nothing if no delegate field is found + super.processClassTree(tree); + } + + @Override + public void processMethodTree(MethodTree tree) { + super.processMethodTree(tree); + if (delegate == null || !isMarkedWithOverride(tree)) { + return; + } + MethodInvocationTree candidateDelegateCall = getLastExpression(tree.getBody()); + boolean hasExceptionalExit = + hasExceptionalExit(tree.getBody(), UnsupportedOperationException.class); + if (hasExceptionalExit) { + return; + } + if (candidateDelegateCall == null) { + checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); + return; + } + Name enclosingMethodName = tree.getName(); + if (!isValidDelegateCall(enclosingMethodName, candidateDelegateCall)) { + checker.reportWarning(tree, "invalid.delegate", tree.getName(), delegate.getName()); + } + } + + /** + * Return true if the given method call is a valid delegate call for the enclosing method. + * + *

    A delegate method call must fulfill the following properties: its receiver must be the + * current field marked with {@link Delegate} in the class, and the name of the method call must + * match that of the enclosing method. + * + * @param enclosingMethodName the name of the enclosing method + * @param delegatedMethodCall the delegated method call + * @return true if the given method call is a valid delegate call for the enclosing method + */ + private boolean isValidDelegateCall( + Name enclosingMethodName, MethodInvocationTree delegatedMethodCall) { + assert delegate != null; // This method should only be invoked when delegate is non-null + ExpressionTree methodSelectTree = delegatedMethodCall.getMethodSelect(); + MemberSelectTree fieldAccessTree = (MemberSelectTree) methodSelectTree; + VariableElement delegatedField = TreeUtils.asFieldAccess(fieldAccessTree.getExpression()); + Name delegatedMethodName = TreeUtils.methodName(delegatedMethodCall); + // TODO: is there a better way to check? Comparing names seems fragile. + return enclosingMethodName.equals(delegatedMethodName) + && delegatedField != null + && delegatedField.getSimpleName().equals(delegate.getName()); + } + + /** + * Returns the fields of a class marked with a {@link Delegate} annotation. + * + * @param tree a class + * @return the fields of a class marked with a {@link Delegate} annotation + */ + private List getDelegateFields(ClassTree tree) { + List delegateFields = new ArrayList<>(); + for (VariableTree field : TreeUtils.fieldsFromTree(tree)) { + List annosOnField = + TreeUtils.annotationsFromTypeAnnotationTrees(field.getModifiers().getAnnotations()); + if (annosOnField.stream() + .anyMatch(anno -> atypeFactory.areSameByClass(anno, Delegate.class))) { + delegateFields.add(field); + } + } + return delegateFields; + } + + /** + * Returns the last expression in a method body. + * + *

    This method is used to identify a possible delegate method call. It will check whether a + * method has only one statement (a method invocation or a return statement), and return the + * expression that is associated with it. Otherwise, it will return null. + * + * @param tree the method body + * @return the last expression in the method body + */ + private @Nullable MethodInvocationTree getLastExpression(BlockTree tree) { + List stmts = tree.getStatements(); + if (stmts.size() != 1) { + return null; + } + StatementTree stmt = stmts.get(0); + ExpressionTree lastExprInMethod = null; + if (stmt instanceof ExpressionStatementTree) { + lastExprInMethod = ((ExpressionStatementTree) stmt).getExpression(); + } else if (stmt instanceof ReturnTree) { + lastExprInMethod = ((ReturnTree) stmt).getExpression(); + } + if (!(lastExprInMethod instanceof MethodInvocationTree)) { + return null; + } + return (MethodInvocationTree) lastExprInMethod; + } + + /** + * Return true if the last (and only) statement of the block throws an exception of the given + * class. + * + * @param tree a block tree + * @param clazz a class of exception (usually {@link UnsupportedOperationException}) + * @return true if the last and only statement throws an exception of the given class + */ + private boolean hasExceptionalExit(BlockTree tree, Class clazz) { + List stmts = tree.getStatements(); + if (stmts.size() != 1) { + return false; + } + StatementTree lastStmt = stmts.get(0); + if (!(lastStmt instanceof ThrowTree)) { + return false; + } + ThrowTree throwStmt = (ThrowTree) lastStmt; + AnnotatedTypeMirror throwType = atypeFactory.getAnnotatedType(throwStmt.getExpression()); + Class exceptionClass = TypesUtils.getClassFromType(throwType.getUnderlyingType()); + return exceptionClass.equals(clazz); + } + + /** + * Validate whether a class overrides all declared methods in its superclass. + * + *

    This is a basic implementation that naively checks whether all the superclass methods have + * been overridden by the subclass. It is unlikely in practice that a delegating subclass needs to + * override all the methods in a superclass for postconditions to hold. + * + * @param tree a class tree + */ + private void checkSuperClassOverrides(ClassTree tree) { + TypeElement classTreeElt = TreeUtils.elementFromDeclaration(tree); + if (classTreeElt == null || classTreeElt.getSuperclass() == null) { + return; + } + DeclaredType superClassMirror = (DeclaredType) classTreeElt.getSuperclass(); + if (superClassMirror == null || superClassMirror.getKind() == TypeKind.NONE) { + return; + } + Set overriddenMethods = + getOverriddenMethods(tree).stream() + .map(ExecutableElement::getSimpleName) + .collect(Collectors.toSet()); + Set methodsDeclaredInSuperClass = + new HashSet<>(ElementFilter.methodsIn(superClassMirror.asElement().getEnclosedElements())); + Set methodsThatMustBeOverriden = + methodsDeclaredInSuperClass.stream() + .filter(e -> atypeFactory.getDeclAnnotation(e, DelegatorMustOverride.class) != null) + .map(ExecutableElement::getSimpleName) + .collect(Collectors.toSet()); + + // TODO: comparing a set of names isn't ideal, what about overloading? + if (!overriddenMethods.containsAll(methodsThatMustBeOverriden)) { + checker.reportWarning( + tree, + "delegate.override", + tree.getSimpleName(), + TypesUtils.getQualifiedName(superClassMirror)); + } + } + + /** + * Return a set of all methods in the class that are marked with {@link Override}. + * + * @param tree the class tree + * @return a set of all methods in the class that are marked with {@link Override} + */ + private Set getOverriddenMethods(ClassTree tree) { + Set overriddenMethods = new HashSet<>(); + for (Tree member : tree.getMembers()) { + if (!(member instanceof MethodTree)) { + continue; + } + MethodTree method = (MethodTree) member; + if (isMarkedWithOverride(method)) { + overriddenMethods.add(TreeUtils.elementFromDeclaration(method)); + } + } + return overriddenMethods; + } + + /** + * Return true if a method is marked with {@link Override}. + * + * @param tree the method declaration + * @return true if the given method declaration is annotated with {@link Override} + */ + private boolean isMarkedWithOverride(MethodTree tree) { + Element method = TreeUtils.elementFromDeclaration(tree); + return atypeFactory.getDeclAnnotation(method, Override.class) != null; + } +}